Improve progress reporting logic

- Listener based reporting of progress allows the content length
  to be persisted into the download index (and notified via a
  download state change) as soon as it's available.
- Moved contentLength back into Download proper. It should only
  ever change once, so I'm not sure it belongs in the mutable part
  of Download.
- Made a DownloadProgress class, for naming sanity.

PiperOrigin-RevId: 244242487
This commit is contained in:
olly 2019-04-18 21:02:48 +01:00 committed by Oliver Woodman
parent 0ccda60ab4
commit 82061e9afb
21 changed files with 593 additions and 463 deletions

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.offline;
import static com.google.android.exoplayer2.offline.Download.STATE_QUEUED; import static com.google.android.exoplayer2.offline.Download.STATE_QUEUED;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -97,10 +98,11 @@ public final class ActionFileUpgradeUtil {
new Download( new Download(
request, request,
STATE_QUEUED, STATE_QUEUED,
Download.FAILURE_REASON_NONE,
Download.STOP_REASON_NONE,
/* startTimeMs= */ nowMs, /* startTimeMs= */ nowMs,
/* updateTimeMs= */ nowMs); /* updateTimeMs= */ nowMs,
/* contentLength= */ C.LENGTH_UNSET,
Download.STOP_REASON_NONE,
Download.FAILURE_REASON_NONE);
} }
downloadIndex.putDownload(download); downloadIndex.putDownload(download);
} }

View File

