Commit 3919f384 by eguven Committed by Oliver Woodman

Make DownloadManager use DownloadIndex

PiperOrigin-RevId: 240320220
parent e4b49477
...@@ -114,21 +114,14 @@ public class DemoApplication extends Application { ...@@ -114,21 +114,14 @@ public class DemoApplication extends Application {
private synchronized void initDownloadManager() { private synchronized void initDownloadManager() {
if (downloadManager == null) { if (downloadManager == null) {
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(new ExoDatabaseProvider(this)); DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(new ExoDatabaseProvider(this));
File actionFile = new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE); upgradeActionFile(DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex);
if (actionFile.exists()) { upgradeActionFile(DOWNLOAD_ACTION_FILE, downloadIndex);
try {
DownloadIndexUtil.upgradeActionFile(new ActionFile(actionFile), downloadIndex, null);
} catch (IOException e) {
Log.e(TAG, "Upgrading action file failed", e);
}
actionFile.delete();
}
DownloaderConstructorHelper downloaderConstructorHelper = DownloaderConstructorHelper downloaderConstructorHelper =
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
downloadManager = downloadManager =
new DownloadManager( new DownloadManager(
this, this,
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE), downloadIndex,
new DefaultDownloaderFactory(downloaderConstructorHelper), new DefaultDownloaderFactory(downloaderConstructorHelper),
MAX_SIMULTANEOUS_DOWNLOADS, MAX_SIMULTANEOUS_DOWNLOADS,
DownloadManager.DEFAULT_MIN_RETRY_COUNT, DownloadManager.DEFAULT_MIN_RETRY_COUNT,
...@@ -139,6 +132,18 @@ public class DemoApplication extends Application { ...@@ -139,6 +132,18 @@ public class DemoApplication extends Application {
} }
} }
private void upgradeActionFile(String file, DefaultDownloadIndex downloadIndex) {
ActionFile actionFile = new ActionFile(new File(getDownloadDirectory(), file));
if (actionFile.exists()) {
try {
DownloadIndexUtil.upgradeActionFile(actionFile, downloadIndex, null);
} catch (IOException e) {
Log.e(TAG, "Upgrading action file failed", e);
}
actionFile.delete();
}
}
private File getDownloadDirectory() { private File getDownloadDirectory() {
if (downloadDirectory == null) { if (downloadDirectory == null) {
downloadDirectory = getExternalFilesDir(null); downloadDirectory = getExternalFilesDir(null);
......
...@@ -18,8 +18,6 @@ package com.google.android.exoplayer2.demo; ...@@ -18,8 +18,6 @@ package com.google.android.exoplayer2.demo;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import android.widget.Toast; import android.widget.Toast;
...@@ -66,9 +64,8 @@ public class DownloadTracker implements DownloadManager.Listener { ...@@ -66,9 +64,8 @@ public class DownloadTracker implements DownloadManager.Listener {
private final Context context; private final Context context;
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
private final CopyOnWriteArraySet<Listener> listeners; private final CopyOnWriteArraySet<Listener> listeners;
private final HashMap<Uri, DownloadState> trackedDownloadStates; private final HashMap<Uri, DownloadState> downloadStates;
private final DefaultDownloadIndex downloadIndex; private final DefaultDownloadIndex downloadIndex;
private final Handler indexHandler;
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper; @Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
...@@ -78,11 +75,8 @@ public class DownloadTracker implements DownloadManager.Listener { ...@@ -78,11 +75,8 @@ public class DownloadTracker implements DownloadManager.Listener {
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.downloadIndex = downloadIndex; this.downloadIndex = downloadIndex;
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
trackedDownloadStates = new HashMap<>(); downloadStates = new HashMap<>();
HandlerThread indexThread = new HandlerThread("DownloadTracker"); loadDownloads();
indexThread.start();
indexHandler = new Handler(indexThread.getLooper());
loadTrackedActions();
} }
public void addListener(Listener listener) { public void addListener(Listener listener) {
...@@ -94,15 +88,16 @@ public class DownloadTracker implements DownloadManager.Listener { ...@@ -94,15 +88,16 @@ public class DownloadTracker implements DownloadManager.Listener {
} }
public boolean isDownloaded(Uri uri) { public boolean isDownloaded(Uri uri) {
return trackedDownloadStates.containsKey(uri); DownloadState downloadState = downloadStates.get(uri);
return downloadState != null && downloadState.state != DownloadState.STATE_FAILED;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public List<StreamKey> getOfflineStreamKeys(Uri uri) { public List<StreamKey> getOfflineStreamKeys(Uri uri) {
if (!trackedDownloadStates.containsKey(uri)) { DownloadState downloadState = downloadStates.get(uri);
return Collections.emptyList(); return downloadState != null && downloadState.state != DownloadState.STATE_FAILED
} ? Arrays.asList(downloadState.streamKeys)
return Arrays.asList(trackedDownloadStates.get(uri).streamKeys); : Collections.emptyList();
} }
public void toggleDownload( public void toggleDownload(
...@@ -129,59 +124,34 @@ public class DownloadTracker implements DownloadManager.Listener { ...@@ -129,59 +124,34 @@ public class DownloadTracker implements DownloadManager.Listener {
@Override @Override
public void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState) { public void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState) {
if (downloadState.state == DownloadState.STATE_REMOVED boolean downloaded = isDownloaded(downloadState.uri);
|| downloadState.state == DownloadState.STATE_FAILED) { if (downloadState.state == DownloadState.STATE_REMOVED) {
// A download has been removed, or has failed. Stop tracking it. downloadStates.remove(downloadState.uri);
if (trackedDownloadStates.remove(downloadState.uri) != null) { } else {
handleTrackedDownloadStateChanged(downloadState); downloadStates.put(downloadState.uri, downloadState);
}
if (downloaded != isDownloaded(downloadState.uri)) {
for (Listener listener : listeners) {
listener.onDownloadsChanged();
} }
} }
} }
// Internal methods // Internal methods
private void loadTrackedActions() { private void loadDownloads() {
try { try {
DownloadStateCursor downloadStates = downloadIndex.getDownloadStates(); DownloadStateCursor loadedDownloadStates = downloadIndex.getDownloadStates();
while (downloadStates.moveToNext()) { while (loadedDownloadStates.moveToNext()) {
DownloadState downloadState = downloadStates.getDownloadState(); DownloadState downloadState = loadedDownloadStates.getDownloadState();
trackedDownloadStates.put(downloadState.uri, downloadState); downloadStates.put(downloadState.uri, downloadState);
} }
downloadStates.close(); loadedDownloadStates.close();
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, "Failed to query download states", e); Log.w(TAG, "Failed to query download states", e);
} }
} }
private void handleTrackedDownloadStateChanged(DownloadState downloadState) {
for (Listener listener : listeners) {
listener.onDownloadsChanged();
}
indexHandler.post(
() -> {
try {
if (downloadState.state == DownloadState.STATE_REMOVED) {
downloadIndex.removeDownloadState(downloadState.id);
} else {
downloadIndex.putDownloadState(downloadState);
}
} catch (IOException e) {
// TODO: This whole method is going away in cr/232854678.
}
});
}
private void startDownload(DownloadAction action) {
if (trackedDownloadStates.containsKey(action.uri)) {
// This content is already being downloaded. Do nothing.
return;
}
DownloadState downloadState = new DownloadState(action);
trackedDownloadStates.put(downloadState.uri, downloadState);
handleTrackedDownloadStateChanged(downloadState);
startServiceWithAction(action);
}
private void startServiceWithAction(DownloadAction action) { private void startServiceWithAction(DownloadAction action) {
DownloadService.startWithAction(context, DemoDownloadService.class, action, false); DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
} }
...@@ -238,7 +208,7 @@ public class DownloadTracker implements DownloadManager.Listener { ...@@ -238,7 +208,7 @@ public class DownloadTracker implements DownloadManager.Listener {
if (helper.getPeriodCount() == 0) { if (helper.getPeriodCount() == 0) {
Log.d(TAG, "No periods found. Downloading entire stream."); Log.d(TAG, "No periods found. Downloading entire stream.");
DownloadAction downloadAction = downloadHelper.getDownloadAction(Util.getUtf8Bytes(name)); DownloadAction downloadAction = downloadHelper.getDownloadAction(Util.getUtf8Bytes(name));
startDownload(downloadAction); startServiceWithAction(downloadAction);
downloadHelper.release(); downloadHelper.release();
return; return;
} }
...@@ -280,7 +250,7 @@ public class DownloadTracker implements DownloadManager.Listener { ...@@ -280,7 +250,7 @@ public class DownloadTracker implements DownloadManager.Listener {
} }
} }
DownloadAction downloadAction = downloadHelper.getDownloadAction(Util.getUtf8Bytes(name)); DownloadAction downloadAction = downloadHelper.getDownloadAction(Util.getUtf8Bytes(name));
startDownload(downloadAction); startServiceWithAction(downloadAction);
} }
// DialogInterface.OnDismissListener implementation. // DialogInterface.OnDismissListener implementation.
......
...@@ -36,11 +36,12 @@ import android.os.Looper; ...@@ -36,11 +36,12 @@ import android.os.Looper;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.database.DatabaseIOException;
import com.google.android.exoplayer2.database.DatabaseProvider;
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.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
...@@ -48,7 +49,6 @@ import java.util.ArrayDeque; ...@@ -48,7 +49,6 @@ import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...@@ -130,7 +130,7 @@ public final class DownloadManager { ...@@ -130,7 +130,7 @@ public final class DownloadManager {
private final int maxSimultaneousDownloads; private final int maxSimultaneousDownloads;
private final int minRetryCount; private final int minRetryCount;
private final Context context; private final Context context;
private final ActionFile actionFile; private final DefaultDownloadIndex downloadIndex;
private final DownloaderFactory downloaderFactory; private final DownloaderFactory downloaderFactory;
private final ArrayList<Download> downloads; private final ArrayList<Download> downloads;
private final HashMap<Download, DownloadThread> activeDownloads; private final HashMap<Download, DownloadThread> activeDownloads;
...@@ -146,18 +146,20 @@ public final class DownloadManager { ...@@ -146,18 +146,20 @@ public final class DownloadManager {
private int manualStopReason; private int manualStopReason;
private RequirementsWatcher requirementsWatcher; private RequirementsWatcher requirementsWatcher;
private int simultaneousDownloads; private int simultaneousDownloads;
private boolean loadingDownload;
/** /**
* Constructs a {@link DownloadManager}. * Constructs a {@link DownloadManager}.
* *
* @param context Any context. * @param context Any context.
* @param actionFile The file in which active actions are saved. * @param databaseProvider Used to create a {@link DownloadIndex} which holds download states.
* @param downloaderFactory A factory for creating {@link Downloader}s. * @param downloaderFactory A factory for creating {@link Downloader}s.
*/ */
public DownloadManager(Context context, File actionFile, DownloaderFactory downloaderFactory) { public DownloadManager(
Context context, DatabaseProvider databaseProvider, DownloaderFactory downloaderFactory) {
this( this(
context, context,
actionFile, databaseProvider,
downloaderFactory, downloaderFactory,
DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS,
DEFAULT_MIN_RETRY_COUNT, DEFAULT_MIN_RETRY_COUNT,
...@@ -168,7 +170,33 @@ public final class DownloadManager { ...@@ -168,7 +170,33 @@ public final class DownloadManager {
* Constructs a {@link DownloadManager}. * Constructs a {@link DownloadManager}.
* *
* @param context Any context. * @param context Any context.
* @param actionFile The file in which active actions are saved. * @param databaseProvider Used to create a {@link DownloadIndex} which holds download states.
* @param downloaderFactory A factory for creating {@link Downloader}s.
* @param maxSimultaneousDownloads The maximum number of simultaneous downloads.
* @param minRetryCount The minimum number of times a download must be retried before failing.
* @param requirements The requirements needed to be met to start downloads.
*/
public DownloadManager(
Context context,
DatabaseProvider databaseProvider,
DownloaderFactory downloaderFactory,
int maxSimultaneousDownloads,
int minRetryCount,
Requirements requirements) {
this(
context,
new DefaultDownloadIndex(databaseProvider),
downloaderFactory,
maxSimultaneousDownloads,
minRetryCount,
requirements);
}
/**
* Constructs a {@link DownloadManager}.
*
* @param context Any context.
* @param downloadIndex The {@link DefaultDownloadIndex} which holds download states.
* @param downloaderFactory A factory for creating {@link Downloader}s. * @param downloaderFactory A factory for creating {@link Downloader}s.
* @param maxSimultaneousDownloads The maximum number of simultaneous downloads. * @param maxSimultaneousDownloads The maximum number of simultaneous downloads.
* @param minRetryCount The minimum number of times a download must be retried before failing. * @param minRetryCount The minimum number of times a download must be retried before failing.
...@@ -176,13 +204,13 @@ public final class DownloadManager { ...@@ -176,13 +204,13 @@ public final class DownloadManager {
*/ */
public DownloadManager( public DownloadManager(
Context context, Context context,
File actionFile, DefaultDownloadIndex downloadIndex,
DownloaderFactory downloaderFactory, DownloaderFactory downloaderFactory,
int maxSimultaneousDownloads, int maxSimultaneousDownloads,
int minRetryCount, int minRetryCount,
Requirements requirements) { Requirements requirements) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.actionFile = new ActionFile(actionFile); this.downloadIndex = downloadIndex;
this.downloaderFactory = downloaderFactory; this.downloaderFactory = downloaderFactory;
this.maxSimultaneousDownloads = maxSimultaneousDownloads; this.maxSimultaneousDownloads = maxSimultaneousDownloads;
this.minRetryCount = minRetryCount; this.minRetryCount = minRetryCount;
...@@ -204,11 +232,16 @@ public final class DownloadManager { ...@@ -204,11 +232,16 @@ public final class DownloadManager {
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
actionQueue = new ArrayDeque<>(); actionQueue = new ArrayDeque<>();
notMetRequirements = watchRequirements(requirements); setNotMetRequirements(watchRequirements(requirements));
loadActions(); loadDownloads();
logd("Created"); logd("Created");
} }
/** Returns the used {@link DownloadIndex}. */
public DownloadIndex getDownloadIndex() {
return downloadIndex;
}
/** /**
* Sets the requirements needed to be met to start downloads. * Sets the requirements needed to be met to start downloads.
* *
...@@ -251,10 +284,7 @@ public final class DownloadManager { ...@@ -251,10 +284,7 @@ public final class DownloadManager {
*/ */
public void startDownloads() { public void startDownloads() {
logd("manual stop is cancelled"); logd("manual stop is cancelled");
manualStopReason = MANUAL_STOP_REASON_NONE; setManualStopReason(/* id= */ null, MANUAL_STOP_REASON_NONE);
for (int i = 0; i < downloads.size(); i++) {
downloads.get(i).setManualStopReason(MANUAL_STOP_REASON_NONE);
}
} }
/** Signals all downloads to stop. Call {@link #startDownloads()} to let them to be started. */ /** Signals all downloads to stop. Call {@link #startDownloads()} to let them to be started. */
...@@ -273,10 +303,7 @@ public final class DownloadManager { ...@@ -273,10 +303,7 @@ public final class DownloadManager {
public void stopDownloads(int manualStopReason) { public void stopDownloads(int manualStopReason) {
Assertions.checkArgument(manualStopReason != MANUAL_STOP_REASON_NONE); Assertions.checkArgument(manualStopReason != MANUAL_STOP_REASON_NONE);
logd("downloads are stopped manually"); logd("downloads are stopped manually");
this.manualStopReason = manualStopReason; setManualStopReason(/* id= */ null, manualStopReason);
for (int i = 0; i < downloads.size(); i++) {
downloads.get(i).setManualStopReason(this.manualStopReason);
}
} }
/** /**
...@@ -286,11 +313,7 @@ public final class DownloadManager { ...@@ -286,11 +313,7 @@ public final class DownloadManager {
* @param id The unique content id of the download to be started. * @param id The unique content id of the download to be started.
*/ */
public void startDownload(String id) { public void startDownload(String id) {
Download download = getDownload(id); setManualStopReason(id, MANUAL_STOP_REASON_NONE);
if (download != null) {
logd("manual stop is cancelled", download);
download.setManualStopReason(MANUAL_STOP_REASON_NONE);
}
} }
/** /**
...@@ -315,11 +338,7 @@ public final class DownloadManager { ...@@ -315,11 +338,7 @@ public final class DownloadManager {
*/ */
public void stopDownload(String id, int manualStopReason) { public void stopDownload(String id, int manualStopReason) {
Assertions.checkArgument(manualStopReason != MANUAL_STOP_REASON_NONE); Assertions.checkArgument(manualStopReason != MANUAL_STOP_REASON_NONE);
Download download = getDownload(id); setManualStopReason(id, manualStopReason);
if (download != null) {
logd("download is stopped manually", download);
download.setManualStopReason(manualStopReason);
}
} }
/** /**
...@@ -329,11 +348,9 @@ public final class DownloadManager { ...@@ -329,11 +348,9 @@ public final class DownloadManager {
*/ */
public void handleAction(DownloadAction action) { public void handleAction(DownloadAction action) {
Assertions.checkState(!released); Assertions.checkState(!released);
actionQueue.add(action);
if (initialized) { if (initialized) {
addDownloadForAction(action); processActionQueue();
saveActions();
} else {
actionQueue.add(action);
} }
} }
...@@ -343,25 +360,12 @@ public final class DownloadManager { ...@@ -343,25 +360,12 @@ public final class DownloadManager {
return downloads.size(); return downloads.size();
} }
/**
* Returns {@link DownloadState} for the given content id, or null if no such download exists.
*
* @param id The unique content id.
* @return DownloadState for the given content id, or null if no such download exists.
*/
@Nullable
public DownloadState getDownloadState(String id) {
Assertions.checkState(!released);
Download download = getDownload(id);
return download != null ? download.getDownloadState() : null;
}
/** Returns the states of all current downloads. */ /** Returns the states of all current downloads. */
public DownloadState[] getAllDownloadStates() { public DownloadState[] getAllDownloadStates() {
Assertions.checkState(!released); Assertions.checkState(!released);
DownloadState[] states = new DownloadState[downloads.size()]; DownloadState[] states = new DownloadState[downloads.size()];
for (int i = 0; i < states.length; i++) { for (int i = 0; i < states.length; i++) {
states[i] = downloads.get(i).getDownloadState(); states[i] = downloads.get(i).getUpdatedDownloadState();
} }
return states; return states;
} }
...@@ -375,7 +379,7 @@ public final class DownloadManager { ...@@ -375,7 +379,7 @@ public final class DownloadManager {
/** Returns whether there are no active downloads. */ /** Returns whether there are no active downloads. */
public boolean isIdle() { public boolean isIdle() {
Assertions.checkState(!released); Assertions.checkState(!released);
return initialized && activeDownloads.isEmpty(); return initialized && activeDownloads.isEmpty() && actionQueue.isEmpty() && !loadingDownload;
} }
/** /**
...@@ -399,17 +403,32 @@ public final class DownloadManager { ...@@ -399,17 +403,32 @@ public final class DownloadManager {
logd("Released"); logd("Released");
} }
private void addDownloadForAction(DownloadAction action) { private void setManualStopReason(@Nullable String id, int manualStopReason) {
for (int i = 0; i < downloads.size(); i++) { if (id != null) {
Download download = downloads.get(i); Download download = getDownload(id);
if (download.addAction(action)) { if (download != null) {
logd("Action is added to existing download", download); logd("download manual stop reason is set to : " + manualStopReason, download);
download.setManualStopReason(manualStopReason);
return; return;
} }
} else {
this.manualStopReason = manualStopReason;
for (int i = 0; i < downloads.size(); i++) {
downloads.get(i).setManualStopReason(manualStopReason);
}
} }
Download download = new Download(this, action, notMetRequirements, manualStopReason); fileIOHandler.post(
downloads.add(download); () -> {
logd("Download is added", download); try {
if (id != null) {
downloadIndex.setManualStopReason(id, manualStopReason);
} else {
downloadIndex.setManualStopReason(manualStopReason);
}
} catch (DatabaseIOException e) {
Log.e(TAG, "setManualStopReason failed", e);
}
});
} }
private void maybeNotifyListenersIdle() { private void maybeNotifyListenersIdle() {
...@@ -422,33 +441,44 @@ public final class DownloadManager { ...@@ -422,33 +441,44 @@ public final class DownloadManager {
} }
} }
private void onDownloadStateChange(Download download) { private void onDownloadStateChange(Download download, DownloadState downloadState) {
if (released) { if (released) {
return; return;
} }
logd("Download state is changed", download); logd("Download state is changed", download);
DownloadState downloadState = download.getDownloadState(); updateDownloadIndex(downloadState);
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onDownloadStateChanged(this, downloadState); listener.onDownloadStateChanged(this, downloadState);
} }
if (download.isFinished()) { if (download.isFinished()) {
downloads.remove(download); downloads.remove(download);
saveActions();
} }
} }
private void onRequirementsStateChanged(@Requirements.RequirementFlags int notMetRequirements) { private void onRequirementsStateChanged(@Requirements.RequirementFlags int notMetRequirements) {
this.notMetRequirements = notMetRequirements;
logdFlags("Not met requirements are changed", notMetRequirements);
Requirements requirements = requirementsWatcher.getRequirements(); Requirements requirements = requirementsWatcher.getRequirements();
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onRequirementsStateChanged(DownloadManager.this, requirements, notMetRequirements); listener.onRequirementsStateChanged(DownloadManager.this, requirements, notMetRequirements);
} }
setNotMetRequirements(notMetRequirements);
}
private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) {
this.notMetRequirements = notMetRequirements;
logdFlags("Not met requirements are changed", notMetRequirements);
for (int i = 0; i < downloads.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
downloads.get(i).setNotMetRequirements(notMetRequirements); downloads.get(i).setNotMetRequirements(notMetRequirements);
} }
} }
@Requirements.RequirementFlags
private int watchRequirements(Requirements requirements) {
RequirementsWatcher.Listener listener =
(requirementsWatcher, notMetRequirements) -> onRequirementsStateChanged(notMetRequirements);
requirementsWatcher = new RequirementsWatcher(context, listener, requirements);
return requirementsWatcher.start();
}
@Nullable @Nullable
private Download getDownload(String id) { private Download getDownload(String id) {
for (int i = 0; i < downloads.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
...@@ -460,34 +490,85 @@ public final class DownloadManager { ...@@ -460,34 +490,85 @@ public final class DownloadManager {
return null; return null;
} }
private void loadActions() { private void processActionQueue() {
if (loadingDownload || actionQueue.isEmpty()) {
return;
}
DownloadAction action = actionQueue.remove();
Download download = getDownload(action.id);
if (download != null) {
download.addAction(action);
logd("Action is added to existing download", download);
return;
}
loadDownload(action);
}
private void loadDownload(DownloadAction action) {
loadingDownload = true;
fileIOHandler.post( fileIOHandler.post(
() -> { () -> {
DownloadAction[] loadedActions; DownloadState downloadState = null;
try { try {
loadedActions = actionFile.load(); downloadState = downloadIndex.getDownloadState(action.id);
logd("Action file is loaded."); } catch (DatabaseIOException e) {
} catch (Throwable e) { Log.e(TAG, "loadDownload failed", e);
Log.e(TAG, "Action file loading failed.", e);
loadedActions = new DownloadAction[0];
} }
final DownloadAction[] actions = loadedActions; DownloadState finalDownloadState = downloadState;
handler.post( handler.post(
() -> { () -> {
loadingDownload = false;
if (released) { if (released) {
return; return;
} }
for (DownloadAction action : actions) { DownloadState state;
addDownloadForAction(action); if (finalDownloadState == null) {
state = new DownloadState(action);
logd("Download state is created for " + action.id);
} else {
state = finalDownloadState.mergeAction(action);
logd("Download state is loaded for " + action.id);
}
addDownloadForState(state);
processActionQueue();
});
});
}
private void loadDownloads() {
fileIOHandler.post(
() -> {
DownloadState[] loadedStates;
try {
DownloadStateCursor cursor =
downloadIndex.getDownloadStates(
STATE_QUEUED,
STATE_STOPPED,
STATE_DOWNLOADING,
STATE_REMOVING,
STATE_RESTARTING);
loadedStates = new DownloadState[cursor.getCount()];
for (int i = 0, length = loadedStates.length; i < length; i++) {
cursor.moveToNext();
loadedStates[i] = cursor.getDownloadState();
}
logd("Download states are loaded.");
} catch (Throwable e) {
Log.e(TAG, "Download state loading failed.", e);
loadedStates = new DownloadState[0];
}
final DownloadState[] states = loadedStates;
handler.post(
() -> {
if (released) {
return;
} }
if (!actionQueue.isEmpty()) { for (DownloadState downloadState : states) {
while (!actionQueue.isEmpty()) { addDownloadForState(downloadState);
addDownloadForAction(actionQueue.remove());
}
saveActions();
} }
logd("Downloads are created."); logd("Downloads are created.");
initialized = true; initialized = true;
processActionQueue();
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onInitialized(DownloadManager.this); listener.onInitialized(DownloadManager.this);
} }
...@@ -498,22 +579,26 @@ public final class DownloadManager { ...@@ -498,22 +579,26 @@ public final class DownloadManager {
}); });
} }
private void saveActions() { private void addDownloadForState(DownloadState downloadState) {
if (released) { Download download = new Download(this, downloadState, notMetRequirements, manualStopReason);
return; downloads.add(download);
} logd("Download is added", download);
ArrayList<DownloadAction> actions = new ArrayList<>(downloads.size()); }
for (int i = 0; i < downloads.size(); i++) {
downloads.get(i).addActions(actions); private void updateDownloadIndex(DownloadState downloadState) {
}
final DownloadAction[] actionsArray = actions.toArray(new DownloadAction[0]);
fileIOHandler.post( fileIOHandler.post(
() -> { () -> {
if (released) {
return;
}
try { try {
actionFile.store(actionsArray); if (downloadState.state == DownloadState.STATE_REMOVED) {
logd("Actions persisted."); downloadIndex.removeDownloadState(downloadState.id);
} catch (IOException e) { } else {
Log.e(TAG, "Persisting actions failed.", e); downloadIndex.putDownloadState(downloadState);
}
} catch (DatabaseIOException e) {
Log.e(TAG, "updateDownloadIndex failed", e);
} }
}); });
} }
...@@ -536,20 +621,6 @@ public final class DownloadManager { ...@@ -536,20 +621,6 @@ public final class DownloadManager {
} }
} }
@Requirements.RequirementFlags
private int watchRequirements(Requirements requirements) {
RequirementsWatcher.Listener listener =
(requirementsWatcher, notMetRequirements) -> onRequirementsStateChanged(notMetRequirements);
requirementsWatcher = new RequirementsWatcher(context, listener, requirements);
@Requirements.RequirementFlags int notMetRequirements = requirementsWatcher.start();
if (notMetRequirements == 0) {
startDownloads();
} else {
stopDownloads();
}
return notMetRequirements;
}
@StartThreadResults @StartThreadResults
private int startDownloadThread(Download download, DownloadAction action) { private int startDownloadThread(Download download, DownloadAction action) {
if (!initialized || released) { if (!initialized || released) {
...@@ -634,13 +705,13 @@ public final class DownloadManager { ...@@ -634,13 +705,13 @@ public final class DownloadManager {
private Download( private Download(
DownloadManager downloadManager, DownloadManager downloadManager,
DownloadAction action, DownloadState downloadState,
@Requirements.RequirementFlags int notMetRequirements, @Requirements.RequirementFlags int notMetRequirements,
int manualStopReason) { int manualStopReason) {
this.downloadManager = downloadManager; this.downloadManager = downloadManager;
this.downloadState = downloadState;
this.notMetRequirements = notMetRequirements; this.notMetRequirements = notMetRequirements;
this.manualStopReason = manualStopReason; this.manualStopReason = manualStopReason;
downloadState = new DownloadState(action);
initialize(downloadState.state); initialize(downloadState.state);
} }
...@@ -649,17 +720,17 @@ public final class DownloadManager { ...@@ -649,17 +720,17 @@ public final class DownloadManager {
return downloadState.id; return downloadState.id;
} }
public boolean addAction(DownloadAction newAction) { public void addAction(DownloadAction newAction) {
if (!getId().equals(newAction.id)) { Assertions.checkArgument(getId().equals(newAction.id));
return false; if (!downloadState.type.equals(newAction.type)) {
String format = "Action type (%s) doesn't match existing download type (%s)";
Log.e(TAG, String.format(format, newAction.type, downloadState.type));
} }
Assertions.checkState(downloadState.type.equals(newAction.type));
downloadState = downloadState.mergeAction(newAction); downloadState = downloadState.mergeAction(newAction);
initialize(downloadState.state); initialize(downloadState.state);
return true;
} }
public DownloadState getDownloadState() { public DownloadState getUpdatedDownloadState() {
float downloadPercentage = C.PERCENTAGE_UNSET; float downloadPercentage = C.PERCENTAGE_UNSET;
long downloadedBytes = 0; long downloadedBytes = 0;
long totalBytes = C.LENGTH_UNSET; long totalBytes = C.LENGTH_UNSET;
...@@ -721,6 +792,7 @@ public final class DownloadManager { ...@@ -721,6 +792,7 @@ public final class DownloadManager {
} }
private void updateStopState() { private void updateStopState() {
DownloadState oldDownloadState = downloadState;
if (canStart()) { if (canStart()) {
if (state == STATE_STOPPED) { if (state == STATE_STOPPED) {
startOrQueue(); startOrQueue();
...@@ -731,6 +803,9 @@ public final class DownloadManager { ...@@ -731,6 +803,9 @@ public final class DownloadManager {
setState(STATE_STOPPED); setState(STATE_STOPPED);
} }
} }
if (oldDownloadState == downloadState) {
downloadManager.onDownloadStateChange(this, getUpdatedDownloadState());
}
} }
private void initialize(int initialState) { private void initialize(int initialState) {
...@@ -745,7 +820,7 @@ public final class DownloadManager { ...@@ -745,7 +820,7 @@ public final class DownloadManager {
setState(STATE_STOPPED); setState(STATE_STOPPED);
} }
if (state == initialState) { if (state == initialState) {
downloadManager.onDownloadStateChange(this); downloadManager.onDownloadStateChange(this, getUpdatedDownloadState());
} }
} }
...@@ -770,20 +845,18 @@ public final class DownloadManager { ...@@ -770,20 +845,18 @@ public final class DownloadManager {
return DownloadAction.createRemoveAction( return DownloadAction.createRemoveAction(
downloadState.type, downloadState.uri, downloadState.cacheKey); downloadState.type, downloadState.uri, downloadState.cacheKey);
} }
return getDownloadAction(downloadState); return DownloadAction.createDownloadAction(
} downloadState.type,
downloadState.uri,
private void addActions(List<DownloadAction> actions) { Arrays.asList(downloadState.streamKeys),
actions.add(getAction()); downloadState.cacheKey,
if (state == STATE_RESTARTING) { downloadState.customMetadata);
actions.add(getDownloadAction(downloadState));
}
} }
private void setState(@DownloadState.State int newState) { private void setState(@DownloadState.State int newState) {
if (state != newState) { if (state != newState) {
state = newState; state = newState;
downloadManager.onDownloadStateChange(this); downloadManager.onDownloadStateChange(this, getUpdatedDownloadState());
} }
} }
...@@ -808,14 +881,6 @@ public final class DownloadManager { ...@@ -808,14 +881,6 @@ public final class DownloadManager {
} }
} }
private static DownloadAction getDownloadAction(DownloadState downloadState) {
return DownloadAction.createDownloadAction(
downloadState.type,
downloadState.uri,
Arrays.asList(downloadState.streamKeys),
downloadState.cacheKey,
downloadState.customMetadata);
}
} }
private class DownloadThread extends Thread { private class DownloadThread extends Thread {
......
...@@ -25,10 +25,10 @@ import com.google.android.exoplayer2.C; ...@@ -25,10 +25,10 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloadState.State; import com.google.android.exoplayer2.offline.DownloadState.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.RobolectricUtil; import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
...@@ -64,7 +64,7 @@ public class DownloadManagerTest { ...@@ -64,7 +64,7 @@ public class DownloadManagerTest {
private Uri uri2; private Uri uri2;
private Uri uri3; private Uri uri3;
private DummyMainThread dummyMainThread; private DummyMainThread dummyMainThread;
private File actionFile; private DefaultDownloadIndex downloadIndex;
private TestDownloadManagerListener downloadManagerListener; private TestDownloadManagerListener downloadManagerListener;
private FakeDownloaderFactory downloaderFactory; private FakeDownloaderFactory downloaderFactory;
private DownloadManager downloadManager; private DownloadManager downloadManager;
...@@ -77,7 +77,7 @@ public class DownloadManagerTest { ...@@ -77,7 +77,7 @@ public class DownloadManagerTest {
uri2 = Uri.parse("http://abc.com/media2"); uri2 = Uri.parse("http://abc.com/media2");
uri3 = Uri.parse("http://abc.com/media3"); uri3 = Uri.parse("http://abc.com/media3");
dummyMainThread = new DummyMainThread(); dummyMainThread = new DummyMainThread();
actionFile = Util.createTempFile(ApplicationProvider.getApplicationContext(), "ExoPlayerTest"); downloadIndex = new DefaultDownloadIndex(TestUtil.getTestDatabaseProvider());
downloaderFactory = new FakeDownloaderFactory(); downloaderFactory = new FakeDownloaderFactory();
setUpDownloadManager(100); setUpDownloadManager(100);
} }
...@@ -85,7 +85,6 @@ public class DownloadManagerTest { ...@@ -85,7 +85,6 @@ public class DownloadManagerTest {
@After @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
releaseDownloadManager(); releaseDownloadManager();
actionFile.delete();
dummyMainThread.release(); dummyMainThread.release();
} }
...@@ -359,6 +358,7 @@ public class DownloadManagerTest { ...@@ -359,6 +358,7 @@ public class DownloadManagerTest {
TaskWrapper task2 = new DownloadRunner(uri2).postDownloadAction().getTask(); TaskWrapper task2 = new DownloadRunner(uri2).postDownloadAction().getTask();
TaskWrapper task3 = new DownloadRunner(uri3).postRemoveAction().getTask(); TaskWrapper task3 = new DownloadRunner(uri3).postRemoveAction().getTask();
task3.assertRemoving();
DownloadState[] states = downloadManager.getAllDownloadStates(); DownloadState[] states = downloadManager.getAllDownloadStates();
assertThat(states).hasLength(3); assertThat(states).hasLength(3);
...@@ -471,7 +471,7 @@ public class DownloadManagerTest { ...@@ -471,7 +471,7 @@ public class DownloadManagerTest {
downloadManager = downloadManager =
new DownloadManager( new DownloadManager(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),
actionFile, downloadIndex,
downloaderFactory, downloaderFactory,
maxActiveDownloadTasks, maxActiveDownloadTasks,
MIN_RETRY_COUNT, MIN_RETRY_COUNT,
...@@ -494,8 +494,8 @@ public class DownloadManagerTest { ...@@ -494,8 +494,8 @@ public class DownloadManagerTest {
} }
} }
private void runOnMainThread(final Runnable r) { private void runOnMainThread(final TestRunnable r) {
dummyMainThread.runOnMainThread(r); dummyMainThread.runTestOnMainThread(r);
} }
private final class DownloadRunner { private final class DownloadRunner {
......
...@@ -27,6 +27,7 @@ import android.os.ConditionVariable; ...@@ -27,6 +27,7 @@ import android.os.ConditionVariable;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
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.offline.DefaultDownloadIndex;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
...@@ -69,7 +70,7 @@ public class DownloadManagerDashTest { ...@@ -69,7 +70,7 @@ public class DownloadManagerDashTest {
private StreamKey fakeStreamKey1; private StreamKey fakeStreamKey1;
private StreamKey fakeStreamKey2; private StreamKey fakeStreamKey2;
private TestDownloadManagerListener downloadManagerListener; private TestDownloadManagerListener downloadManagerListener;
private File actionFile; private DefaultDownloadIndex downloadIndex;
private DummyMainThread dummyMainThread; private DummyMainThread dummyMainThread;
@Before @Before
...@@ -95,7 +96,7 @@ public class DownloadManagerDashTest { ...@@ -95,7 +96,7 @@ public class DownloadManagerDashTest {
fakeStreamKey1 = new StreamKey(0, 0, 0); fakeStreamKey1 = new StreamKey(0, 0, 0);
fakeStreamKey2 = new StreamKey(0, 1, 0); fakeStreamKey2 = new StreamKey(0, 1, 0);
actionFile = new File(tempFolder, "actionFile"); downloadIndex = new DefaultDownloadIndex(TestUtil.getTestDatabaseProvider());
createDownloadManager(); createDownloadManager();
} }
...@@ -136,8 +137,6 @@ public class DownloadManagerDashTest { ...@@ -136,8 +137,6 @@ public class DownloadManagerDashTest {
downloadManager.release(); downloadManager.release();
}); });
assertThat(actionFile.exists()).isTrue();
assertThat(actionFile.length()).isGreaterThan(0L);
assertCacheEmpty(cache); assertCacheEmpty(cache);
// Revert fakeDataSet to normal. // Revert fakeDataSet to normal.
...@@ -239,13 +238,13 @@ public class DownloadManagerDashTest { ...@@ -239,13 +238,13 @@ public class DownloadManagerDashTest {
} }
private void createDownloadManager() { private void createDownloadManager() {
dummyMainThread.runOnMainThread( dummyMainThread.runTestOnMainThread(
() -> { () -> {
Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet); Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet);
downloadManager = downloadManager =
new DownloadManager( new DownloadManager(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),
actionFile, downloadIndex,
new DefaultDownloaderFactory( new DefaultDownloaderFactory(
new DownloaderConstructorHelper(cache, fakeDataSourceFactory)), new DownloaderConstructorHelper(cache, fakeDataSourceFactory)),
/* maxSimultaneousDownloads= */ 1, /* maxSimultaneousDownloads= */ 1,
......
...@@ -26,6 +26,7 @@ import android.net.Uri; ...@@ -26,6 +26,7 @@ import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
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.offline.DefaultDownloadIndex;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
...@@ -107,19 +108,14 @@ public class DownloadServiceDashTest { ...@@ -107,19 +108,14 @@ public class DownloadServiceDashTest {
fakeStreamKey1 = new StreamKey(0, 0, 0); fakeStreamKey1 = new StreamKey(0, 0, 0);
fakeStreamKey2 = new StreamKey(0, 1, 0); fakeStreamKey2 = new StreamKey(0, 1, 0);
dummyMainThread.runOnMainThread( dummyMainThread.runTestOnMainThread(
() -> { () -> {
File actionFile; DefaultDownloadIndex downloadIndex =
try { new DefaultDownloadIndex(TestUtil.getTestDatabaseProvider());
actionFile = Util.createTempFile(context, "ExoPlayerTest");
} catch (IOException e) {
throw new RuntimeException(e);
}
actionFile.delete();
final DownloadManager dashDownloadManager = final DownloadManager dashDownloadManager =
new DownloadManager( new DownloadManager(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),
actionFile, downloadIndex,
new DefaultDownloaderFactory( new DefaultDownloaderFactory(
new DownloaderConstructorHelper(cache, fakeDataSourceFactory)), new DownloaderConstructorHelper(cache, fakeDataSourceFactory)),
/* maxSimultaneousDownloads= */ 1, /* maxSimultaneousDownloads= */ 1,
......
...@@ -27,6 +27,11 @@ import java.util.concurrent.atomic.AtomicReference; ...@@ -27,6 +27,11 @@ import java.util.concurrent.atomic.AtomicReference;
/** Helper class to simulate main/UI thread in tests. */ /** Helper class to simulate main/UI thread in tests. */
public final class DummyMainThread { public final class DummyMainThread {
/** {@link Runnable} variant which can throw a checked exception. */
public interface TestRunnable {
void run() throws Exception;
}
/** Default timeout value used for {@link #runOnMainThread(Runnable)}. */ /** Default timeout value used for {@link #runOnMainThread(Runnable)}. */
public static final int TIMEOUT_MS = 10000; public static final int TIMEOUT_MS = 10000;
...@@ -57,8 +62,33 @@ public final class DummyMainThread { ...@@ -57,8 +62,33 @@ public final class DummyMainThread {
* @param runnable The {@link Runnable} to run. * @param runnable The {@link Runnable} to run.
*/ */
public void runOnMainThread(int timeoutMs, final Runnable runnable) { public void runOnMainThread(int timeoutMs, final Runnable runnable) {
runTestOnMainThread(timeoutMs, runnable::run);
}
/**
* Runs the provided {@link TestRunnable} on the main thread, blocking until execution completes
* or until {@link #TIMEOUT_MS} milliseconds have passed.
*
* @param runnable The {@link TestRunnable} to run.
*/
public void runTestOnMainThread(final TestRunnable runnable) {
runTestOnMainThread(TIMEOUT_MS, runnable);
}
/**
* Runs the provided {@link TestRunnable} on the main thread, blocking until execution completes
* or until timeout milliseconds have passed.
*
* @param timeoutMs The maximum time to wait in milliseconds.
* @param runnable The {@link TestRunnable} to run.
*/
public void runTestOnMainThread(int timeoutMs, final TestRunnable runnable) {
if (Looper.myLooper() == handler.getLooper()) { if (Looper.myLooper() == handler.getLooper()) {
runnable.run(); try {
runnable.run();
} catch (Exception e) {
Util.sneakyThrow(e);
}
} else { } else {
ConditionVariable finishedCondition = new ConditionVariable(); ConditionVariable finishedCondition = new ConditionVariable();
AtomicReference<Throwable> thrown = new AtomicReference<>(); AtomicReference<Throwable> thrown = new AtomicReference<>();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment