diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java index 51996ed284..b601874f8d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.offline; import static com.google.android.exoplayer2.offline.Download.STATE_QUEUED; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import java.io.File; import java.io.IOException; @@ -97,10 +98,11 @@ public final class ActionFileUpgradeUtil { new Download( request, STATE_QUEUED, - Download.FAILURE_REASON_NONE, - Download.STOP_REASON_NONE, /* startTimeMs= */ nowMs, - /* updateTimeMs= */ nowMs); + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + Download.STOP_REASON_NONE, + Download.FAILURE_REASON_NONE); } downloadIndex.putDownload(download); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index 30297f19ce..6838c24628 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -27,7 +27,6 @@ import android.text.TextUtils; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -210,15 +209,15 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { values.put(COLUMN_CUSTOM_CACHE_KEY, download.request.customCacheKey); values.put(COLUMN_DATA, download.request.data); values.put(COLUMN_STATE, download.state); - values.put(COLUMN_DOWNLOAD_PERCENTAGE, download.getDownloadPercentage()); - values.put(COLUMN_DOWNLOADED_BYTES, download.getDownloadedBytes()); - values.put(COLUMN_TOTAL_BYTES, download.getTotalBytes()); - values.put(COLUMN_FAILURE_REASON, download.failureReason); - values.put(COLUMN_STOP_FLAGS, 0); - values.put(COLUMN_NOT_MET_REQUIREMENTS, 0); - values.put(COLUMN_STOP_REASON, download.stopReason); values.put(COLUMN_START_TIME_MS, download.startTimeMs); values.put(COLUMN_UPDATE_TIME_MS, download.updateTimeMs); + values.put(COLUMN_TOTAL_BYTES, download.contentLength); + values.put(COLUMN_STOP_REASON, download.stopReason); + values.put(COLUMN_FAILURE_REASON, download.failureReason); + values.put(COLUMN_DOWNLOAD_PERCENTAGE, download.getPercentDownloaded()); + values.put(COLUMN_DOWNLOADED_BYTES, download.getBytesDownloaded()); + values.put(COLUMN_STOP_FLAGS, 0); + values.put(COLUMN_NOT_MET_REQUIREMENTS, 0); try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); @@ -337,18 +336,18 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)), cursor.getString(COLUMN_INDEX_CUSTOM_CACHE_KEY), cursor.getBlob(COLUMN_INDEX_DATA)); - CachingCounters cachingCounters = new CachingCounters(); - cachingCounters.alreadyCachedBytes = cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES); - cachingCounters.contentLength = cursor.getLong(COLUMN_INDEX_TOTAL_BYTES); - cachingCounters.percentage = cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE); + DownloadProgress downloadProgress = new DownloadProgress(); + downloadProgress.bytesDownloaded = cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES); + downloadProgress.percentDownloaded = cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE); return new Download( request, cursor.getInt(COLUMN_INDEX_STATE), - cursor.getInt(COLUMN_INDEX_FAILURE_REASON), - cursor.getInt(COLUMN_INDEX_STOP_REASON), cursor.getLong(COLUMN_INDEX_START_TIME_MS), cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), - cachingCounters); + cursor.getLong(COLUMN_INDEX_TOTAL_BYTES), + cursor.getInt(COLUMN_INDEX_STOP_REASON), + cursor.getInt(COLUMN_INDEX_FAILURE_REASON), + downloadProgress); } private static String encodeStreamKeys(List streamKeys) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java index 343b9d6a49..9f6b473208 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Download.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.offline; import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -96,60 +95,65 @@ public final class Download { /** The download request. */ public final DownloadRequest request; - /** The state of the download. */ @State public final int state; /** The first time when download entry is created. */ public final long startTimeMs; /** The last update time. */ public final long updateTimeMs; + /** The total size of the content in bytes, or {@link C#LENGTH_UNSET} if unknown. */ + public final long contentLength; + /** The reason the download is stopped, or {@link #STOP_REASON_NONE}. */ + public final int stopReason; /** * If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise {@link * #FAILURE_REASON_NONE}. */ @FailureReason public final int failureReason; - /** The reason the download is stopped, or {@link #STOP_REASON_NONE}. */ - public final int stopReason; - /* package */ CachingCounters counters; + /* package */ final DownloadProgress progress; - /* package */ Download( + public Download( DownloadRequest request, @State int state, - @FailureReason int failureReason, - int stopReason, long startTimeMs, - long updateTimeMs) { + long updateTimeMs, + long contentLength, + int stopReason, + @FailureReason int failureReason) { this( request, state, - failureReason, - stopReason, startTimeMs, updateTimeMs, - new CachingCounters()); + contentLength, + stopReason, + failureReason, + new DownloadProgress()); } - /* package */ Download( + public Download( DownloadRequest request, @State int state, - @FailureReason int failureReason, - int stopReason, long startTimeMs, long updateTimeMs, - CachingCounters counters) { - Assertions.checkNotNull(counters); + long contentLength, + int stopReason, + @FailureReason int failureReason, + DownloadProgress progress) { + Assertions.checkNotNull(progress); Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); if (stopReason != 0) { Assertions.checkState(state != STATE_DOWNLOADING && state != STATE_QUEUED); } this.request = request; this.state = state; - this.failureReason = failureReason; - this.stopReason = stopReason; this.startTimeMs = startTimeMs; this.updateTimeMs = updateTimeMs; - this.counters = counters; + this.contentLength = contentLength; + this.stopReason = stopReason; + this.failureReason = failureReason; + this.progress = progress; } /** Returns whether the download is completed or failed. These are terminal states. */ @@ -158,30 +162,15 @@ public final class Download { } /** Returns the total number of downloaded bytes. */ - public long getDownloadedBytes() { - return counters.totalCachedBytes(); - } - - /** Returns the total size of the media, or {@link C#LENGTH_UNSET} if unknown. */ - public long getTotalBytes() { - return counters.contentLength; + public long getBytesDownloaded() { + return progress.bytesDownloaded; } /** * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is * available. */ - public float getDownloadPercentage() { - return counters.percentage; - } - - /** - * Sets counters which are updated by a {@link Downloader}. - * - * @param counters An instance of {@link CachingCounters}. - */ - protected void setCounters(CachingCounters counters) { - Assertions.checkNotNull(counters); - this.counters = counters; + public float getPercentDownloaded() { + return progress.percentDownloaded; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index f914c861f9..d4df5cd18b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -36,7 +36,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.RequirementsWatcher; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -131,7 +130,8 @@ public final class DownloadManager { private static final int MSG_ADD_DOWNLOAD = 4; private static final int MSG_REMOVE_DOWNLOAD = 5; private static final int MSG_DOWNLOAD_THREAD_STOPPED = 6; - private static final int MSG_RELEASE = 7; + private static final int MSG_CONTENT_LENGTH_CHANGED = 7; + private static final int MSG_RELEASE = 8; @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -539,6 +539,11 @@ public final class DownloadManager { onDownloadThreadStoppedInternal(downloadThread); processedExternalMessage = false; // This message is posted internally. break; + case MSG_CONTENT_LENGTH_CHANGED: + downloadThread = (DownloadThread) message.obj; + onDownloadThreadContentLengthChangedInternal(downloadThread); + processedExternalMessage = false; // This message is posted internally. + break; case MSG_RELEASE: releaseInternal(); return true; // Don't post back to mainHandler on release. @@ -634,10 +639,11 @@ public final class DownloadManager { new Download( request, stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, - Download.FAILURE_REASON_NONE, - stopReason, /* startTimeMs= */ nowMs, - /* updateTimeMs= */ nowMs); + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + stopReason, + Download.FAILURE_REASON_NONE); logd("Download state is created for " + request.id); } else { download = mergeRequest(download, request, stopReason); @@ -682,6 +688,11 @@ public final class DownloadManager { } } + private void onDownloadThreadContentLengthChangedInternal(DownloadThread downloadThread) { + String downloadId = downloadThread.request.id; + getDownload(downloadId).setContentLength(downloadThread.contentLength); + } + private void releaseInternal() { for (DownloadThread downloadThread : downloadThreads.values()) { downloadThread.cancel(/* released= */ true); @@ -737,10 +748,11 @@ public final class DownloadManager { parallelDownloads++; } Downloader downloader = downloaderFactory.createDownloader(request); + DownloadProgress downloadProgress = downloadInternal.download.progress; DownloadThread downloadThread = - new DownloadThread(request, downloader, isRemove, minRetryCount, internalHandler); + new DownloadThread( + request, downloader, downloadProgress, isRemove, minRetryCount, internalHandler); downloadThreads.put(downloadId, downloadThread); - downloadInternal.setCounters(downloadThread.downloader.getCounters()); downloadThread.start(); logd("Download is started", downloadInternal); return START_THREAD_SUCCEEDED; @@ -802,22 +814,23 @@ public final class DownloadManager { return new Download( download.request.copyWithMergedRequest(request), state, - FAILURE_REASON_NONE, - stopReason, startTimeMs, /* updateTimeMs= */ nowMs, - download.counters); + /* contentLength= */ C.LENGTH_UNSET, + stopReason, + FAILURE_REASON_NONE); } private static Download copyWithState(Download download, @Download.State int state) { return new Download( download.request, state, - FAILURE_REASON_NONE, - download.stopReason, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), - download.counters); + download.contentLength, + download.stopReason, + FAILURE_REASON_NONE, + download.progress); } private static void logd(String message) { @@ -850,13 +863,17 @@ public final class DownloadManager { // TODO: Get rid of these and use download directly. @Download.State private int state; + private long contentLength; private int stopReason; @MonotonicNonNull @Download.FailureReason private int failureReason; private DownloadInternal(DownloadManager downloadManager, Download download) { this.downloadManager = downloadManager; this.download = download; + state = download.state; + contentLength = download.contentLength; stopReason = download.stopReason; + failureReason = download.failureReason; } private void initialize() { @@ -877,11 +894,12 @@ public final class DownloadManager { new Download( download.request, state, - state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason, - stopReason, download.startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), - download.counters); + contentLength, + stopReason, + state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason, + download.progress); return download; } @@ -911,8 +929,12 @@ public final class DownloadManager { return state == STATE_REMOVING || state == STATE_RESTARTING; } - public void setCounters(CachingCounters counters) { - download.setCounters(counters); + public void setContentLength(long contentLength) { + if (this.contentLength == contentLength) { + return; + } + this.contentLength = contentLength; + downloadManager.onDownloadChangedInternal(this, getUpdatedDownload()); } private void updateStopState() { @@ -992,28 +1014,34 @@ public final class DownloadManager { } } - private static class DownloadThread extends Thread { + private static class DownloadThread 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; - private volatile Handler onStoppedHandler; + private volatile Handler updateHandler; private volatile boolean isCanceled; private Throwable finalError; + private long contentLength; + private DownloadThread( DownloadRequest request, Downloader downloader, + DownloadProgress downloadProgress, boolean isRemove, int minRetryCount, - Handler onStoppedHandler) { + Handler updateHandler) { this.request = request; - this.isRemove = isRemove; this.downloader = downloader; + this.downloadProgress = downloadProgress; + this.isRemove = isRemove; this.minRetryCount = minRetryCount; - this.onStoppedHandler = onStoppedHandler; + this.updateHandler = updateHandler; + contentLength = C.LENGTH_UNSET; } public void cancel(boolean released) { @@ -1022,7 +1050,7 @@ public final class DownloadManager { // 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. - onStoppedHandler = null; + updateHandler = null; } isCanceled = true; downloader.cancel(); @@ -1042,14 +1070,14 @@ public final class DownloadManager { long errorPosition = C.LENGTH_UNSET; while (!isCanceled) { try { - downloader.download(); + downloader.download(/* progressListener= */ this); break; } catch (IOException e) { if (!isCanceled) { - long downloadedBytes = downloader.getDownloadedBytes(); - if (downloadedBytes != errorPosition) { - logd("Reset error count. downloadedBytes = " + downloadedBytes, request); - errorPosition = downloadedBytes; + long bytesDownloaded = downloadProgress.bytesDownloaded; + if (bytesDownloaded != errorPosition) { + logd("Reset error count. bytesDownloaded = " + bytesDownloaded, request); + errorPosition = bytesDownloaded; errorCount = 0; } if (++errorCount > minRetryCount) { @@ -1064,13 +1092,26 @@ public final class DownloadManager { } catch (Throwable e) { finalError = e; } - Handler onStoppedHandler = this.onStoppedHandler; - if (onStoppedHandler != null) { - onStoppedHandler.obtainMessage(MSG_DOWNLOAD_THREAD_STOPPED, this).sendToTarget(); + Handler updateHandler = this.updateHandler; + if (updateHandler != null) { + updateHandler.obtainMessage(MSG_DOWNLOAD_THREAD_STOPPED, this).sendToTarget(); } } - private int getRetryDelayMillis(int errorCount) { + @Override + public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) { + downloadProgress.bytesDownloaded = bytesDownloaded; + downloadProgress.percentDownloaded = percentDownloaded; + if (contentLength != this.contentLength) { + this.contentLength = contentLength; + Handler updateHandler = this.updateHandler; + if (updateHandler != null) { + updateHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget(); + } + } + } + + private static int getRetryDelayMillis(int errorCount) { return Math.min((errorCount - 1) * 1000, 5000); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java new file mode 100644 index 0000000000..9d946daa28 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2019 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 com.google.android.exoplayer2.C; + +/** Mutable {@link Download} progress. */ +public class DownloadProgress { + + /** The number of bytes that have been downloaded. */ + public long bytesDownloaded; + + /** The percentage that has been downloaded, or {@link C#PERCENTAGE_UNSET} if unknown. */ + public float percentDownloaded; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java index 39f562ac19..fa10d5842b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java @@ -15,44 +15,44 @@ */ package com.google.android.exoplayer2.offline; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import java.io.IOException; -/** - * An interface for stream downloaders. - */ +/** Downloads and removes a piece of content. */ public interface Downloader { + /** Receives progress updates during download operations. */ + interface ProgressListener { + + /** + * Called when progress is made during a download operation. + * + * @param contentLength The length of the content in bytes, or {@link C#LENGTH_UNSET} if + * unknown. + * @param bytesDownloaded The number of bytes that have been downloaded. + * @param percentDownloaded The percentage of the content that has been downloaded, or {@link + * C#PERCENTAGE_UNSET}. + */ + void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded); + } + /** - * Downloads the media. + * Downloads the content. * - * @throws DownloadException Thrown if the media cannot be downloaded. + * @param progressListener A listener to receive progress updates, or {@code null}. + * @throws DownloadException Thrown if the content cannot be downloaded. * @throws InterruptedException If the thread has been interrupted. * @throws IOException Thrown when there is an io error while downloading. */ - void download() throws InterruptedException, IOException; + void download(@Nullable ProgressListener progressListener) + throws InterruptedException, IOException; - /** Interrupts any current download operation and prevents future operations from running. */ + /** Cancels the download operation and prevents future download operations from running. */ void cancel(); - /** Returns the total number of downloaded bytes. */ - long getDownloadedBytes(); - - /** Returns the total size of the media, or {@link C#LENGTH_UNSET} if unknown. */ - long getTotalBytes(); - /** - * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is - * available. - */ - float getDownloadPercentage(); - - /** Returns a {@link CachingCounters} which holds download counters. */ - CachingCounters getCounters(); - - /** - * Removes the media. + * Removes the content. * * @throws InterruptedException Thrown if the thread was interrupted. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index 9794b19b62..17f4047bc0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; import com.google.android.exoplayer2.upstream.cache.CacheUtil; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; @@ -40,7 +39,6 @@ public final class ProgressiveDownloader implements Downloader { private final CacheDataSource dataSource; private final CacheKeyFactory cacheKeyFactory; private final PriorityTaskManager priorityTaskManager; - private final CacheUtil.CachingCounters cachingCounters; private final AtomicBoolean isCanceled; /** @@ -62,12 +60,12 @@ public final class ProgressiveDownloader implements Downloader { this.dataSource = constructorHelper.createCacheDataSource(); this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); - cachingCounters = new CachingCounters(); isCanceled = new AtomicBoolean(); } @Override - public void download() throws InterruptedException, IOException { + public void download(@Nullable ProgressListener progressListener) + throws InterruptedException, IOException { priorityTaskManager.add(C.PRIORITY_DOWNLOAD); try { CacheUtil.cache( @@ -78,7 +76,7 @@ public final class ProgressiveDownloader implements Downloader { new byte[BUFFER_SIZE_BYTES], priorityTaskManager, C.PRIORITY_DOWNLOAD, - cachingCounters, + progressListener == null ? null : new ProgressForwarder(progressListener), isCanceled, /* enableEOFException= */ true); } finally { @@ -91,28 +89,26 @@ public final class ProgressiveDownloader implements Downloader { isCanceled.set(true); } - @Override - public long getDownloadedBytes() { - return cachingCounters.totalCachedBytes(); - } - - @Override - public long getTotalBytes() { - return cachingCounters.contentLength; - } - - @Override - public float getDownloadPercentage() { - return cachingCounters.percentage; - } - - @Override - public CachingCounters getCounters() { - return cachingCounters; - } - @Override public void remove() { CacheUtil.remove(dataSpec, cache, cacheKeyFactory); } + + private static final class ProgressForwarder implements CacheUtil.ProgressListener { + + private final ProgressListener progessListener; + + public ProgressForwarder(ProgressListener progressListener) { + this.progessListener = progressListener; + } + + @Override + public void onProgress(long contentLength, long bytesCached, long newBytesCached) { + float percentDownloaded = + contentLength == C.LENGTH_UNSET || contentLength == 0 + ? C.PERCENTAGE_UNSET + : ((bytesCached * 100f) / contentLength); + progessListener.onProgress(contentLength, bytesCached, percentDownloaded); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index 4dbae47775..1643812ece 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -24,7 +26,6 @@ import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; import com.google.android.exoplayer2.upstream.cache.CacheUtil; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -42,6 +43,7 @@ public abstract class SegmentDownloader> impleme /** Smallest unit of content to be downloaded. */ protected static class Segment implements Comparable { + /** The start time of the segment in microseconds. */ public final long startTimeUs; @@ -70,10 +72,6 @@ public abstract class SegmentDownloader> impleme private final PriorityTaskManager priorityTaskManager; private final ArrayList streamKeys; private final AtomicBoolean isCanceled; - private final CacheUtil.CachingCounters counters; - - private volatile int totalSegments; - private volatile int downloadedSegments; /** * @param manifestUri The {@link Uri} of the manifest to be downloaded. @@ -90,9 +88,7 @@ public abstract class SegmentDownloader> impleme this.offlineDataSource = constructorHelper.createOfflineCacheDataSource(); this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); - totalSegments = C.LENGTH_UNSET; isCanceled = new AtomicBoolean(); - counters = new CachingCounters(); } /** @@ -102,35 +98,71 @@ public abstract class SegmentDownloader> impleme * @throws IOException Thrown when there is an error downloading. * @throws InterruptedException If the thread has been interrupted. */ - // downloadedSegments and downloadedBytes are only written from this method, and this method - // should not be called from more than one thread. Hence non-atomic updates are valid. - @SuppressWarnings("NonAtomicVolatileUpdate") @Override - public final void download() throws IOException, InterruptedException { + public final void download(@Nullable ProgressListener progressListener) + throws IOException, InterruptedException { priorityTaskManager.add(C.PRIORITY_DOWNLOAD); - try { - List segments = initDownload(); + // Get the manifest and all of the segments. + M manifest = getManifest(dataSource, manifestDataSpec); + if (!streamKeys.isEmpty()) { + manifest = manifest.copy(streamKeys); + } + List segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false); + + // Scan the segments, removing any that are fully downloaded. + int totalSegments = segments.size(); + int segmentsDownloaded = 0; + long contentLength = 0; + long bytesDownloaded = 0; + for (int i = segments.size() - 1; i >= 0; i--) { + Segment segment = segments.get(i); + Pair segmentLengthAndBytesDownloaded = + CacheUtil.getCached(segment.dataSpec, cache, cacheKeyFactory); + long segmentLength = segmentLengthAndBytesDownloaded.first; + long segmentBytesDownloaded = segmentLengthAndBytesDownloaded.second; + bytesDownloaded += segmentBytesDownloaded; + if (segmentLength != C.LENGTH_UNSET) { + if (segmentLength == segmentBytesDownloaded) { + // The segment is fully downloaded. + segmentsDownloaded++; + segments.remove(i); + } + if (contentLength != C.LENGTH_UNSET) { + contentLength += segmentLength; + } + } else { + contentLength = C.LENGTH_UNSET; + } + } Collections.sort(segments); + + // Download the segments. + ProgressNotifier progressNotifier = null; + if (progressListener != null) { + progressNotifier = + new ProgressNotifier( + progressListener, + contentLength, + totalSegments, + bytesDownloaded, + segmentsDownloaded); + } byte[] buffer = new byte[BUFFER_SIZE_BYTES]; - CachingCounters cachingCounters = new CachingCounters(); for (int i = 0; i < segments.size(); i++) { - try { - CacheUtil.cache( - segments.get(i).dataSpec, - cache, - cacheKeyFactory, - dataSource, - buffer, - priorityTaskManager, - C.PRIORITY_DOWNLOAD, - cachingCounters, - isCanceled, - true); - downloadedSegments++; - } finally { - counters.newlyCachedBytes += cachingCounters.newlyCachedBytes; - updatePercentage(); + CacheUtil.cache( + segments.get(i).dataSpec, + cache, + cacheKeyFactory, + dataSource, + buffer, + priorityTaskManager, + C.PRIORITY_DOWNLOAD, + progressNotifier, + isCanceled, + true); + if (progressNotifier != null) { + progressNotifier.onSegmentDownloaded(); } } } finally { @@ -143,26 +175,6 @@ public abstract class SegmentDownloader> impleme isCanceled.set(true); } - @Override - public final long getDownloadedBytes() { - return counters.totalCachedBytes(); - } - - @Override - public long getTotalBytes() { - return counters.contentLength; - } - - @Override - public final float getDownloadPercentage() { - return counters.percentage; - } - - @Override - public CachingCounters getCounters() { - return counters; - } - @Override public final void remove() throws InterruptedException { try { @@ -199,64 +211,15 @@ public abstract class SegmentDownloader> impleme * @param allowIncompleteList Whether to continue in the case that a load error prevents all * segments from being listed. If true then a partial segment list will be returned. If false * an {@link IOException} will be thrown. + * @return The list of downloadable {@link Segment}s. * @throws InterruptedException Thrown if the thread was interrupted. * @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if * the media is not in a form that allows for its segments to be listed. - * @return The list of downloadable {@link Segment}s. */ protected abstract List getSegments( DataSource dataSource, M manifest, boolean allowIncompleteList) throws InterruptedException, IOException; - /** Initializes the download, returning a list of {@link Segment}s that need to be downloaded. */ - // Writes to downloadedSegments and downloadedBytes are safe. See the comment on download(). - @SuppressWarnings("NonAtomicVolatileUpdate") - private List initDownload() throws IOException, InterruptedException { - M manifest = getManifest(dataSource, manifestDataSpec); - if (!streamKeys.isEmpty()) { - manifest = manifest.copy(streamKeys); - } - List segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false); - CachingCounters cachingCounters = new CachingCounters(); - totalSegments = segments.size(); - downloadedSegments = 0; - counters.alreadyCachedBytes = 0; - counters.newlyCachedBytes = 0; - long totalBytes = 0; - for (int i = segments.size() - 1; i >= 0; i--) { - Segment segment = segments.get(i); - CacheUtil.getCached(segment.dataSpec, cache, cacheKeyFactory, cachingCounters); - counters.alreadyCachedBytes += cachingCounters.alreadyCachedBytes; - if (cachingCounters.contentLength != C.LENGTH_UNSET) { - if (cachingCounters.alreadyCachedBytes == cachingCounters.contentLength) { - // The segment is fully downloaded. - downloadedSegments++; - segments.remove(i); - } - if (totalBytes != C.LENGTH_UNSET) { - totalBytes += cachingCounters.contentLength; - } - } else { - totalBytes = C.LENGTH_UNSET; - } - } - counters.contentLength = totalBytes; - updatePercentage(); - return segments; - } - - private void updatePercentage() { - counters.updatePercentage(); - if (counters.percentage == C.PERCENTAGE_UNSET) { - int totalSegments = this.totalSegments; - int downloadedSegments = this.downloadedSegments; - if (totalSegments != C.LENGTH_UNSET && downloadedSegments != C.LENGTH_UNSET) { - counters.percentage = - totalSegments == 0 ? 100f : (downloadedSegments * 100f) / totalSegments; - } - } - } - private void removeDataSpec(DataSpec dataSpec) { CacheUtil.remove(dataSpec, cache, cacheKeyFactory); } @@ -269,4 +232,49 @@ public abstract class SegmentDownloader> impleme /* key= */ null, /* flags= */ DataSpec.FLAG_ALLOW_GZIP); } + + private static final class ProgressNotifier implements CacheUtil.ProgressListener { + + private final ProgressListener progressListener; + + private final long contentLength; + private final int totalSegments; + + private long bytesDownloaded; + private int segmentsDownloaded; + + public ProgressNotifier( + ProgressListener progressListener, + long contentLength, + int totalSegments, + long bytesDownloaded, + int segmentsDownloaded) { + this.progressListener = progressListener; + this.contentLength = contentLength; + this.totalSegments = totalSegments; + this.bytesDownloaded = bytesDownloaded; + this.segmentsDownloaded = segmentsDownloaded; + } + + @Override + public void onProgress(long requestLength, long bytesCached, long newBytesCached) { + bytesDownloaded += newBytesCached; + progressListener.onProgress(contentLength, bytesDownloaded, getPercentDownloaded()); + } + + public void onSegmentDownloaded() { + segmentsDownloaded++; + progressListener.onProgress(contentLength, bytesDownloaded, getPercentDownloaded()); + } + + private float getPercentDownloaded() { + if (contentLength != C.LENGTH_UNSET && contentLength != 0) { + return (bytesDownloaded * 100f) / contentLength; + } else if (totalSegments != 0) { + return (segmentsDownloaded * 100f) / totalSegments; + } else { + return C.PERCENTAGE_UNSET; + } + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index f715da118b..219d736835 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; import androidx.annotation.Nullable; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -31,36 +32,21 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * Caching related utility methods. */ -@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) public final class CacheUtil { - /** Counters used during caching. */ - public static class CachingCounters { - /** The number of bytes already in the cache. */ - public volatile long alreadyCachedBytes; - /** The number of newly cached bytes. */ - public volatile long newlyCachedBytes; - /** The length of the content being cached in bytes, or {@link C#LENGTH_UNSET} if unknown. */ - public volatile long contentLength = C.LENGTH_UNSET; - /** The percentage of cached data, or {@link C#PERCENTAGE_UNSET} if unavailable. */ - public volatile float percentage; + /** Receives progress updates during cache operations. */ + public interface ProgressListener { /** - * Returns the sum of {@link #alreadyCachedBytes} and {@link #newlyCachedBytes}. + * Called when progress is made during a cache operation. + * + * @param requestLength The length of the content being cached in bytes, or {@link + * C#LENGTH_UNSET} if unknown. + * @param bytesCached The number of bytes that are cached. + * @param newBytesCached The number of bytes that have been newly cached since the last progress + * update. */ - public long totalCachedBytes() { - return alreadyCachedBytes + newlyCachedBytes; - } - - /** Updates {@link #percentage} value using other values. */ - public void updatePercentage() { - // Take local snapshot of the volatile field - long contentLength = this.contentLength; - percentage = - contentLength == C.LENGTH_UNSET - ? C.PERCENTAGE_UNSET - : ((totalCachedBytes() * 100f) / contentLength); - } + void onProgress(long requestLength, long bytesCached, long newBytesCached); } /** Default buffer size to be used while caching. */ @@ -80,48 +66,43 @@ public final class CacheUtil { } /** - * Sets a {@link CachingCounters} to contain the number of bytes already downloaded and the length - * for the content defined by a {@code dataSpec}. {@link CachingCounters#newlyCachedBytes} is - * reset to 0. + * Queries the cache to obtain the request length and the number of bytes already cached for a + * given {@link DataSpec}. * * @param dataSpec Defines the data to be checked. * @param cache A {@link Cache} which has the data. * @param cacheKeyFactory An optional factory for cache keys. - * @param counters The {@link CachingCounters} to update. + * @return A pair containing the request length and the number of bytes that are already cached. */ - public static void getCached( - DataSpec dataSpec, - Cache cache, - @Nullable CacheKeyFactory cacheKeyFactory, - CachingCounters counters) { + public static Pair getCached( + DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) { String key = buildCacheKey(dataSpec, cacheKeyFactory); long position = dataSpec.absoluteStreamPosition; - long bytesLeft; + long requestLength; if (dataSpec.length != C.LENGTH_UNSET) { - bytesLeft = dataSpec.length; + requestLength = dataSpec.length; } else { long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); - bytesLeft = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; + requestLength = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; } - counters.contentLength = bytesLeft; - counters.alreadyCachedBytes = 0; - counters.newlyCachedBytes = 0; + long bytesAlreadyCached = 0; + long bytesLeft = requestLength; while (bytesLeft != 0) { long blockLength = cache.getCachedLength( key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE); if (blockLength > 0) { - counters.alreadyCachedBytes += blockLength; + bytesAlreadyCached += blockLength; } else { blockLength = -blockLength; if (blockLength == Long.MAX_VALUE) { - return; + break; } } position += blockLength; bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength; } - counters.updatePercentage(); + return Pair.create(requestLength, bytesAlreadyCached); } /** @@ -132,7 +113,7 @@ public final class CacheUtil { * @param cache A {@link Cache} to store the data. * @param cacheKeyFactory An optional factory for cache keys. * @param upstream A {@link DataSource} for reading data not in the cache. - * @param counters If not null, updated during caching. + * @param progressListener A listener to receive progress updates, or {@code null}. * @param isCanceled An optional flag that will interrupt caching if set to true. * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. @@ -142,7 +123,7 @@ public final class CacheUtil { Cache cache, @Nullable CacheKeyFactory cacheKeyFactory, DataSource upstream, - @Nullable CachingCounters counters, + @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled) throws IOException, InterruptedException { cache( @@ -153,7 +134,7 @@ public final class CacheUtil { new byte[DEFAULT_BUFFER_SIZE_BYTES], /* priorityTaskManager= */ null, /* priority= */ 0, - counters, + progressListener, isCanceled, /* enableEOFException= */ false); } @@ -176,7 +157,7 @@ public final class CacheUtil { * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with * caching. * @param priority The priority of this task. Used with {@code priorityTaskManager}. - * @param counters If not null, updated during caching. + * @param progressListener A listener to receive progress updates, or {@code null}. * @param isCanceled An optional flag that will interrupt caching if set to true. * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been * reached unexpectedly. @@ -191,19 +172,18 @@ public final class CacheUtil { byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, - @Nullable CachingCounters counters, + @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled, boolean enableEOFException) throws IOException, InterruptedException { Assertions.checkNotNull(dataSource); Assertions.checkNotNull(buffer); - if (counters != null) { - // Initialize the CachingCounter values. - getCached(dataSpec, cache, cacheKeyFactory, counters); - } else { - // Dummy CachingCounters. No need to initialize as they will not be visible to the caller. - counters = new CachingCounters(); + ProgressNotifier progressNotifier = null; + if (progressListener != null) { + progressNotifier = new ProgressNotifier(progressListener); + Pair lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory); + progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second); } String key = buildCacheKey(dataSpec, cacheKeyFactory); @@ -234,7 +214,7 @@ public final class CacheUtil { buffer, priorityTaskManager, priority, - counters, + progressNotifier, isCanceled); if (read < blockLength) { // Reached to the end of the data. @@ -261,7 +241,7 @@ public final class CacheUtil { * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with * caching. * @param priority The priority of this task. - * @param counters Counters to be set during reading. + * @param progressNotifier A notifier through which to report progress updates, or {@code null}. * @param isCanceled An optional flag that will interrupt caching if set to true. * @return Number of read bytes, or 0 if no data is available because the end of the opened range * has been reached. @@ -274,7 +254,7 @@ public final class CacheUtil { byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, - CachingCounters counters, + @Nullable ProgressNotifier progressNotifier, AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; @@ -298,8 +278,8 @@ public final class CacheUtil { dataSpec.key, dataSpec.flags); long resolvedLength = dataSource.open(dataSpec); - if (counters.contentLength == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) { - counters.contentLength = positionOffset + resolvedLength; + if (progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { + progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength); } long totalBytesRead = 0; while (totalBytesRead != length) { @@ -312,14 +292,15 @@ public final class CacheUtil { ? (int) Math.min(buffer.length, length - totalBytesRead) : buffer.length); if (bytesRead == C.RESULT_END_OF_INPUT) { - if (counters.contentLength == C.LENGTH_UNSET) { - counters.contentLength = positionOffset + totalBytesRead; + if (progressNotifier != null) { + progressNotifier.onRequestLengthResolved(positionOffset + totalBytesRead); } break; } totalBytesRead += bytesRead; - counters.newlyCachedBytes += bytesRead; - counters.updatePercentage(); + if (progressNotifier != null) { + progressNotifier.onBytesCached(bytesRead); + } } return totalBytesRead; } catch (PriorityTaskManager.PriorityTooLowException exception) { @@ -374,4 +355,34 @@ public final class CacheUtil { private CacheUtil() {} + private static final class ProgressNotifier { + /** The listener to notify when progress is made. */ + private final ProgressListener listener; + /** The length of the content being cached in bytes, or {@link C#LENGTH_UNSET} if unknown. */ + private long requestLength; + /** The number of bytes that are cached. */ + private long bytesCached; + + public ProgressNotifier(ProgressListener listener) { + this.listener = listener; + } + + public void init(long requestLength, long bytesCached) { + this.requestLength = requestLength; + this.bytesCached = bytesCached; + listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0); + } + + public void onRequestLengthResolved(long requestLength) { + if (this.requestLength == C.LENGTH_UNSET && requestLength != C.LENGTH_UNSET) { + this.requestLength = requestLength; + listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0); + } + } + + public void onBytesCached(long newBytesCached) { + bytesCached += newBytesCached; + listener.onProgress(requestLength, bytesCached, newBytesCached); + } + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java index 73c73b6647..f163e8d206 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java @@ -76,9 +76,9 @@ public class DefaultDownloadIndexTest { .setUri("different uri") .setCacheKey("different cacheKey") .setState(Download.STATE_FAILED) - .setDownloadPercentage(50) - .setDownloadedBytes(200) - .setTotalBytes(400) + .setPercentDownloaded(50) + .setBytesDownloaded(200) + .setContentLength(400) .setFailureReason(Download.FAILURE_REASON_UNKNOWN) .setStopReason(0x12345678) .setStartTimeMs(10) @@ -300,10 +300,10 @@ public class DefaultDownloadIndexTest { assertThat(download.state).isEqualTo(that.state); assertThat(download.startTimeMs).isEqualTo(that.startTimeMs); assertThat(download.updateTimeMs).isEqualTo(that.updateTimeMs); - assertThat(download.failureReason).isEqualTo(that.failureReason); + assertThat(download.contentLength).isEqualTo(that.contentLength); assertThat(download.stopReason).isEqualTo(that.stopReason); - assertThat(download.getDownloadPercentage()).isEqualTo(that.getDownloadPercentage()); - assertThat(download.getDownloadedBytes()).isEqualTo(that.getDownloadedBytes()); - assertThat(download.getTotalBytes()).isEqualTo(that.getTotalBytes()); + assertThat(download.failureReason).isEqualTo(that.failureReason); + assertThat(download.getPercentDownloaded()).isEqualTo(that.getPercentDownloaded()); + assertThat(download.getBytesDownloaded()).isEqualTo(that.getBytesDownloaded()); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java index b5d84fa4bc..f901b00f53 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; +import com.google.android.exoplayer2.C; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -29,52 +29,61 @@ import java.util.List; * creation for tests. Tests must avoid depending on the default values but explicitly set tested * parameters during test initialization. */ -class DownloadBuilder { - private final CachingCounters counters; +/* package */ final class DownloadBuilder { + + private final DownloadProgress progress; + private String id; private String type; private Uri uri; - @Nullable private String cacheKey; - private int state; - private int failureReason; - private int stopReason; - private long startTimeMs; - private long updateTimeMs; private List streamKeys; + @Nullable private String cacheKey; private byte[] customMetadata; - DownloadBuilder(String id) { - this(id, "type", Uri.parse("uri"), /* cacheKey= */ null, new byte[0], Collections.emptyList()); + private int state; + private long startTimeMs; + private long updateTimeMs; + private long contentLength; + private int stopReason; + private int failureReason; + + /* package */ DownloadBuilder(String id) { + this( + id, + "type", + Uri.parse("uri"), + /* streamKeys= */ Collections.emptyList(), + /* cacheKey= */ null, + new byte[0]); } - DownloadBuilder(DownloadRequest request) { + /* package */ DownloadBuilder(DownloadRequest request) { this( request.id, request.type, request.uri, + request.streamKeys, request.customCacheKey, - request.data, - request.streamKeys); + request.data); } - DownloadBuilder( + /* package */ DownloadBuilder( String id, String type, Uri uri, + List streamKeys, String cacheKey, - byte[] customMetadata, - List streamKeys) { + byte[] customMetadata) { this.id = id; this.type = type; this.uri = uri; - this.cacheKey = cacheKey; - this.state = Download.STATE_QUEUED; - this.failureReason = Download.FAILURE_REASON_NONE; - this.startTimeMs = (long) 0; - this.updateTimeMs = (long) 0; this.streamKeys = streamKeys; + this.cacheKey = cacheKey; this.customMetadata = customMetadata; - this.counters = new CachingCounters(); + this.state = Download.STATE_QUEUED; + this.contentLength = C.LENGTH_UNSET; + this.failureReason = Download.FAILURE_REASON_NONE; + this.progress = new DownloadProgress(); } public DownloadBuilder setId(String id) { @@ -107,18 +116,18 @@ class DownloadBuilder { return this; } - public DownloadBuilder setDownloadPercentage(float downloadPercentage) { - counters.percentage = downloadPercentage; + public DownloadBuilder setPercentDownloaded(float percentDownloaded) { + progress.percentDownloaded = percentDownloaded; return this; } - public DownloadBuilder setDownloadedBytes(long downloadedBytes) { - counters.alreadyCachedBytes = downloadedBytes; + public DownloadBuilder setBytesDownloaded(long bytesDownloaded) { + progress.bytesDownloaded = bytesDownloaded; return this; } - public DownloadBuilder setTotalBytes(long totalBytes) { - counters.contentLength = totalBytes; + public DownloadBuilder setContentLength(long contentLength) { + this.contentLength = contentLength; return this; } @@ -156,6 +165,13 @@ class DownloadBuilder { DownloadRequest request = new DownloadRequest(id, type, uri, streamKeys, cacheKey, customMetadata); return new Download( - request, state, failureReason, stopReason, startTimeMs, updateTimeMs, counters); + request, + state, + startTimeMs, + updateTimeMs, + contentLength, + stopReason, + failureReason, + progress); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 7973174f6e..b1d5e1fc29 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -20,13 +20,13 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.Download.State; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestUtil; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -184,7 +184,7 @@ public class DownloadManagerTest { int tooManyRetries = MIN_RETRY_COUNT + 10; for (int i = 0; i < tooManyRetries; i++) { - downloader.increaseDownloadedByteCount(); + downloader.incrementBytesDownloaded(); downloader.assertStarted(MAX_RETRY_DELAY).fail(); } downloader.assertStarted(MAX_RETRY_DELAY).unblock(); @@ -555,11 +555,11 @@ public class DownloadManagerTest { private static void assertEqualIgnoringTimeFields(Download download, Download that) { assertThat(download.request).isEqualTo(that.request); assertThat(download.state).isEqualTo(that.state); + assertThat(download.contentLength).isEqualTo(that.contentLength); assertThat(download.failureReason).isEqualTo(that.failureReason); assertThat(download.stopReason).isEqualTo(that.stopReason); - assertThat(download.getDownloadPercentage()).isEqualTo(that.getDownloadPercentage()); - assertThat(download.getDownloadedBytes()).isEqualTo(that.getDownloadedBytes()); - assertThat(download.getTotalBytes()).isEqualTo(that.getTotalBytes()); + assertThat(download.getPercentDownloaded()).isEqualTo(that.getPercentDownloaded()); + assertThat(download.getBytesDownloaded()).isEqualTo(that.getBytesDownloaded()); } private static DownloadRequest createDownloadRequest() { @@ -722,21 +722,23 @@ public class DownloadManagerTest { private volatile boolean cancelled; private volatile boolean enableDownloadIOException; private volatile int startCount; - private CachingCounters counters; + private volatile int bytesDownloaded; private FakeDownloader() { this.started = new CountDownLatch(1); this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); - counters = new CachingCounters(); } @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"}) @Override - public void download() throws InterruptedException, IOException { + public void download(ProgressListener listener) throws InterruptedException, IOException { // It's ok to update this directly as no other thread will update it. startCount++; started.countDown(); block(); + if (bytesDownloaded > 0) { + listener.onProgress(C.LENGTH_UNSET, bytesDownloaded, C.PERCENTAGE_UNSET); + } if (enableDownloadIOException) { enableDownloadIOException = false; throw new IOException(); @@ -783,7 +785,7 @@ public class DownloadManagerTest { return this; } - private FakeDownloader assertStartCount(int count) throws InterruptedException { + private FakeDownloader assertStartCount(int count) { assertThat(startCount).isEqualTo(count); return this; } @@ -823,34 +825,14 @@ public class DownloadManagerTest { return unblock(); } - @Override - public long getDownloadedBytes() { - return counters.newlyCachedBytes; - } - - @Override - public long getTotalBytes() { - return counters.contentLength; - } - - @Override - public float getDownloadPercentage() { - return counters.percentage; - } - - @Override - public CachingCounters getCounters() { - return counters; - } - private void assertDoesNotStart() throws InterruptedException { Thread.sleep(ASSERT_FALSE_TIME); assertThat(started.getCount()).isEqualTo(1); } @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"}) - private void increaseDownloadedByteCount() { - counters.newlyCachedBytes++; + private void incrementBytesDownloaded() { + bytesDownloaded++; } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 4005edc3a6..956a5fc283 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -343,7 +343,7 @@ public final class CacheDataSourceTest { cache, /* cacheKeyFactory= */ null, upstream2, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null); // Read the rest of the data. @@ -392,7 +392,7 @@ public final class CacheDataSourceTest { cache, /* cacheKeyFactory= */ null, upstream2, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null); // Read the rest of the data. @@ -416,7 +416,7 @@ public final class CacheDataSourceTest { cache, /* cacheKeyFactory= */ null, upstream, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null); // Create cache read-only CacheDataSource. @@ -452,7 +452,7 @@ public final class CacheDataSourceTest { cache, /* cacheKeyFactory= */ null, upstream, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null); // Create blocking CacheDataSource. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java index ba06862385..9a449b2ebd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -22,6 +22,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import android.net.Uri; +import android.util.Pair; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -30,7 +31,6 @@ import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.FileDataSource; -import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.File; @@ -100,12 +100,12 @@ public final class CacheUtilTest { } @After - public void tearDown() throws Exception { + public void tearDown() { Util.recursiveDelete(tempFolder); } @Test - public void testGenerateKey() throws Exception { + public void testGenerateKey() { assertThat(CacheUtil.generateKey(Uri.EMPTY)).isNotNull(); Uri testUri = Uri.parse("test"); @@ -120,7 +120,7 @@ public final class CacheUtilTest { } @Test - public void testDefaultCacheKeyFactory_buildCacheKey() throws Exception { + public void testDefaultCacheKeyFactory_buildCacheKey() { Uri testUri = Uri.parse("test"); String key = "key"; // If DataSpec.key is present, returns it. @@ -136,62 +136,66 @@ public final class CacheUtilTest { } @Test - public void testGetCachedNoData() throws Exception { - CachingCounters counters = new CachingCounters(); - CacheUtil.getCached( - new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); + public void testGetCachedNoData() { + Pair contentLengthAndBytesCached = + CacheUtil.getCached( + new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null); - assertCounters(counters, 0, 0, C.LENGTH_UNSET); + assertThat(contentLengthAndBytesCached.first).isEqualTo(C.LENGTH_UNSET); + assertThat(contentLengthAndBytesCached.second).isEqualTo(0); } @Test - public void testGetCachedDataUnknownLength() throws Exception { + public void testGetCachedDataUnknownLength() { // Mock there is 100 bytes cached at the beginning mockCache.spansAndGaps = new int[] {100}; - CachingCounters counters = new CachingCounters(); - CacheUtil.getCached( - new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); + Pair contentLengthAndBytesCached = + CacheUtil.getCached( + new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null); - assertCounters(counters, 100, 0, C.LENGTH_UNSET); + assertThat(contentLengthAndBytesCached.first).isEqualTo(C.LENGTH_UNSET); + assertThat(contentLengthAndBytesCached.second).isEqualTo(100); } @Test - public void testGetCachedNoDataKnownLength() throws Exception { + public void testGetCachedNoDataKnownLength() { mockCache.contentLength = 1000; - CachingCounters counters = new CachingCounters(); - CacheUtil.getCached( - new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); + Pair contentLengthAndBytesCached = + CacheUtil.getCached( + new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null); - assertCounters(counters, 0, 0, 1000); + assertThat(contentLengthAndBytesCached.first).isEqualTo(1000); + assertThat(contentLengthAndBytesCached.second).isEqualTo(0); } @Test - public void testGetCached() throws Exception { + public void testGetCached() { mockCache.contentLength = 1000; mockCache.spansAndGaps = new int[] {100, 100, 200}; - CachingCounters counters = new CachingCounters(); - CacheUtil.getCached( - new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); + Pair contentLengthAndBytesCached = + CacheUtil.getCached( + new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null); - assertCounters(counters, 300, 0, 1000); + assertThat(contentLengthAndBytesCached.first).isEqualTo(1000); + assertThat(contentLengthAndBytesCached.second).isEqualTo(300); } @Test - public void testGetCachedFromNonZeroPosition() throws Exception { + public void testGetCachedFromNonZeroPosition() { mockCache.contentLength = 1000; mockCache.spansAndGaps = new int[] {100, 100, 200}; - CachingCounters counters = new CachingCounters(); - CacheUtil.getCached( - new DataSpec( - Uri.parse("test"), - /* absoluteStreamPosition= */ 100, - /* length= */ C.LENGTH_UNSET, - /* key= */ null), - mockCache, - /* cacheKeyFactory= */ null, - counters); + Pair contentLengthAndBytesCached = + CacheUtil.getCached( + new DataSpec( + Uri.parse("test"), + /* absoluteStreamPosition= */ 100, + /* length= */ C.LENGTH_UNSET, + /* key= */ null), + mockCache, + /* cacheKeyFactory= */ null); - assertCounters(counters, 200, 0, 900); + assertThat(contentLengthAndBytesCached.first).isEqualTo(900); + assertThat(contentLengthAndBytesCached.second).isEqualTo(200); } @Test @@ -208,7 +212,7 @@ public final class CacheUtilTest { counters, /* isCanceled= */ null); - assertCounters(counters, 0, 100, 100); + counters.assertValues(0, 100, 100); assertCachedData(cache, fakeDataSet); } @@ -223,7 +227,8 @@ public final class CacheUtilTest { CacheUtil.cache( dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); - assertCounters(counters, 0, 20, 20); + counters.assertValues(0, 20, 20); + counters.reset(); CacheUtil.cache( new DataSpec(testUri), @@ -233,7 +238,7 @@ public final class CacheUtilTest { counters, /* isCanceled= */ null); - assertCounters(counters, 20, 80, 100); + counters.assertValues(20, 80, 100); assertCachedData(cache, fakeDataSet); } @@ -249,7 +254,7 @@ public final class CacheUtilTest { CacheUtil.cache( dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); - assertCounters(counters, 0, 100, 100); + counters.assertValues(0, 100, 100); assertCachedData(cache, fakeDataSet); } @@ -266,7 +271,8 @@ public final class CacheUtilTest { CacheUtil.cache( dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); - assertCounters(counters, 0, 20, 20); + counters.assertValues(0, 20, 20); + counters.reset(); CacheUtil.cache( new DataSpec(testUri), @@ -276,7 +282,7 @@ public final class CacheUtilTest { counters, /* isCanceled= */ null); - assertCounters(counters, 20, 80, 100); + counters.assertValues(20, 80, 100); assertCachedData(cache, fakeDataSet); } @@ -291,7 +297,7 @@ public final class CacheUtilTest { CacheUtil.cache( dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); - assertCounters(counters, 0, 100, 1000); + counters.assertValues(0, 100, 1000); assertCachedData(cache, fakeDataSet); } @@ -312,7 +318,7 @@ public final class CacheUtilTest { new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], /* priorityTaskManager= */ null, /* priority= */ 0, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null, /* enableEOFException= */ true); fail(); @@ -328,9 +334,9 @@ public final class CacheUtilTest { new FakeDataSet() .newData("test_data") .appendReadData(TestUtil.buildTestData(100)) - .appendReadAction(() -> assertCounters(counters, 0, 100, 300)) + .appendReadAction(() -> counters.assertValues(0, 100, 300)) .appendReadData(TestUtil.buildTestData(100)) - .appendReadAction(() -> assertCounters(counters, 0, 200, 300)) + .appendReadAction(() -> counters.assertValues(0, 200, 300)) .appendReadData(TestUtil.buildTestData(100)) .endData(); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -343,7 +349,7 @@ public final class CacheUtilTest { counters, /* isCanceled= */ null); - assertCounters(counters, 0, 300, 300); + counters.assertValues(0, 300, 300); assertCachedData(cache, fakeDataSet); } @@ -369,7 +375,7 @@ public final class CacheUtilTest { new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], /* priorityTaskManager= */ null, /* priority= */ 0, - /* counters= */ null, + /* progressListener= */ null, /* isCanceled= */ null, true); CacheUtil.remove(dataSpec, cache, /* cacheKeyFactory= */ null); @@ -377,10 +383,34 @@ public final class CacheUtilTest { assertCacheEmpty(cache); } - private static void assertCounters(CachingCounters counters, int alreadyCachedBytes, - int newlyCachedBytes, int contentLength) { - assertThat(counters.alreadyCachedBytes).isEqualTo(alreadyCachedBytes); - assertThat(counters.newlyCachedBytes).isEqualTo(newlyCachedBytes); - assertThat(counters.contentLength).isEqualTo(contentLength); + private static final class CachingCounters implements CacheUtil.ProgressListener { + + private long contentLength = C.LENGTH_UNSET; + private long bytesAlreadyCached; + private long bytesNewlyCached; + private boolean seenFirstProgressUpdate; + + @Override + public void onProgress(long contentLength, long bytesCached, long newBytesCached) { + this.contentLength = contentLength; + if (!seenFirstProgressUpdate) { + bytesAlreadyCached = bytesCached; + seenFirstProgressUpdate = true; + } + bytesNewlyCached = bytesCached - bytesAlreadyCached; + } + + public void assertValues(int bytesAlreadyCached, int bytesNewlyCached, int contentLength) { + assertThat(this.bytesAlreadyCached).isEqualTo(bytesAlreadyCached); + assertThat(this.bytesNewlyCached).isEqualTo(bytesNewlyCached); + assertThat(this.contentLength).isEqualTo(contentLength); + } + + public void reset() { + contentLength = C.LENGTH_UNSET; + bytesAlreadyCached = 0; + bytesNewlyCached = 0; + seenFirstProgressUpdate = false; + } } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index 5636c73491..2754a3341a 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -45,7 +45,7 @@ import java.util.List; *

Example usage: * *

{@code
- * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
+ * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
  * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
  * DownloaderConstructorHelper constructorHelper =
  *     new DownloaderConstructorHelper(cache, factory);
@@ -55,7 +55,7 @@ import java.util.List;
  *     new DashDownloader(
  *         manifestUrl, Collections.singletonList(new StreamKey(0, 0, 0)), constructorHelper);
  * // Perform the download.
- * dashDownloader.download();
+ * dashDownloader.download(progressListener);
  * // Access downloaded data using CacheDataSource
  * CacheDataSource cacheDataSource =
  *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java
index 9eacd28f8d..b3a6b8271b 100644
--- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java
+++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java
@@ -62,6 +62,7 @@ public class DashDownloaderTest {
 
   private SimpleCache cache;
   private File tempFolder;
+  private ProgressListener progressListener;
 
   @Before
   public void setUp() throws Exception {
@@ -69,6 +70,7 @@ public class DashDownloaderTest {
     tempFolder =
         Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
     cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
+    progressListener = new ProgressListener();
   }
 
   @After
@@ -77,7 +79,7 @@ public class DashDownloaderTest {
   }
 
   @Test
-  public void testCreateWithDefaultDownloaderFactory() throws Exception {
+  public void testCreateWithDefaultDownloaderFactory() {
     DownloaderConstructorHelper constructorHelper =
         new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
     DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
@@ -105,7 +107,7 @@ public class DashDownloaderTest {
             .setRandomData("audio_segment_3", 6);
 
     DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
-    dashDownloader.download();
+    dashDownloader.download(progressListener);
     assertCachedData(cache, fakeDataSet);
   }
 
@@ -124,7 +126,7 @@ public class DashDownloaderTest {
             .setRandomData("audio_segment_3", 6);
 
     DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
-    dashDownloader.download();
+    dashDownloader.download(progressListener);
     assertCachedData(cache, fakeDataSet);
   }
 
@@ -143,7 +145,7 @@ public class DashDownloaderTest {
 
     DashDownloader dashDownloader =
         getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
-    dashDownloader.download();
+    dashDownloader.download(progressListener);
     assertCachedData(cache, fakeDataSet);
   }
 
@@ -164,7 +166,7 @@ public class DashDownloaderTest {
             .setRandomData("period_2_segment_3", 3);
 
     DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
-    dashDownloader.download();
+    dashDownloader.download(progressListener);
     assertCachedData(cache, fakeDataSet);
   }
 
@@ -186,7 +188,7 @@ public class DashDownloaderTest {
 
     DashDownloader dashDownloader =
         getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
-    dashDownloader.download();
+    dashDownloader.download(progressListener);
 
     DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs();
     assertThat(openedDataSpecs.length).isEqualTo(8);
@@ -218,7 +220,7 @@ public class DashDownloaderTest {
 
     DashDownloader dashDownloader =
         getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(1, 0, 0));
-    dashDownloader.download();
+    dashDownloader.download(progressListener);
 
     DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs();
     assertThat(openedDataSpecs.length).isEqualTo(8);
@@ -248,12 +250,12 @@ public class DashDownloaderTest {
 
     DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
     try {
-      dashDownloader.download();
+      dashDownloader.download(progressListener);
       fail();
     } catch (IOException e) {
       // Expected.
     }
-    dashDownloader.download();
+    dashDownloader.download(progressListener);
     assertCachedData(cache, fakeDataSet);
   }
 
@@ -272,18 +274,17 @@ public class DashDownloaderTest {
             .setRandomData("audio_segment_3", 6);
 
     DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
-    assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(0);
 
     try {
-      dashDownloader.download();
+      dashDownloader.download(progressListener);
       fail();
     } catch (IOException e) {
       // Failure expected after downloading init data, segment 1 and 2 bytes in segment 2.
     }
-    assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(10 + 4 + 2);
+    progressListener.assertBytesDownloaded(10 + 4 + 2);
 
-    dashDownloader.download();
-    assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(10 + 4 + 5 + 6);
+    dashDownloader.download(progressListener);
+    progressListener.assertBytesDownloaded(10 + 4 + 5 + 6);
   }
 
   @Test
@@ -301,7 +302,7 @@ public class DashDownloaderTest {
 
     DashDownloader dashDownloader =
         getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
-    dashDownloader.download();
+    dashDownloader.download(progressListener);
     dashDownloader.remove();
     assertCacheEmpty(cache);
   }
@@ -315,7 +316,7 @@ public class DashDownloaderTest {
 
     DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
     try {
-      dashDownloader.download();
+      dashDownloader.download(progressListener);
       fail();
     } catch (DownloadException e) {
       // Expected.
@@ -339,4 +340,17 @@ public class DashDownloaderTest {
     return keysList;
   }
 
+  private static final class ProgressListener implements Downloader.ProgressListener {
+
+    private long bytesDownloaded;
+
+    @Override
+    public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) {
+      this.bytesDownloaded = bytesDownloaded;
+    }
+
+    public void assertBytesDownloaded(long bytesDownloaded) {
+      assertThat(this.bytesDownloaded).isEqualTo(bytesDownloaded);
+    }
+  }
 }
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
index 8e744f9a77..6e6d0afd49 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
@@ -39,7 +39,7 @@ import java.util.List;
  * 

Example usage: * *

{@code
- * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
+ * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
  * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
  * DownloaderConstructorHelper constructorHelper =
  *     new DownloaderConstructorHelper(cache, factory);
@@ -50,7 +50,7 @@ import java.util.List;
  *         Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0)),
  *         constructorHelper);
  * // Perform the download.
- * hlsDownloader.download();
+ * hlsDownloader.download(progressListener);
  * // Access downloaded data using CacheDataSource
  * CacheDataSource cacheDataSource =
  *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
index b92953c3b5..7d77a78316 100644
--- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
+++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
@@ -67,6 +67,7 @@ public class HlsDownloaderTest {
 
   private SimpleCache cache;
   private File tempFolder;
+  private ProgressListener progressListener;
   private FakeDataSet fakeDataSet;
 
   @Before
@@ -74,7 +75,7 @@ public class HlsDownloaderTest {
     tempFolder =
         Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
     cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
-
+    progressListener = new ProgressListener();
     fakeDataSet =
         new FakeDataSet()
             .setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA)
@@ -94,7 +95,7 @@ public class HlsDownloaderTest {
   }
 
   @Test
-  public void testCreateWithDefaultDownloaderFactory() throws Exception {
+  public void testCreateWithDefaultDownloaderFactory() {
     DownloaderConstructorHelper constructorHelper =
         new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
     DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
@@ -115,17 +116,16 @@ public class HlsDownloaderTest {
   public void testCounterMethods() throws Exception {
     HlsDownloader downloader =
         getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX));
-    downloader.download();
+    downloader.download(progressListener);
 
-    assertThat(downloader.getDownloadedBytes())
-        .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
+    progressListener.assertBytesDownloaded(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
   }
 
   @Test
   public void testDownloadRepresentation() throws Exception {
     HlsDownloader downloader =
         getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX));
-    downloader.download();
+    downloader.download(progressListener);
 
     assertCachedData(
         cache,
@@ -143,7 +143,7 @@ public class HlsDownloaderTest {
         getHlsDownloader(
             MASTER_PLAYLIST_URI,
             getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX));
-    downloader.download();
+    downloader.download(progressListener);
 
     assertCachedData(cache, fakeDataSet);
   }
@@ -162,7 +162,7 @@ public class HlsDownloaderTest {
         .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence2.ts", 15);
 
     HlsDownloader downloader = getHlsDownloader(MASTER_PLAYLIST_URI, getKeys());
-    downloader.download();
+    downloader.download(progressListener);
 
     assertCachedData(cache, fakeDataSet);
   }
@@ -173,7 +173,7 @@ public class HlsDownloaderTest {
         getHlsDownloader(
             MASTER_PLAYLIST_URI,
             getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX));
-    downloader.download();
+    downloader.download(progressListener);
     downloader.remove();
 
     assertCacheEmpty(cache);
@@ -182,7 +182,7 @@ public class HlsDownloaderTest {
   @Test
   public void testDownloadMediaPlaylist() throws Exception {
     HlsDownloader downloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI, getKeys());
-    downloader.download();
+    downloader.download(progressListener);
 
     assertCachedData(
         cache,
@@ -205,7 +205,7 @@ public class HlsDownloaderTest {
             .setRandomData("fileSequence2.ts", 12);
 
     HlsDownloader downloader = getHlsDownloader(ENC_MEDIA_PLAYLIST_URI, getKeys());
-    downloader.download();
+    downloader.download(progressListener);
     assertCachedData(cache, fakeDataSet);
   }
 
@@ -222,4 +222,18 @@ public class HlsDownloaderTest {
     }
     return streamKeys;
   }
+
+  private static final class ProgressListener implements Downloader.ProgressListener {
+
+    private long bytesDownloaded;
+
+    @Override
+    public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) {
+      this.bytesDownloaded = bytesDownloaded;
+    }
+
+    public void assertBytesDownloaded(long bytesDownloaded) {
+      assertThat(this.bytesDownloaded).isEqualTo(bytesDownloaded);
+    }
+  }
 }
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
index 18820ca49c..1331fe4617 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
@@ -37,7 +37,7 @@ import java.util.List;
  * 

Example usage: * *

{@code
- * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
+ * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
  * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
  * DownloaderConstructorHelper constructorHelper =
  *     new DownloaderConstructorHelper(cache, factory);
@@ -48,7 +48,7 @@ import java.util.List;
  *         Collections.singletonList(new StreamKey(0, 0)),
  *         constructorHelper);
  * // Perform the download.
- * ssDownloader.download();
+ * ssDownloader.download(progressListener);
  * // Access downloaded data using CacheDataSource
  * CacheDataSource cacheDataSource =
  *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java
index b26b8eaac4..178cd44dd3 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java
@@ -75,12 +75,12 @@ public final class DownloadNotificationHelper {
         continue;
       }
       haveDownloadTasks = true;
-      float downloadPercentage = download.getDownloadPercentage();
+      float downloadPercentage = download.getPercentDownloaded();
       if (downloadPercentage != C.PERCENTAGE_UNSET) {
         allDownloadPercentagesUnknown = false;
         totalPercentage += downloadPercentage;
       }
-      haveDownloadedBytes |= download.getDownloadedBytes() > 0;
+      haveDownloadedBytes |= download.getBytesDownloaded() > 0;
       downloadTaskCount++;
     }
 
diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java
index 67c840e681..f5af2472c9 100644
--- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java
+++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java
@@ -89,7 +89,7 @@ public final class DashDownloadTest {
   @Test
   public void testDownload() throws Exception {
     DashDownloader dashDownloader = downloadContent();
-    dashDownloader.download();
+    dashDownloader.download(/* progressListener= */ null);
 
     testRunner
         .setStreamName("test_h264_fixed_download")