@ -27,7 +27,6 @@ import android.text.TextUtils;
import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseIOException;
import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.VersionTable; 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.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList; 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_CUSTOM_CACHE_KEY, download.request.customCacheKey);
values.put(COLUMN_DATA, download.request.data); values.put(COLUMN_DATA, download.request.data);
values.put(COLUMN_STATE, download.state); 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_START_TIME_MS, download.startTimeMs);
values.put(COLUMN_UPDATE_TIME_MS, download.updateTimeMs); 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 { try {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);
@ -337,18 +336,18 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex {
decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)), decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)),
cursor.getString(COLUMN_INDEX_CUSTOM_CACHE_KEY), cursor.getString(COLUMN_INDEX_CUSTOM_CACHE_KEY),
cursor.getBlob(COLUMN_INDEX_DATA)); cursor.getBlob(COLUMN_INDEX_DATA));
CachingCounters cachingCounters = new CachingCounters(); DownloadProgress downloadProgress = new DownloadProgress();
cachingCounters.alreadyCachedBytes = cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES); downloadProgress.bytesDownloaded = cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES);
cachingCounters.contentLength = cursor.getLong(COLUMN_INDEX_TOTAL_BYTES); downloadProgress.percentDownloaded = cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE);
cachingCounters.percentage = cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE);
return new Download( return new Download(
request, request,
cursor.getInt(COLUMN_INDEX_STATE), 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_START_TIME_MS),
cursor.getLong(COLUMN_INDEX_UPDATE_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<StreamKey> streamKeys) { private static String encodeStreamKeys(List<StreamKey> streamKeys) {

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.offline;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -96,60 +95,65 @@ public final class Download {
/** The download request. */ /** The download request. */
public final DownloadRequest request; public final DownloadRequest request;
/** The state of the download. */ /** The state of the download. */
@State public final int state; @State public final int state;
/** The first time when download entry is created. */ /** The first time when download entry is created. */
public final long startTimeMs; public final long startTimeMs;
/** The last update time. */ /** The last update time. */
public final long updateTimeMs; 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 * If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise {@link
* #FAILURE_REASON_NONE}. * #FAILURE_REASON_NONE}.
*/ */
@FailureReason public final int failureReason; @FailureReason public final int failureReason;
/** 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, DownloadRequest request,
@State int state, @State int state,
@FailureReason int failureReason,
int stopReason,
long startTimeMs, long startTimeMs,
long updateTimeMs) { long updateTimeMs,
long contentLength,
int stopReason,
@FailureReason int failureReason) {
this( this(
request, request,
state, state,
failureReason,
stopReason,
startTimeMs, startTimeMs,
updateTimeMs, updateTimeMs,
new CachingCounters()); contentLength,
stopReason,
failureReason,
new DownloadProgress());
} }
/* package */ Download( public Download(
DownloadRequest request, DownloadRequest request,
@State int state, @State int state,
@FailureReason int failureReason,
int stopReason,
long startTimeMs, long startTimeMs,
long updateTimeMs, long updateTimeMs,
CachingCounters counters) { long contentLength,
Assertions.checkNotNull(counters); int stopReason,
@FailureReason int failureReason,
DownloadProgress progress) {
Assertions.checkNotNull(progress);
Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED));
if (stopReason != 0) { if (stopReason != 0) {
Assertions.checkState(state != STATE_DOWNLOADING && state != STATE_QUEUED); Assertions.checkState(state != STATE_DOWNLOADING && state != STATE_QUEUED);
} }
this.request = request; this.request = request;
this.state = state; this.state = state;
this.failureReason = failureReason;
this.stopReason = stopReason;
this.startTimeMs = startTimeMs; this.startTimeMs = startTimeMs;
this.updateTimeMs = updateTimeMs; 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. */ /** 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. */ /** Returns the total number of downloaded bytes. */
public long getDownloadedBytes() { public long getBytesDownloaded() {
return counters.totalCachedBytes(); return progress.bytesDownloaded;
}
/** Returns the total size of the media, or {@link C#LENGTH_UNSET} if unknown. */
public long getTotalBytes() {
return counters.contentLength;
} }
/** /**
* Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is
* available. * available.
*/ */
public float getDownloadPercentage() { public float getPercentDownloaded() {
return counters.percentage; return progress.percentDownloaded;
}
/**
* 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;
} }
} }

View File

@ -36,7 +36,6 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.RequirementsWatcher; 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.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; 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_ADD_DOWNLOAD = 4;
private static final int MSG_REMOVE_DOWNLOAD = 5; private static final int MSG_REMOVE_DOWNLOAD = 5;
private static final int MSG_DOWNLOAD_THREAD_STOPPED = 6; 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) @Retention(RetentionPolicy.SOURCE)
@IntDef({ @IntDef({
@ -539,6 +539,11 @@ public final class DownloadManager {
onDownloadThreadStoppedInternal(downloadThread); onDownloadThreadStoppedInternal(downloadThread);
processedExternalMessage = false; // This message is posted internally. processedExternalMessage = false; // This message is posted internally.
break; break;
case MSG_CONTENT_LENGTH_CHANGED:
downloadThread = (DownloadThread) message.obj;
onDownloadThreadContentLengthChangedInternal(downloadThread);
processedExternalMessage = false; // This message is posted internally.
break;
case MSG_RELEASE: case MSG_RELEASE:
releaseInternal(); releaseInternal();
return true; // Don't post back to mainHandler on release. return true; // Don't post back to mainHandler on release.
@ -634,10 +639,11 @@ public final class DownloadManager {
new Download( new Download(
request, request,
stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, stopReason != Download.STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED,
Download.FAILURE_REASON_NONE,
stopReason,
/* startTimeMs= */ nowMs, /* startTimeMs= */ nowMs,
/* updateTimeMs= */ nowMs); /* updateTimeMs= */ nowMs,
/* contentLength= */ C.LENGTH_UNSET,
stopReason,
Download.FAILURE_REASON_NONE);
logd("Download state is created for " + request.id); logd("Download state is created for " + request.id);
} else { } else {
download = mergeRequest(download, request, stopReason); 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() { private void releaseInternal() {
for (DownloadThread downloadThread : downloadThreads.values()) { for (DownloadThread downloadThread : downloadThreads.values()) {
downloadThread.cancel(/* released= */ true); downloadThread.cancel(/* released= */ true);
@ -737,10 +748,11 @@ public final class DownloadManager {
parallelDownloads++; parallelDownloads++;
} }
Downloader downloader = downloaderFactory.createDownloader(request); Downloader downloader = downloaderFactory.createDownloader(request);
DownloadProgress downloadProgress = downloadInternal.download.progress;
DownloadThread downloadThread = DownloadThread downloadThread =
new DownloadThread(request, downloader, isRemove, minRetryCount, internalHandler); new DownloadThread(
request, downloader, downloadProgress, isRemove, minRetryCount, internalHandler);
downloadThreads.put(downloadId, downloadThread); downloadThreads.put(downloadId, downloadThread);
downloadInternal.setCounters(downloadThread.downloader.getCounters());
downloadThread.start(); downloadThread.start();
logd("Download is started", downloadInternal); logd("Download is started", downloadInternal);
return START_THREAD_SUCCEEDED; return START_THREAD_SUCCEEDED;
@ -802,22 +814,23 @@ public final class DownloadManager {
return new Download( return new Download(
download.request.copyWithMergedRequest(request), download.request.copyWithMergedRequest(request),
state, state,
FAILURE_REASON_NONE,
stopReason,
startTimeMs, startTimeMs,
/* updateTimeMs= */ nowMs, /* updateTimeMs= */ nowMs,
download.counters); /* contentLength= */ C.LENGTH_UNSET,
stopReason,
FAILURE_REASON_NONE);
} }
private static Download copyWithState(Download download, @Download.State int state) { private static Download copyWithState(Download download, @Download.State int state) {
return new Download( return new Download(
download.request, download.request,
state, state,
FAILURE_REASON_NONE,
download.stopReason,
download.startTimeMs, download.startTimeMs,
/* updateTimeMs= */ System.currentTimeMillis(), /* updateTimeMs= */ System.currentTimeMillis(),
download.counters); download.contentLength,
download.stopReason,
FAILURE_REASON_NONE,
download.progress);
} }
private static void logd(String message) { private static void logd(String message) {
@ -850,13 +863,17 @@ public final class DownloadManager {
// TODO: Get rid of these and use download directly. // TODO: Get rid of these and use download directly.
@Download.State private int state; @Download.State private int state;
private long contentLength;
private int stopReason; private int stopReason;
@MonotonicNonNull @Download.FailureReason private int failureReason; @MonotonicNonNull @Download.FailureReason private int failureReason;
private DownloadInternal(DownloadManager downloadManager, Download download) { private DownloadInternal(DownloadManager downloadManager, Download download) {
this.downloadManager = downloadManager; this.downloadManager = downloadManager;
this.download = download; this.download = download;
state = download.state;
contentLength = download.contentLength;
stopReason = download.stopReason; stopReason = download.stopReason;
failureReason = download.failureReason;
} }
private void initialize() { private void initialize() {
@ -877,11 +894,12 @@ public final class DownloadManager {
new Download( new Download(
download.request, download.request,
state, state,
state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason,
stopReason,
download.startTimeMs, download.startTimeMs,
/* updateTimeMs= */ System.currentTimeMillis(), /* updateTimeMs= */ System.currentTimeMillis(),
download.counters); contentLength,
stopReason,
state != STATE_FAILED ? FAILURE_REASON_NONE : failureReason,
download.progress);
return download; return download;
} }
@ -911,8 +929,12 @@ public final class DownloadManager {
return state == STATE_REMOVING || state == STATE_RESTARTING; return state == STATE_REMOVING || state == STATE_RESTARTING;
} }
public void setCounters(CachingCounters counters) { public void setContentLength(long contentLength) {
download.setCounters(counters); if (this.contentLength == contentLength) {
return;
}
this.contentLength = contentLength;
downloadManager.onDownloadChangedInternal(this, getUpdatedDownload());
} }
private void updateStopState() { 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 DownloadRequest request;
private final Downloader downloader; private final Downloader downloader;
private final DownloadProgress downloadProgress;
private final boolean isRemove; private final boolean isRemove;
private final int minRetryCount; private final int minRetryCount;
private volatile Handler onStoppedHandler; private volatile Handler updateHandler;
private volatile boolean isCanceled; private volatile boolean isCanceled;
private Throwable finalError; private Throwable finalError;
private long contentLength;
private DownloadThread( private DownloadThread(
DownloadRequest request, DownloadRequest request,
Downloader downloader, Downloader downloader,
DownloadProgress downloadProgress,
boolean isRemove, boolean isRemove,
int minRetryCount, int minRetryCount,
Handler onStoppedHandler) { Handler updateHandler) {
this.request = request; this.request = request;
this.isRemove = isRemove;
this.downloader = downloader; this.downloader = downloader;
this.downloadProgress = downloadProgress;
this.isRemove = isRemove;
this.minRetryCount = minRetryCount; this.minRetryCount = minRetryCount;
this.onStoppedHandler = onStoppedHandler; this.updateHandler = updateHandler;
contentLength = C.LENGTH_UNSET;
} }
public void cancel(boolean released) { 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 // 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 // null the handler reference here so that it doesn't prevent garbage collection of the
// download manager whilst cancellation is ongoing. // download manager whilst cancellation is ongoing.
onStoppedHandler = null; updateHandler = null;
} }
isCanceled = true; isCanceled = true;
downloader.cancel(); downloader.cancel();
@ -1042,14 +1070,14 @@ public final class DownloadManager {
long errorPosition = C.LENGTH_UNSET; long errorPosition = C.LENGTH_UNSET;
while (!isCanceled) { while (!isCanceled) {
try { try {
downloader.download(); downloader.download(/* progressListener= */ this);
break; break;
} catch (IOException e) { } catch (IOException e) {
if (!isCanceled) { if (!isCanceled) {
long downloadedBytes = downloader.getDownloadedBytes(); long bytesDownloaded = downloadProgress.bytesDownloaded;
if (downloadedBytes != errorPosition) { if (bytesDownloaded != errorPosition) {
logd("Reset error count. downloadedBytes = " + downloadedBytes, request); logd("Reset error count. bytesDownloaded = " + bytesDownloaded, request);
errorPosition = downloadedBytes; errorPosition = bytesDownloaded;
errorCount = 0; errorCount = 0;
} }
if (++errorCount > minRetryCount) { if (++errorCount > minRetryCount) {
@ -1064,13 +1092,26 @@ public final class DownloadManager {
} catch (Throwable e) { } catch (Throwable e) {
finalError = e; finalError = e;
} }
Handler onStoppedHandler = this.onStoppedHandler; Handler updateHandler = this.updateHandler;
if (onStoppedHandler != null) { if (updateHandler != null) {
onStoppedHandler.obtainMessage(MSG_DOWNLOAD_THREAD_STOPPED, this).sendToTarget(); 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); return Math.min((errorCount - 1) * 1000, 5000);
} }
} }

View File

@ -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;
}

View File

@ -15,44 +15,44 @@
*/ */
package com.google.android.exoplayer2.offline; package com.google.android.exoplayer2.offline;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters;
import java.io.IOException; import java.io.IOException;
/** /** Downloads and removes a piece of content. */
* An interface for stream downloaders.
*/
public interface Downloader { 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 InterruptedException If the thread has been interrupted.
* @throws IOException Thrown when there is an io error while downloading. * @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(); 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 * Removes the content.
* available.
*/
float getDownloadPercentage();
/** Returns a {@link CachingCounters} which holds download counters. */
CachingCounters getCounters();
/**
* Removes the media.
* *
* @throws InterruptedException Thrown if the thread was interrupted. * @throws InterruptedException Thrown if the thread was interrupted.
*/ */

View File

@ -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.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory;
import com.google.android.exoplayer2.upstream.cache.CacheUtil; 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.PriorityTaskManager;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -40,7 +39,6 @@ public final class ProgressiveDownloader implements Downloader {
private final CacheDataSource dataSource; private final CacheDataSource dataSource;
private final CacheKeyFactory cacheKeyFactory; private final CacheKeyFactory cacheKeyFactory;
private final PriorityTaskManager priorityTaskManager; private final PriorityTaskManager priorityTaskManager;
private final CacheUtil.CachingCounters cachingCounters;
private final AtomicBoolean isCanceled; private final AtomicBoolean isCanceled;
/** /**
@ -62,12 +60,12 @@ public final class ProgressiveDownloader implements Downloader {
this.dataSource = constructorHelper.createCacheDataSource(); this.dataSource = constructorHelper.createCacheDataSource();
this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();
this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); this.priorityTaskManager = constructorHelper.getPriorityTaskManager();
cachingCounters = new CachingCounters();
isCanceled = new AtomicBoolean(); isCanceled = new AtomicBoolean();
} }
@Override @Override
public void download() throws InterruptedException, IOException { public void download(@Nullable ProgressListener progressListener)
throws InterruptedException, IOException {
priorityTaskManager.add(C.PRIORITY_DOWNLOAD); priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
try { try {
CacheUtil.cache( CacheUtil.cache(
@ -78,7 +76,7 @@ public final class ProgressiveDownloader implements Downloader {
new byte[BUFFER_SIZE_BYTES], new byte[BUFFER_SIZE_BYTES],
priorityTaskManager, priorityTaskManager,
C.PRIORITY_DOWNLOAD, C.PRIORITY_DOWNLOAD,
cachingCounters, progressListener == null ? null : new ProgressForwarder(progressListener),
isCanceled, isCanceled,
/* enableEOFException= */ true); /* enableEOFException= */ true);
} finally { } finally {
@ -91,28 +89,26 @@ public final class ProgressiveDownloader implements Downloader {
isCanceled.set(true); 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 @Override
public void remove() { public void remove() {
CacheUtil.remove(dataSpec, cache, cacheKeyFactory); 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);
}
}
} }

View File

@ -17,6 +17,8 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; 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.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory;
import com.google.android.exoplayer2.upstream.cache.CacheUtil; 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.PriorityTaskManager;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
@ -42,6 +43,7 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M>> impleme
/** Smallest unit of content to be downloaded. */ /** Smallest unit of content to be downloaded. */
protected static class Segment implements Comparable<Segment> { protected static class Segment implements Comparable<Segment> {
/** The start time of the segment in microseconds. */ /** The start time of the segment in microseconds. */
public final long startTimeUs; public final long startTimeUs;
@ -70,10 +72,6 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M>> impleme
private final PriorityTaskManager priorityTaskManager; private final PriorityTaskManager priorityTaskManager;
private final ArrayList<StreamKey> streamKeys; private final ArrayList<StreamKey> streamKeys;
private final AtomicBoolean isCanceled; 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. * @param manifestUri The {@link Uri} of the manifest to be downloaded.
@ -90,9 +88,7 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M>> impleme
this.offlineDataSource = constructorHelper.createOfflineCacheDataSource(); this.offlineDataSource = constructorHelper.createOfflineCacheDataSource();
this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();
this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); this.priorityTaskManager = constructorHelper.getPriorityTaskManager();
totalSegments = C.LENGTH_UNSET;
isCanceled = new AtomicBoolean(); isCanceled = new AtomicBoolean();
counters = new CachingCounters();
} }
/** /**
@ -102,35 +98,71 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M>> impleme
* @throws IOException Thrown when there is an error downloading. * @throws IOException Thrown when there is an error downloading.
* @throws InterruptedException If the thread has been interrupted. * @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 @Override
public final void download() throws IOException, InterruptedException { public final void download(@Nullable ProgressListener progressListener)
throws IOException, InterruptedException {
priorityTaskManager.add(C.PRIORITY_DOWNLOAD); priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
try { try {
List<Segment> segments = initDownload(); // Get the manifest and all of the segments.
M manifest = getManifest(dataSource, manifestDataSpec);
if (!streamKeys.isEmpty()) {
manifest = manifest.copy(streamKeys);
}
List<Segment> 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<Long, Long> 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); 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]; byte[] buffer = new byte[BUFFER_SIZE_BYTES];
CachingCounters cachingCounters = new CachingCounters();
for (int i = 0; i < segments.size(); i++) { for (int i = 0; i < segments.size(); i++) {
try { CacheUtil.cache(
CacheUtil.cache( segments.get(i).dataSpec,
segments.get(i).dataSpec, cache,
cache, cacheKeyFactory,
cacheKeyFactory, dataSource,
dataSource, buffer,
buffer, priorityTaskManager,
priorityTaskManager, C.PRIORITY_DOWNLOAD,
C.PRIORITY_DOWNLOAD, progressNotifier,
cachingCounters, isCanceled,
isCanceled, true);
true); if (progressNotifier != null) {
downloadedSegments++; progressNotifier.onSegmentDownloaded();
} finally {
counters.newlyCachedBytes += cachingCounters.newlyCachedBytes;
updatePercentage();
} }
} }
} finally { } finally {
@ -143,26 +175,6 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M>> impleme
isCanceled.set(true); 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 @Override
public final void remove() throws InterruptedException { public final void remove() throws InterruptedException {
try { try {
@ -199,64 +211,15 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M>> impleme
* @param allowIncompleteList Whether to continue in the case that a load error prevents all * @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 * segments from being listed. If true then a partial segment list will be returned. If false
* an {@link IOException} will be thrown. * an {@link IOException} will be thrown.
* @return The list of downloadable {@link Segment}s.
* @throws InterruptedException Thrown if the thread was interrupted. * @throws InterruptedException Thrown if the thread was interrupted.
* @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if * @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. * 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<Segment> getSegments( protected abstract List<Segment> getSegments(
DataSource dataSource, M manifest, boolean allowIncompleteList) DataSource dataSource, M manifest, boolean allowIncompleteList)
throws InterruptedException, IOException; 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<Segment> initDownload() throws IOException, InterruptedException {
M manifest = getManifest(dataSource, manifestDataSpec);
if (!streamKeys.isEmpty()) {
manifest = manifest.copy(streamKeys);
}
List<Segment> 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) { private void removeDataSpec(DataSpec dataSpec) {
CacheUtil.remove(dataSpec, cache, cacheKeyFactory); CacheUtil.remove(dataSpec, cache, cacheKeyFactory);
} }
@ -269,4 +232,49 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M>> impleme
/* key= */ null, /* key= */ null,
/* flags= */ DataSpec.FLAG_ALLOW_GZIP); /* 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;
}
}
}
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream.cache;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
@ -31,36 +32,21 @@ import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* Caching related utility methods. * Caching related utility methods.
*/ */
@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"})
public final class CacheUtil { public final class CacheUtil {
/** Counters used during caching. */ /** Receives progress updates during cache operations. */
public static class CachingCounters { public interface ProgressListener {
/** 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;
/** /**
* 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() { void onProgress(long requestLength, long bytesCached, long newBytesCached);
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);
}
} }
/** Default buffer size to be used while caching. */ /** 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 * Queries the cache to obtain the request length and the number of bytes already cached for a
* for the content defined by a {@code dataSpec}. {@link CachingCounters#newlyCachedBytes} is * given {@link DataSpec}.
* reset to 0.
* *
* @param dataSpec Defines the data to be checked. * @param dataSpec Defines the data to be checked.
* @param cache A {@link Cache} which has the data. * @param cache A {@link Cache} which has the data.
* @param cacheKeyFactory An optional factory for cache keys. * @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( public static Pair<Long, Long> getCached(
DataSpec dataSpec, DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {
Cache cache,
@Nullable CacheKeyFactory cacheKeyFactory,
CachingCounters counters) {
String key = buildCacheKey(dataSpec, cacheKeyFactory); String key = buildCacheKey(dataSpec, cacheKeyFactory);
long position = dataSpec.absoluteStreamPosition; long position = dataSpec.absoluteStreamPosition;
long bytesLeft; long requestLength;
if (dataSpec.length != C.LENGTH_UNSET) { if (dataSpec.length != C.LENGTH_UNSET) {
bytesLeft = dataSpec.length; requestLength = dataSpec.length;
} else { } else {
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); 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; long bytesAlreadyCached = 0;
counters.alreadyCachedBytes = 0; long bytesLeft = requestLength;
counters.newlyCachedBytes = 0;
while (bytesLeft != 0) { while (bytesLeft != 0) {
long blockLength = long blockLength =
cache.getCachedLength( cache.getCachedLength(
key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE); key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE);
if (blockLength > 0) { if (blockLength > 0) {
counters.alreadyCachedBytes += blockLength; bytesAlreadyCached += blockLength;
} else { } else {
blockLength = -blockLength; blockLength = -blockLength;
if (blockLength == Long.MAX_VALUE) { if (blockLength == Long.MAX_VALUE) {
return; break;
} }
} }
position += blockLength; position += blockLength;
bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : 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 cache A {@link Cache} to store the data.
* @param cacheKeyFactory An optional factory for cache keys. * @param cacheKeyFactory An optional factory for cache keys.
* @param upstream A {@link DataSource} for reading data not in the cache. * @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. * @param isCanceled An optional flag that will interrupt caching if set to true.
* @throws IOException If an error occurs reading from the source. * @throws IOException If an error occurs reading from the source.
* @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}.
@ -142,7 +123,7 @@ public final class CacheUtil {
Cache cache, Cache cache,
@Nullable CacheKeyFactory cacheKeyFactory, @Nullable CacheKeyFactory cacheKeyFactory,
DataSource upstream, DataSource upstream,
@Nullable CachingCounters counters, @Nullable ProgressListener progressListener,
@Nullable AtomicBoolean isCanceled) @Nullable AtomicBoolean isCanceled)
throws IOException, InterruptedException { throws IOException, InterruptedException {
cache( cache(
@ -153,7 +134,7 @@ public final class CacheUtil {
new byte[DEFAULT_BUFFER_SIZE_BYTES], new byte[DEFAULT_BUFFER_SIZE_BYTES],
/* priorityTaskManager= */ null, /* priorityTaskManager= */ null,
/* priority= */ 0, /* priority= */ 0,
counters, progressListener,
isCanceled, isCanceled,
/* enableEOFException= */ false); /* 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 * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
* caching. * caching.
* @param priority The priority of this task. Used with {@code priorityTaskManager}. * @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 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 * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been
* reached unexpectedly. * reached unexpectedly.
@ -191,19 +172,18 @@ public final class CacheUtil {
byte[] buffer, byte[] buffer,
PriorityTaskManager priorityTaskManager, PriorityTaskManager priorityTaskManager,
int priority, int priority,
@Nullable CachingCounters counters, @Nullable ProgressListener progressListener,
@Nullable AtomicBoolean isCanceled, @Nullable AtomicBoolean isCanceled,
boolean enableEOFException) boolean enableEOFException)
throws IOException, InterruptedException { throws IOException, InterruptedException {
Assertions.checkNotNull(dataSource); Assertions.checkNotNull(dataSource);
Assertions.checkNotNull(buffer); Assertions.checkNotNull(buffer);
if (counters != null) { ProgressNotifier progressNotifier = null;
// Initialize the CachingCounter values. if (progressListener != null) {
getCached(dataSpec, cache, cacheKeyFactory, counters); progressNotifier = new ProgressNotifier(progressListener);
} else { Pair<Long, Long> lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory);
// Dummy CachingCounters. No need to initialize as they will not be visible to the caller. progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second);
counters = new CachingCounters();
} }
String key = buildCacheKey(dataSpec, cacheKeyFactory); String key = buildCacheKey(dataSpec, cacheKeyFactory);
@ -234,7 +214,7 @@ public final class CacheUtil {
buffer, buffer,
priorityTaskManager, priorityTaskManager,
priority, priority,
counters, progressNotifier,
isCanceled); isCanceled);
if (read < blockLength) { if (read < blockLength) {
// Reached to the end of the data. // 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 * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
* caching. * caching.
* @param priority The priority of this task. * @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. * @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 * @return Number of read bytes, or 0 if no data is available because the end of the opened range
* has been reached. * has been reached.
@ -274,7 +254,7 @@ public final class CacheUtil {
byte[] buffer, byte[] buffer,
PriorityTaskManager priorityTaskManager, PriorityTaskManager priorityTaskManager,
int priority, int priority,
CachingCounters counters, @Nullable ProgressNotifier progressNotifier,
AtomicBoolean isCanceled) AtomicBoolean isCanceled)
throws IOException, InterruptedException { throws IOException, InterruptedException {
long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition;
@ -298,8 +278,8 @@ public final class CacheUtil {
dataSpec.key, dataSpec.key,
dataSpec.flags); dataSpec.flags);
long resolvedLength = dataSource.open(dataSpec); long resolvedLength = dataSource.open(dataSpec);
if (counters.contentLength == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) { if (progressNotifier != null && resolvedLength != C.LENGTH_UNSET) {
counters.contentLength = positionOffset + resolvedLength; progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength);
} }
long totalBytesRead = 0; long totalBytesRead = 0;
while (totalBytesRead != length) { while (totalBytesRead != length) {
@ -312,14 +292,15 @@ public final class CacheUtil {
? (int) Math.min(buffer.length, length - totalBytesRead) ? (int) Math.min(buffer.length, length - totalBytesRead)
: buffer.length); : buffer.length);
if (bytesRead == C.RESULT_END_OF_INPUT) { if (bytesRead == C.RESULT_END_OF_INPUT) {
if (counters.contentLength == C.LENGTH_UNSET) { if (progressNotifier != null) {
counters.contentLength = positionOffset + totalBytesRead; progressNotifier.onRequestLengthResolved(positionOffset + totalBytesRead);
} }
break; break;
} }
totalBytesRead += bytesRead; totalBytesRead += bytesRead;
counters.newlyCachedBytes += bytesRead; if (progressNotifier != null) {
counters.updatePercentage(); progressNotifier.onBytesCached(bytesRead);
}
} }
return totalBytesRead; return totalBytesRead;
} catch (PriorityTaskManager.PriorityTooLowException exception) { } catch (PriorityTaskManager.PriorityTooLowException exception) {
@ -374,4 +355,34 @@ public final class CacheUtil {
private 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);
}
}
} }

View File

@ -76,9 +76,9 @@ public class DefaultDownloadIndexTest {
.setUri("different uri") .setUri("different uri")
.setCacheKey("different cacheKey") .setCacheKey("different cacheKey")
.setState(Download.STATE_FAILED) .setState(Download.STATE_FAILED)
.setDownloadPercentage(50) .setPercentDownloaded(50)
.setDownloadedBytes(200) .setBytesDownloaded(200)
.setTotalBytes(400) .setContentLength(400)
.setFailureReason(Download.FAILURE_REASON_UNKNOWN) .setFailureReason(Download.FAILURE_REASON_UNKNOWN)
.setStopReason(0x12345678) .setStopReason(0x12345678)
.setStartTimeMs(10) .setStartTimeMs(10)
@ -300,10 +300,10 @@ public class DefaultDownloadIndexTest {
assertThat(download.state).isEqualTo(that.state); assertThat(download.state).isEqualTo(that.state);
assertThat(download.startTimeMs).isEqualTo(that.startTimeMs); assertThat(download.startTimeMs).isEqualTo(that.startTimeMs);
assertThat(download.updateTimeMs).isEqualTo(that.updateTimeMs); 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.stopReason).isEqualTo(that.stopReason);
assertThat(download.getDownloadPercentage()).isEqualTo(that.getDownloadPercentage()); assertThat(download.failureReason).isEqualTo(that.failureReason);
assertThat(download.getDownloadedBytes()).isEqualTo(that.getDownloadedBytes()); assertThat(download.getPercentDownloaded()).isEqualTo(that.getPercentDownloaded());
assertThat(download.getTotalBytes()).isEqualTo(that.getTotalBytes()); assertThat(download.getBytesDownloaded()).isEqualTo(that.getBytesDownloaded());
} }
} }

View File

@ -17,7 +17,7 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; 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.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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 * creation for tests. Tests must avoid depending on the default values but explicitly set tested
* parameters during test initialization. * parameters during test initialization.
*/ */
class DownloadBuilder { /* package */ final class DownloadBuilder {
private final CachingCounters counters;
private final DownloadProgress progress;
private String id; private String id;
private String type; private String type;
private Uri uri; private Uri uri;
@Nullable private String cacheKey;
private int state;
private int failureReason;
private int stopReason;
private long startTimeMs;
private long updateTimeMs;
private List<StreamKey> streamKeys; private List<StreamKey> streamKeys;
@Nullable private String cacheKey;
private byte[] customMetadata; private byte[] customMetadata;
DownloadBuilder(String id) { private int state;
this(id, "type", Uri.parse("uri"), /* cacheKey= */ null, new byte[0], Collections.emptyList()); 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( this(
request.id, request.id,
request.type, request.type,
request.uri, request.uri,
request.streamKeys,
request.customCacheKey, request.customCacheKey,
request.data, request.data);
request.streamKeys);
} }
DownloadBuilder( /* package */ DownloadBuilder(
String id, String id,
String type, String type,
Uri uri, Uri uri,
List<StreamKey> streamKeys,
String cacheKey, String cacheKey,
byte[] customMetadata, byte[] customMetadata) {
List<StreamKey> streamKeys) {
this.id = id; this.id = id;
this.type = type; this.type = type;
this.uri = uri; 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.streamKeys = streamKeys;
this.cacheKey = cacheKey;
this.customMetadata = customMetadata; 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) { public DownloadBuilder setId(String id) {
@ -107,18 +116,18 @@ class DownloadBuilder {
return this; return this;
} }
public DownloadBuilder setDownloadPercentage(float downloadPercentage) { public DownloadBuilder setPercentDownloaded(float percentDownloaded) {
counters.percentage = downloadPercentage; progress.percentDownloaded = percentDownloaded;
return this; return this;
} }
public DownloadBuilder setDownloadedBytes(long downloadedBytes) { public DownloadBuilder setBytesDownloaded(long bytesDownloaded) {
counters.alreadyCachedBytes = downloadedBytes; progress.bytesDownloaded = bytesDownloaded;
return this; return this;
} }
public DownloadBuilder setTotalBytes(long totalBytes) { public DownloadBuilder setContentLength(long contentLength) {
counters.contentLength = totalBytes; this.contentLength = contentLength;
return this; return this;
} }
@ -156,6 +165,13 @@ class DownloadBuilder {
DownloadRequest request = DownloadRequest request =
new DownloadRequest(id, type, uri, streamKeys, cacheKey, customMetadata); new DownloadRequest(id, type, uri, streamKeys, cacheKey, customMetadata);
return new Download( return new Download(
request, state, failureReason, stopReason, startTimeMs, updateTimeMs, counters); request,
state,
startTimeMs,
updateTimeMs,
contentLength,
stopReason,
failureReason,
progress);
} }
} }

View File

@ -20,13 +20,13 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; 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.offline.Download.State;
import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable;
import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -184,7 +184,7 @@ public class DownloadManagerTest {
int tooManyRetries = MIN_RETRY_COUNT + 10; int tooManyRetries = MIN_RETRY_COUNT + 10;
for (int i = 0; i < tooManyRetries; i++) { for (int i = 0; i < tooManyRetries; i++) {
downloader.increaseDownloadedByteCount(); downloader.incrementBytesDownloaded();
downloader.assertStarted(MAX_RETRY_DELAY).fail(); downloader.assertStarted(MAX_RETRY_DELAY).fail();
} }
downloader.assertStarted(MAX_RETRY_DELAY).unblock(); downloader.assertStarted(MAX_RETRY_DELAY).unblock();
@ -555,11 +555,11 @@ public class DownloadManagerTest {
private static void assertEqualIgnoringTimeFields(Download download, Download that) { private static void assertEqualIgnoringTimeFields(Download download, Download that) {
assertThat(download.request).isEqualTo(that.request); assertThat(download.request).isEqualTo(that.request);
assertThat(download.state).isEqualTo(that.state); assertThat(download.state).isEqualTo(that.state);
assertThat(download.contentLength).isEqualTo(that.contentLength);
assertThat(download.failureReason).isEqualTo(that.failureReason); assertThat(download.failureReason).isEqualTo(that.failureReason);
assertThat(download.stopReason).isEqualTo(that.stopReason); assertThat(download.stopReason).isEqualTo(that.stopReason);
assertThat(download.getDownloadPercentage()).isEqualTo(that.getDownloadPercentage()); assertThat(download.getPercentDownloaded()).isEqualTo(that.getPercentDownloaded());
assertThat(download.getDownloadedBytes()).isEqualTo(that.getDownloadedBytes()); assertThat(download.getBytesDownloaded()).isEqualTo(that.getBytesDownloaded());
assertThat(download.getTotalBytes()).isEqualTo(that.getTotalBytes());
} }
private static DownloadRequest createDownloadRequest() { private static DownloadRequest createDownloadRequest() {
@ -722,21 +722,23 @@ public class DownloadManagerTest {
private volatile boolean cancelled; private volatile boolean cancelled;
private volatile boolean enableDownloadIOException; private volatile boolean enableDownloadIOException;
private volatile int startCount; private volatile int startCount;
private CachingCounters counters; private volatile int bytesDownloaded;
private FakeDownloader() { private FakeDownloader() {
this.started = new CountDownLatch(1); this.started = new CountDownLatch(1);
this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); this.blocker = new com.google.android.exoplayer2.util.ConditionVariable();
counters = new CachingCounters();
} }
@SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"}) @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
@Override @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. // It's ok to update this directly as no other thread will update it.
startCount++; startCount++;
started.countDown(); started.countDown();
block(); block();
if (bytesDownloaded > 0) {
listener.onProgress(C.LENGTH_UNSET, bytesDownloaded, C.PERCENTAGE_UNSET);
}
if (enableDownloadIOException) { if (enableDownloadIOException) {
enableDownloadIOException = false; enableDownloadIOException = false;
throw new IOException(); throw new IOException();
@ -783,7 +785,7 @@ public class DownloadManagerTest {
return this; return this;
} }
private FakeDownloader assertStartCount(int count) throws InterruptedException { private FakeDownloader assertStartCount(int count) {
assertThat(startCount).isEqualTo(count); assertThat(startCount).isEqualTo(count);
return this; return this;
} }
@ -823,34 +825,14 @@ public class DownloadManagerTest {
return unblock(); 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 { private void assertDoesNotStart() throws InterruptedException {
Thread.sleep(ASSERT_FALSE_TIME); Thread.sleep(ASSERT_FALSE_TIME);
assertThat(started.getCount()).isEqualTo(1); assertThat(started.getCount()).isEqualTo(1);
} }
@SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"}) @SuppressWarnings({"NonAtomicOperationOnVolatileField", "NonAtomicVolatileUpdate"})
private void increaseDownloadedByteCount() { private void incrementBytesDownloaded() {
counters.newlyCachedBytes++; bytesDownloaded++;
} }
} }
} }

View File

@ -343,7 +343,7 @@ public final class CacheDataSourceTest {
cache, cache,
/* cacheKeyFactory= */ null, /* cacheKeyFactory= */ null,
upstream2, upstream2,
/* counters= */ null, /* progressListener= */ null,
/* isCanceled= */ null); /* isCanceled= */ null);
// Read the rest of the data. // Read the rest of the data.
@ -392,7 +392,7 @@ public final class CacheDataSourceTest {
cache, cache,
/* cacheKeyFactory= */ null, /* cacheKeyFactory= */ null,
upstream2, upstream2,
/* counters= */ null, /* progressListener= */ null,
/* isCanceled= */ null); /* isCanceled= */ null);
// Read the rest of the data. // Read the rest of the data.
@ -416,7 +416,7 @@ public final class CacheDataSourceTest {
cache, cache,
/* cacheKeyFactory= */ null, /* cacheKeyFactory= */ null,
upstream, upstream,
/* counters= */ null, /* progressListener= */ null,
/* isCanceled= */ null); /* isCanceled= */ null);
// Create cache read-only CacheDataSource. // Create cache read-only CacheDataSource.
@ -452,7 +452,7 @@ public final class CacheDataSourceTest {
cache, cache,
/* cacheKeyFactory= */ null, /* cacheKeyFactory= */ null,
upstream, upstream,
/* counters= */ null, /* progressListener= */ null,
/* isCanceled= */ null); /* isCanceled= */ null);
// Create blocking CacheDataSource. // Create blocking CacheDataSource.

View File

@ -22,6 +22,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import android.net.Uri; import android.net.Uri;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; 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.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.EOFException; import java.io.EOFException;
import java.io.File; import java.io.File;
@ -100,12 +100,12 @@ public final class CacheUtilTest {
} }
@After @After
public void tearDown() throws Exception { public void tearDown() {
Util.recursiveDelete(tempFolder); Util.recursiveDelete(tempFolder);
} }
@Test @Test
public void testGenerateKey() throws Exception { public void testGenerateKey() {
assertThat(CacheUtil.generateKey(Uri.EMPTY)).isNotNull(); assertThat(CacheUtil.generateKey(Uri.EMPTY)).isNotNull();
Uri testUri = Uri.parse("test"); Uri testUri = Uri.parse("test");
@ -120,7 +120,7 @@ public final class CacheUtilTest {
} }
@Test @Test
public void testDefaultCacheKeyFactory_buildCacheKey() throws Exception { public void testDefaultCacheKeyFactory_buildCacheKey() {
Uri testUri = Uri.parse("test"); Uri testUri = Uri.parse("test");
String key = "key"; String key = "key";
// If DataSpec.key is present, returns it. // If DataSpec.key is present, returns it.
@ -136,62 +136,66 @@ public final class CacheUtilTest {
} }
@Test @Test
public void testGetCachedNoData() throws Exception { public void testGetCachedNoData() {
CachingCounters counters = new CachingCounters(); Pair<Long, Long> contentLengthAndBytesCached =
CacheUtil.getCached( CacheUtil.getCached(
new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); 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 @Test
public void testGetCachedDataUnknownLength() throws Exception { public void testGetCachedDataUnknownLength() {
// Mock there is 100 bytes cached at the beginning // Mock there is 100 bytes cached at the beginning
mockCache.spansAndGaps = new int[] {100}; mockCache.spansAndGaps = new int[] {100};
CachingCounters counters = new CachingCounters(); Pair<Long, Long> contentLengthAndBytesCached =
CacheUtil.getCached( CacheUtil.getCached(
new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); 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 @Test
public void testGetCachedNoDataKnownLength() throws Exception { public void testGetCachedNoDataKnownLength() {
mockCache.contentLength = 1000; mockCache.contentLength = 1000;
CachingCounters counters = new CachingCounters(); Pair<Long, Long> contentLengthAndBytesCached =
CacheUtil.getCached( CacheUtil.getCached(
new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null);
assertCounters(counters, 0, 0, 1000); assertThat(contentLengthAndBytesCached.first).isEqualTo(1000);
assertThat(contentLengthAndBytesCached.second).isEqualTo(0);
} }
@Test @Test
public void testGetCached() throws Exception { public void testGetCached() {
mockCache.contentLength = 1000; mockCache.contentLength = 1000;
mockCache.spansAndGaps = new int[] {100, 100, 200}; mockCache.spansAndGaps = new int[] {100, 100, 200};
CachingCounters counters = new CachingCounters(); Pair<Long, Long> contentLengthAndBytesCached =
CacheUtil.getCached( CacheUtil.getCached(
new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null, counters); new DataSpec(Uri.parse("test")), mockCache, /* cacheKeyFactory= */ null);
assertCounters(counters, 300, 0, 1000); assertThat(contentLengthAndBytesCached.first).isEqualTo(1000);
assertThat(contentLengthAndBytesCached.second).isEqualTo(300);
} }
@Test @Test
public void testGetCachedFromNonZeroPosition() throws Exception { public void testGetCachedFromNonZeroPosition() {
mockCache.contentLength = 1000; mockCache.contentLength = 1000;
mockCache.spansAndGaps = new int[] {100, 100, 200}; mockCache.spansAndGaps = new int[] {100, 100, 200};
CachingCounters counters = new CachingCounters(); Pair<Long, Long> contentLengthAndBytesCached =
CacheUtil.getCached( CacheUtil.getCached(
new DataSpec( new DataSpec(
Uri.parse("test"), Uri.parse("test"),
/* absoluteStreamPosition= */ 100, /* absoluteStreamPosition= */ 100,
/* length= */ C.LENGTH_UNSET, /* length= */ C.LENGTH_UNSET,
/* key= */ null), /* key= */ null),
mockCache, mockCache,
/* cacheKeyFactory= */ null, /* cacheKeyFactory= */ null);
counters);
assertCounters(counters, 200, 0, 900); assertThat(contentLengthAndBytesCached.first).isEqualTo(900);
assertThat(contentLengthAndBytesCached.second).isEqualTo(200);
} }
@Test @Test
@ -208,7 +212,7 @@ public final class CacheUtilTest {
counters, counters,
/* isCanceled= */ null); /* isCanceled= */ null);
assertCounters(counters, 0, 100, 100); counters.assertValues(0, 100, 100);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -223,7 +227,8 @@ public final class CacheUtilTest {
CacheUtil.cache( CacheUtil.cache(
dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
assertCounters(counters, 0, 20, 20); counters.assertValues(0, 20, 20);
counters.reset();
CacheUtil.cache( CacheUtil.cache(
new DataSpec(testUri), new DataSpec(testUri),
@ -233,7 +238,7 @@ public final class CacheUtilTest {
counters, counters,
/* isCanceled= */ null); /* isCanceled= */ null);
assertCounters(counters, 20, 80, 100); counters.assertValues(20, 80, 100);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -249,7 +254,7 @@ public final class CacheUtilTest {
CacheUtil.cache( CacheUtil.cache(
dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
assertCounters(counters, 0, 100, 100); counters.assertValues(0, 100, 100);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -266,7 +271,8 @@ public final class CacheUtilTest {
CacheUtil.cache( CacheUtil.cache(
dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
assertCounters(counters, 0, 20, 20); counters.assertValues(0, 20, 20);
counters.reset();
CacheUtil.cache( CacheUtil.cache(
new DataSpec(testUri), new DataSpec(testUri),
@ -276,7 +282,7 @@ public final class CacheUtilTest {
counters, counters,
/* isCanceled= */ null); /* isCanceled= */ null);
assertCounters(counters, 20, 80, 100); counters.assertValues(20, 80, 100);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -291,7 +297,7 @@ public final class CacheUtilTest {
CacheUtil.cache( CacheUtil.cache(
dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
assertCounters(counters, 0, 100, 1000); counters.assertValues(0, 100, 1000);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -312,7 +318,7 @@ public final class CacheUtilTest {
new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES],
/* priorityTaskManager= */ null, /* priorityTaskManager= */ null,
/* priority= */ 0, /* priority= */ 0,
/* counters= */ null, /* progressListener= */ null,
/* isCanceled= */ null, /* isCanceled= */ null,
/* enableEOFException= */ true); /* enableEOFException= */ true);
fail(); fail();
@ -328,9 +334,9 @@ public final class CacheUtilTest {
new FakeDataSet() new FakeDataSet()
.newData("test_data") .newData("test_data")
.appendReadData(TestUtil.buildTestData(100)) .appendReadData(TestUtil.buildTestData(100))
.appendReadAction(() -> assertCounters(counters, 0, 100, 300)) .appendReadAction(() -> counters.assertValues(0, 100, 300))
.appendReadData(TestUtil.buildTestData(100)) .appendReadData(TestUtil.buildTestData(100))
.appendReadAction(() -> assertCounters(counters, 0, 200, 300)) .appendReadAction(() -> counters.assertValues(0, 200, 300))
.appendReadData(TestUtil.buildTestData(100)) .appendReadData(TestUtil.buildTestData(100))
.endData(); .endData();
FakeDataSource dataSource = new FakeDataSource(fakeDataSet); FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
@ -343,7 +349,7 @@ public final class CacheUtilTest {
counters, counters,
/* isCanceled= */ null); /* isCanceled= */ null);
assertCounters(counters, 0, 300, 300); counters.assertValues(0, 300, 300);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -369,7 +375,7 @@ public final class CacheUtilTest {
new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES],
/* priorityTaskManager= */ null, /* priorityTaskManager= */ null,
/* priority= */ 0, /* priority= */ 0,
/* counters= */ null, /* progressListener= */ null,
/* isCanceled= */ null, /* isCanceled= */ null,
true); true);
CacheUtil.remove(dataSpec, cache, /* cacheKeyFactory= */ null); CacheUtil.remove(dataSpec, cache, /* cacheKeyFactory= */ null);
@ -377,10 +383,34 @@ public final class CacheUtilTest {
assertCacheEmpty(cache); assertCacheEmpty(cache);
} }
private static void assertCounters(CachingCounters counters, int alreadyCachedBytes, private static final class CachingCounters implements CacheUtil.ProgressListener {
int newlyCachedBytes, int contentLength) {
assertThat(counters.alreadyCachedBytes).isEqualTo(alreadyCachedBytes); private long contentLength = C.LENGTH_UNSET;
assertThat(counters.newlyCachedBytes).isEqualTo(newlyCachedBytes); private long bytesAlreadyCached;
assertThat(counters.contentLength).isEqualTo(contentLength); 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;
}
} }
} }

View File

@ -45,7 +45,7 @@ import java.util.List;
* <p>Example usage: * <p>Example usage:
* *
* <pre>{@code * <pre>{@code
* SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor()); * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
* DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null); * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
* DownloaderConstructorHelper constructorHelper = * DownloaderConstructorHelper constructorHelper =
* new DownloaderConstructorHelper(cache, factory); * new DownloaderConstructorHelper(cache, factory);
@ -55,7 +55,7 @@ import java.util.List;
* new DashDownloader( * new DashDownloader(
* manifestUrl, Collections.singletonList(new StreamKey(0, 0, 0)), constructorHelper); * manifestUrl, Collections.singletonList(new StreamKey(0, 0, 0)), constructorHelper);
* // Perform the download. * // Perform the download.
* dashDownloader.download(); * dashDownloader.download(progressListener);
* // Access downloaded data using CacheDataSource * // Access downloaded data using CacheDataSource
* CacheDataSource cacheDataSource = * CacheDataSource cacheDataSource =
* new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE); * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);

View File

@ -62,6 +62,7 @@ public class DashDownloaderTest {
private SimpleCache cache; private SimpleCache cache;
private File tempFolder; private File tempFolder;
private ProgressListener progressListener;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@ -69,6 +70,7 @@ public class DashDownloaderTest {
tempFolder = tempFolder =
Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest"); Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
progressListener = new ProgressListener();
} }
@After @After
@ -77,7 +79,7 @@ public class DashDownloaderTest {
} }
@Test @Test
public void testCreateWithDefaultDownloaderFactory() throws Exception { public void testCreateWithDefaultDownloaderFactory() {
DownloaderConstructorHelper constructorHelper = DownloaderConstructorHelper constructorHelper =
new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper); DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
@ -105,7 +107,7 @@ public class DashDownloaderTest {
.setRandomData("audio_segment_3", 6); .setRandomData("audio_segment_3", 6);
DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
dashDownloader.download(); dashDownloader.download(progressListener);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -124,7 +126,7 @@ public class DashDownloaderTest {
.setRandomData("audio_segment_3", 6); .setRandomData("audio_segment_3", 6);
DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
dashDownloader.download(); dashDownloader.download(progressListener);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -143,7 +145,7 @@ public class DashDownloaderTest {
DashDownloader dashDownloader = DashDownloader dashDownloader =
getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0)); getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
dashDownloader.download(); dashDownloader.download(progressListener);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -164,7 +166,7 @@ public class DashDownloaderTest {
.setRandomData("period_2_segment_3", 3); .setRandomData("period_2_segment_3", 3);
DashDownloader dashDownloader = getDashDownloader(fakeDataSet); DashDownloader dashDownloader = getDashDownloader(fakeDataSet);
dashDownloader.download(); dashDownloader.download(progressListener);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -186,7 +188,7 @@ public class DashDownloaderTest {
DashDownloader dashDownloader = DashDownloader dashDownloader =
getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0)); getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
dashDownloader.download(); dashDownloader.download(progressListener);
DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs(); DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs();
assertThat(openedDataSpecs.length).isEqualTo(8); assertThat(openedDataSpecs.length).isEqualTo(8);
@ -218,7 +220,7 @@ public class DashDownloaderTest {
DashDownloader dashDownloader = DashDownloader dashDownloader =
getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(1, 0, 0)); getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(1, 0, 0));
dashDownloader.download(); dashDownloader.download(progressListener);
DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs(); DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs();
assertThat(openedDataSpecs.length).isEqualTo(8); assertThat(openedDataSpecs.length).isEqualTo(8);
@ -248,12 +250,12 @@ public class DashDownloaderTest {
DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
try { try {
dashDownloader.download(); dashDownloader.download(progressListener);
fail(); fail();
} catch (IOException e) { } catch (IOException e) {
// Expected. // Expected.
} }
dashDownloader.download(); dashDownloader.download(progressListener);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -272,18 +274,17 @@ public class DashDownloaderTest {
.setRandomData("audio_segment_3", 6); .setRandomData("audio_segment_3", 6);
DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(0);
try { try {
dashDownloader.download(); dashDownloader.download(progressListener);
fail(); fail();
} catch (IOException e) { } catch (IOException e) {
// Failure expected after downloading init data, segment 1 and 2 bytes in segment 2. // 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(); dashDownloader.download(progressListener);
assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(10 + 4 + 5 + 6); progressListener.assertBytesDownloaded(10 + 4 + 5 + 6);
} }
@Test @Test
@ -301,7 +302,7 @@ public class DashDownloaderTest {
DashDownloader dashDownloader = DashDownloader dashDownloader =
getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0)); getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));
dashDownloader.download(); dashDownloader.download(progressListener);
dashDownloader.remove(); dashDownloader.remove();
assertCacheEmpty(cache); assertCacheEmpty(cache);
} }
@ -315,7 +316,7 @@ public class DashDownloaderTest {
DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));
try { try {
dashDownloader.download(); dashDownloader.download(progressListener);
fail(); fail();
} catch (DownloadException e) { } catch (DownloadException e) {
// Expected. // Expected.
@ -339,4 +340,17 @@ public class DashDownloaderTest {
return keysList; 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);
}
}
} }

View File

@ -39,7 +39,7 @@ import java.util.List;
* <p>Example usage: * <p>Example usage:
* *
* <pre>{@code * <pre>{@code
* SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor()); * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
* DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null); * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
* DownloaderConstructorHelper constructorHelper = * DownloaderConstructorHelper constructorHelper =
* new DownloaderConstructorHelper(cache, factory); * new DownloaderConstructorHelper(cache, factory);
@ -50,7 +50,7 @@ import java.util.List;
* Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0)), * Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0)),
* constructorHelper); * constructorHelper);
* // Perform the download. * // Perform the download.
* hlsDownloader.download(); * hlsDownloader.download(progressListener);
* // Access downloaded data using CacheDataSource * // Access downloaded data using CacheDataSource
* CacheDataSource cacheDataSource = * CacheDataSource cacheDataSource =
* new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE); * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);

View File

@ -67,6 +67,7 @@ public class HlsDownloaderTest {
private SimpleCache cache; private SimpleCache cache;
private File tempFolder; private File tempFolder;
private ProgressListener progressListener;
private FakeDataSet fakeDataSet; private FakeDataSet fakeDataSet;
@Before @Before
@ -74,7 +75,7 @@ public class HlsDownloaderTest {
tempFolder = tempFolder =
Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest"); Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
progressListener = new ProgressListener();
fakeDataSet = fakeDataSet =
new FakeDataSet() new FakeDataSet()
.setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA) .setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA)
@ -94,7 +95,7 @@ public class HlsDownloaderTest {
} }
@Test @Test
public void testCreateWithDefaultDownloaderFactory() throws Exception { public void testCreateWithDefaultDownloaderFactory() {
DownloaderConstructorHelper constructorHelper = DownloaderConstructorHelper constructorHelper =
new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY); new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper); DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
@ -115,17 +116,16 @@ public class HlsDownloaderTest {
public void testCounterMethods() throws Exception { public void testCounterMethods() throws Exception {
HlsDownloader downloader = HlsDownloader downloader =
getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX)); getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX));
downloader.download(); downloader.download(progressListener);
assertThat(downloader.getDownloadedBytes()) progressListener.assertBytesDownloaded(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
.isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);
} }
@Test @Test
public void testDownloadRepresentation() throws Exception { public void testDownloadRepresentation() throws Exception {
HlsDownloader downloader = HlsDownloader downloader =
getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX)); getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX));
downloader.download(); downloader.download(progressListener);
assertCachedData( assertCachedData(
cache, cache,
@ -143,7 +143,7 @@ public class HlsDownloaderTest {
getHlsDownloader( getHlsDownloader(
MASTER_PLAYLIST_URI, MASTER_PLAYLIST_URI,
getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX)); getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX));
downloader.download(); downloader.download(progressListener);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -162,7 +162,7 @@ public class HlsDownloaderTest {
.setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence2.ts", 15); .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence2.ts", 15);
HlsDownloader downloader = getHlsDownloader(MASTER_PLAYLIST_URI, getKeys()); HlsDownloader downloader = getHlsDownloader(MASTER_PLAYLIST_URI, getKeys());
downloader.download(); downloader.download(progressListener);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -173,7 +173,7 @@ public class HlsDownloaderTest {
getHlsDownloader( getHlsDownloader(
MASTER_PLAYLIST_URI, MASTER_PLAYLIST_URI,
getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX)); getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX));
downloader.download(); downloader.download(progressListener);
downloader.remove(); downloader.remove();
assertCacheEmpty(cache); assertCacheEmpty(cache);
@ -182,7 +182,7 @@ public class HlsDownloaderTest {
@Test @Test
public void testDownloadMediaPlaylist() throws Exception { public void testDownloadMediaPlaylist() throws Exception {
HlsDownloader downloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI, getKeys()); HlsDownloader downloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI, getKeys());
downloader.download(); downloader.download(progressListener);
assertCachedData( assertCachedData(
cache, cache,
@ -205,7 +205,7 @@ public class HlsDownloaderTest {
.setRandomData("fileSequence2.ts", 12); .setRandomData("fileSequence2.ts", 12);
HlsDownloader downloader = getHlsDownloader(ENC_MEDIA_PLAYLIST_URI, getKeys()); HlsDownloader downloader = getHlsDownloader(ENC_MEDIA_PLAYLIST_URI, getKeys());
downloader.download(); downloader.download(progressListener);
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@ -222,4 +222,18 @@ public class HlsDownloaderTest {
} }
return streamKeys; 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);
}
}
} }

View File

@ -37,7 +37,7 @@ import java.util.List;
* <p>Example usage: * <p>Example usage:
* *
* <pre>{@code * <pre>{@code
* SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor()); * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
* DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null); * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
* DownloaderConstructorHelper constructorHelper = * DownloaderConstructorHelper constructorHelper =
* new DownloaderConstructorHelper(cache, factory); * new DownloaderConstructorHelper(cache, factory);
@ -48,7 +48,7 @@ import java.util.List;
* Collections.singletonList(new StreamKey(0, 0)), * Collections.singletonList(new StreamKey(0, 0)),
* constructorHelper); * constructorHelper);
* // Perform the download. * // Perform the download.
* ssDownloader.download(); * ssDownloader.download(progressListener);
* // Access downloaded data using CacheDataSource * // Access downloaded data using CacheDataSource
* CacheDataSource cacheDataSource = * CacheDataSource cacheDataSource =
* new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE); * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);

View File

@ -75,12 +75,12 @@ public final class DownloadNotificationHelper {
continue; continue;
} }
haveDownloadTasks = true; haveDownloadTasks = true;
float downloadPercentage = download.getDownloadPercentage(); float downloadPercentage = download.getPercentDownloaded();
if (downloadPercentage != C.PERCENTAGE_UNSET) { if (downloadPercentage != C.PERCENTAGE_UNSET) {
allDownloadPercentagesUnknown = false; allDownloadPercentagesUnknown = false;
totalPercentage += downloadPercentage; totalPercentage += downloadPercentage;
} }
haveDownloadedBytes |= download.getDownloadedBytes() > 0; haveDownloadedBytes |= download.getBytesDownloaded() > 0;
downloadTaskCount++; downloadTaskCount++;
} }

View File

@ -89,7 +89,7 @@ public final class DashDownloadTest {
@Test @Test
public void testDownload() throws Exception { public void testDownload() throws Exception {
DashDownloader dashDownloader = downloadContent(); DashDownloader dashDownloader = downloadContent();
dashDownloader.download(); dashDownloader.download(/* progressListener= */ null);
testRunner testRunner
.setStreamName("test_h264_fixed_download") .setStreamName("test_h264_fixed_download")