Commit a588717b by olly Committed by Oliver Woodman

Move download state transitions into DownloadManager

Non-trivial download state transitions are currently split across
DownloadManager and Download. These transitions are part of the
same state machine, so it's clearer if they're all in the same place
(i.e. DownloadManager, since this is the component that transitions
downloads between states).

PiperOrigin-RevId: 243249915
parent f623a9de
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.offline;
import static com.google.android.exoplayer2.offline.Download.STATE_QUEUED;
import androidx.annotation.Nullable;
import java.io.File;
import java.io.IOException;
......@@ -88,9 +90,17 @@ public final class ActionFileUpgradeUtil {
throws IOException {
Download download = downloadIndex.getDownload(action.id);
if (download != null) {
download = download.copyWithMergedAction(action, /* canStart= */ true);
download = DownloadManager.mergeAction(download, action, download.manualStopReason);
} else {
download = new Download(action);
long nowMs = System.currentTimeMillis();
download =
new Download(
action,
STATE_QUEUED,
Download.FAILURE_REASON_NONE,
Download.MANUAL_STOP_REASON_NONE,
/* startTimeMs= */ nowMs,
/* updateTimeMs= */ nowMs);
}
downloadIndex.putDownload(download);
}
......
......@@ -111,25 +111,22 @@ public final class Download {
/** The reason the download is manually stopped, or {@link #MANUAL_STOP_REASON_NONE}. */
public final int manualStopReason;
/*package*/ CachingCounters counters;
/* package */ CachingCounters counters;
/**
* Creates a {@link Download} using a {@link DownloadAction}.
*
* @param action The {@link DownloadAction}.
*/
public Download(DownloadAction action) {
this(action, System.currentTimeMillis());
}
private Download(DownloadAction action, long currentTimeMs) {
/* package */ Download(
DownloadAction action,
@State int state,
@FailureReason int failureReason,
int manualStopReason,
long startTimeMs,
long updateTimeMs) {
this(
action,
/* state= */ STATE_QUEUED,
FAILURE_REASON_NONE,
/* manualStopReason= */ 0,
/* startTimeMs= */ currentTimeMs,
/* updateTimeMs= */ currentTimeMs,
state,
failureReason,
manualStopReason,
startTimeMs,
updateTimeMs,
new CachingCounters());
}
......@@ -155,39 +152,9 @@ public final class Download {
this.counters = counters;
}
/**
* Merges the given {@link DownloadAction} and creates a new {@link Download}. The action must
* have the same id and type.
*
* @param newAction The {@link DownloadAction} to be merged.
* @param canStart Whether the download is eligible to be started.
* @return A new {@link Download}.
*/
public Download copyWithMergedAction(DownloadAction newAction, boolean canStart) {
return new Download(
action.copyWithMergedAction(newAction),
getNextState(state, canStart && manualStopReason == 0),
FAILURE_REASON_NONE,
manualStopReason,
startTimeMs,
/* updateTimeMs= */ System.currentTimeMillis(),
counters);
}
/**
* Returns a copy with the specified state, clearing {@link #failureReason}.
*
* @param state The {@link State}.
*/
public Download copyWithState(@State int state) {
return new Download(
action,
state,
FAILURE_REASON_NONE,
manualStopReason,
startTimeMs,
/* updateTimeMs= */ System.currentTimeMillis(),
counters);
/** Returns whether the download is completed or failed. These are terminal states. */
public boolean isTerminalState() {
return state == STATE_COMPLETED || state == STATE_FAILED;
}
/** Returns the total number of downloaded bytes. */
......@@ -217,14 +184,4 @@ public final class Download {
Assertions.checkNotNull(counters);
this.counters = counters;
}
private static int getNextState(@State int currentState, boolean canStart) {
if (currentState == STATE_REMOVING || currentState == STATE_RESTARTING) {
return STATE_RESTARTING;
} else if (canStart) {
return STATE_QUEUED;
} else {
return STATE_STOPPED;
}
}
}
......@@ -66,6 +66,7 @@ public final class DownloadManager {
/** Listener for {@link DownloadManager} events. */
public interface Listener {
/**
* Called when all actions have been restored.
*
......@@ -485,7 +486,7 @@ public final class DownloadManager {
private void onDownloadChanged(Download download) {
int downloadIndex = getDownloadIndex(download.action.id);
if (download.state == STATE_COMPLETED || download.state == STATE_FAILED) {
if (download.isTerminalState()) {
if (downloadIndex != C.INDEX_UNSET) {
downloads.remove(downloadIndex);
}
......@@ -643,19 +644,26 @@ public final class DownloadManager {
}
}
// TODO: Use manualStopReason.
private void addDownloadInternal(DownloadAction action, int manualStopReason) {
DownloadInternal downloadInternal = getDownload(action.id);
if (downloadInternal != null) {
downloadInternal.addAction(action);
downloadInternal.addAction(action, manualStopReason);
logd("Action is added to existing download", downloadInternal);
} else {
Download download = loadDownload(action.id);
if (download == null) {
download = new Download(action);
long nowMs = System.currentTimeMillis();
download =
new Download(
action,
manualStopReason != Download.MANUAL_STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED,
Download.FAILURE_REASON_NONE,
manualStopReason,
/* startTimeMs= */ nowMs,
/* updateTimeMs= */ nowMs);
logd("Download state is created for " + action.id);
} else {
download = download.copyWithMergedAction(action, /* canStart= */ canStartDownloads());
download = mergeAction(download, action, manualStopReason);
logd("Download state is loaded for " + action.id);
}
addDownloadForState(download);
......@@ -669,7 +677,7 @@ public final class DownloadManager {
} else {
Download download = loadDownload(id);
if (download != null) {
addDownloadForState(download.copyWithState(STATE_REMOVING));
addDownloadForState(copyWithState(download, STATE_REMOVING));
} else {
logd("Can't remove download. No download with id: " + id);
}
......@@ -798,6 +806,39 @@ public final class DownloadManager {
return downloadsStarted && notMetRequirements == 0;
}
/* package */ static Download mergeAction(
Download download, DownloadAction action, int manualStopReason) {
@Download.State int state = download.state;
if (state == STATE_REMOVING || state == STATE_RESTARTING) {
state = STATE_RESTARTING;
} else if (manualStopReason != MANUAL_STOP_REASON_NONE) {
state = STATE_STOPPED;
} else {
state = STATE_QUEUED;
}
long nowMs = System.currentTimeMillis();
long startTimeMs = download.isTerminalState() ? nowMs : download.startTimeMs;
return new Download(
download.action.copyWithMergedAction(action),
state,
FAILURE_REASON_NONE,
manualStopReason,
startTimeMs,
/* updateTimeMs= */ nowMs,
download.counters);
}
private static Download copyWithState(Download download, @Download.State int state) {
return new Download(
download.action,
state,
FAILURE_REASON_NONE,
download.manualStopReason,
download.startTimeMs,
/* updateTimeMs= */ System.currentTimeMillis(),
download.counters);
}
private static void logd(String message) {
if (DEBUG) {
Log.d(TAG, message);
......@@ -841,8 +882,8 @@ public final class DownloadManager {
initialize(download.state);
}
public void addAction(DownloadAction newAction) {
download = download.copyWithMergedAction(newAction, downloadManager.canStartDownloads());
public void addAction(DownloadAction newAction, int manualStopReason) {
download = mergeAction(download, newAction, manualStopReason);
initialize();
}
......
......@@ -56,7 +56,46 @@ public class ActionFileUpgradeUtilTest {
}
@Test
public void addAction_nonExistingDownload_createsNewDownload() throws IOException {
public void upgradeAndDelete_createsDownloads() throws IOException {
// Copy the test asset to a file.
byte[] actionFileBytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(),
"offline/action_file_for_download_index_upgrade.exi");
try (FileOutputStream output = new FileOutputStream(tempFile)) {
output.write(actionFileBytes);
}
StreamKey expectedStreamKey1 =
new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5);
StreamKey expectedStreamKey2 =
new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2);
DownloadAction expectedAction1 =
new DownloadAction(
"key123",
TYPE_DASH,
Uri.parse("https://www.test.com/download1"),
asList(expectedStreamKey1),
/* customCacheKey= */ "key123",
new byte[] {1, 2, 3, 4});
DownloadAction expectedAction2 =
new DownloadAction(
"key234",
TYPE_DASH,
Uri.parse("https://www.test.com/download2"),
asList(expectedStreamKey2),
/* customCacheKey= */ "key234",
new byte[] {5, 4, 3, 2, 1});
ActionFileUpgradeUtil.upgradeAndDelete(
tempFile, /* downloadIdProvider= */ null, downloadIndex, /* deleteOnFailure= */ true);
assertDownloadIndexContainsAction(expectedAction1, Download.STATE_QUEUED);
assertDownloadIndexContainsAction(expectedAction2, Download.STATE_QUEUED);
}
@Test
public void mergeAction_nonExistingDownload_createsNewDownload() throws IOException {
byte[] data = new byte[] {1, 2, 3, 4};
DownloadAction action =
new DownloadAction(
......@@ -75,7 +114,7 @@ public class ActionFileUpgradeUtilTest {
}
@Test
public void addAction_existingDownload_createsMergedDownload() throws IOException {
public void mergeAction_existingDownload_createsMergedDownload() throws IOException {
StreamKey streamKey1 =
new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5);
StreamKey streamKey2 =
......@@ -97,7 +136,6 @@ public class ActionFileUpgradeUtilTest {
/* customCacheKey= */ "key123",
new byte[] {5, 4, 3, 2, 1});
ActionFileUpgradeUtil.mergeAction(action1, downloadIndex);
ActionFileUpgradeUtil.mergeAction(action2, downloadIndex);
Download download = downloadIndex.getDownload(action2.id);
......@@ -110,44 +148,6 @@ public class ActionFileUpgradeUtilTest {
assertThat(download.state).isEqualTo(Download.STATE_QUEUED);
}
@Test
public void upgradeActionFile_createsDownloads() throws IOException {
// Copy the test asset to a file.
byte[] actionFileBytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(),
"offline/action_file_for_download_index_upgrade.exi");
try (FileOutputStream output = new FileOutputStream(tempFile)) {
output.write(actionFileBytes);
}
StreamKey expectedStreamKey1 =
new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5);
StreamKey expectedStreamKey2 =
new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2);
DownloadAction expectedAction1 =
new DownloadAction(
"key123",
TYPE_DASH,
Uri.parse("https://www.test.com/download1"),
asList(expectedStreamKey1),
/* customCacheKey= */ "key123",
new byte[] {1, 2, 3, 4});
DownloadAction expectedAction2 =
new DownloadAction(
"key234",
TYPE_DASH,
Uri.parse("https://www.test.com/download2"),
asList(expectedStreamKey2),
/* customCacheKey= */ "key234",
new byte[] {5, 4, 3, 2, 1});
ActionFileUpgradeUtil.upgradeAndDelete(
tempFile, /* downloadIdProvider= */ null, downloadIndex, /* deleteOnFailure= */ true);
assertDownloadIndexContainsAction(expectedAction1, Download.STATE_QUEUED);
assertDownloadIndexContainsAction(expectedAction2, Download.STATE_QUEUED);
}
private void assertDownloadIndexContainsAction(DownloadAction action, int state)
throws IOException {
Download download = downloadIndex.getDownload(action.id);
......
......@@ -60,7 +60,7 @@ public class DefaultDownloadIndexTest {
downloadIndex.putDownload(download);
Download readDownload = downloadIndex.getDownload(id);
DownloadTest.assertEqual(readDownload, download);
assertEqual(readDownload, download);
}
@Test
......@@ -91,7 +91,7 @@ public class DefaultDownloadIndexTest {
Download readDownload = downloadIndex.getDownload(id);
assertThat(readDownload).isNotNull();
DownloadTest.assertEqual(readDownload, download);
assertEqual(readDownload, download);
}
@Test
......@@ -103,7 +103,7 @@ public class DefaultDownloadIndexTest {
downloadIndex = new DefaultDownloadIndex(databaseProvider);
Download readDownload = downloadIndex.getDownload(id);
assertThat(readDownload).isNotNull();
DownloadTest.assertEqual(readDownload, download);
assertEqual(readDownload, download);
}
@Test
......@@ -138,9 +138,9 @@ public class DefaultDownloadIndexTest {
try (DownloadCursor cursor = downloadIndex.getDownloads()) {
assertThat(cursor.getCount()).isEqualTo(2);
cursor.moveToNext();
DownloadTest.assertEqual(cursor.getDownload(), download2);
assertEqual(cursor.getDownload(), download2);
cursor.moveToNext();
DownloadTest.assertEqual(cursor.getDownload(), download1);
assertEqual(cursor.getDownload(), download1);
}
}
......@@ -161,9 +161,9 @@ public class DefaultDownloadIndexTest {
downloadIndex.getDownloads(Download.STATE_REMOVING, Download.STATE_COMPLETED)) {
assertThat(cursor.getCount()).isEqualTo(2);
cursor.moveToNext();
DownloadTest.assertEqual(cursor.getDownload(), download1);
assertEqual(cursor.getDownload(), download1);
cursor.moveToNext();
DownloadTest.assertEqual(cursor.getDownload(), download3);
assertEqual(cursor.getDownload(), download3);
}
}
......@@ -216,7 +216,7 @@ public class DefaultDownloadIndexTest {
Download readDownload = downloadIndex.getDownload(id);
Download expectedDownload =
downloadBuilder.setManualStopReason(Download.MANUAL_STOP_REASON_NONE).build();
DownloadTest.assertEqual(readDownload, expectedDownload);
assertEqual(readDownload, expectedDownload);
}
@Test
......@@ -234,7 +234,7 @@ public class DefaultDownloadIndexTest {
Download readDownload = downloadIndex.getDownload(id);
Download expectedDownload = downloadBuilder.setManualStopReason(manualStopReason).build();
DownloadTest.assertEqual(readDownload, expectedDownload);
assertEqual(readDownload, expectedDownload);
}
@Test
......@@ -248,7 +248,7 @@ public class DefaultDownloadIndexTest {
downloadIndex.setManualStopReason(notMetRequirements);
Download readDownload = downloadIndex.getDownload(id);
DownloadTest.assertEqual(readDownload, download);
assertEqual(readDownload, download);
}
@Test
......@@ -264,7 +264,7 @@ public class DefaultDownloadIndexTest {
Download readDownload = downloadIndex.getDownload(id);
Download expectedDownload =
downloadBuilder.setManualStopReason(Download.MANUAL_STOP_REASON_NONE).build();
DownloadTest.assertEqual(readDownload, expectedDownload);
assertEqual(readDownload, expectedDownload);
}
@Test
......@@ -282,7 +282,7 @@ public class DefaultDownloadIndexTest {
Download readDownload = downloadIndex.getDownload(id);
Download expectedDownload = downloadBuilder.setManualStopReason(manualStopReason).build();
DownloadTest.assertEqual(readDownload, expectedDownload);
assertEqual(readDownload, expectedDownload);
}
@Test
......@@ -297,6 +297,18 @@ public class DefaultDownloadIndexTest {
downloadIndex.setManualStopReason(id, notMetRequirements);
Download readDownload = downloadIndex.getDownload(id);
DownloadTest.assertEqual(readDownload, download);
assertEqual(readDownload, download);
}
private static void assertEqual(Download download, Download that) {
assertThat(download.action).isEqualTo(that.action);
assertThat(download.state).isEqualTo(that.state);
assertThat(download.startTimeMs).isEqualTo(that.startTimeMs);
assertThat(download.updateTimeMs).isEqualTo(that.updateTimeMs);
assertThat(download.failureReason).isEqualTo(that.failureReason);
assertThat(download.manualStopReason).isEqualTo(that.manualStopReason);
assertThat(download.getDownloadPercentage()).isEqualTo(that.getDownloadPercentage());
assertThat(download.getDownloadedBytes()).isEqualTo(that.getDownloadedBytes());
assertThat(download.getTotalBytes()).isEqualTo(that.getTotalBytes());
}
}
......@@ -31,6 +31,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
......@@ -449,6 +450,71 @@ public class DownloadManagerTest {
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
}
@Test
public void mergeAction_removingDownload_becomesRestarting() {
DownloadAction downloadAction = createDownloadAction();
DownloadBuilder downloadBuilder =
new DownloadBuilder(downloadAction).setState(Download.STATE_REMOVING);
Download download = downloadBuilder.build();
Download mergedDownload =
DownloadManager.mergeAction(download, downloadAction, download.manualStopReason);
Download expectedDownload = downloadBuilder.setState(Download.STATE_RESTARTING).build();
assertEqualIgnoringTimeFields(mergedDownload, expectedDownload);
}
@Test
public void mergeAction_failedDownload_becomesQueued() {
DownloadAction downloadAction = createDownloadAction();
DownloadBuilder downloadBuilder =
new DownloadBuilder(downloadAction)
.setState(Download.STATE_FAILED)
.setFailureReason(Download.FAILURE_REASON_UNKNOWN);
Download download = downloadBuilder.build();
Download mergedDownload =
DownloadManager.mergeAction(download, downloadAction, download.manualStopReason);
Download expectedDownload =
downloadBuilder
.setState(Download.STATE_QUEUED)
.setFailureReason(Download.FAILURE_REASON_NONE)
.build();
assertEqualIgnoringTimeFields(mergedDownload, expectedDownload);
}
@Test
public void mergeAction_stoppedDownload_staysStopped() {
DownloadAction downloadAction = createDownloadAction();
DownloadBuilder downloadBuilder =
new DownloadBuilder(downloadAction)
.setState(Download.STATE_STOPPED)
.setManualStopReason(/* manualStopReason= */ 1);
Download download = downloadBuilder.build();
Download mergedDownload =
DownloadManager.mergeAction(download, downloadAction, download.manualStopReason);
assertEqualIgnoringTimeFields(mergedDownload, download);
}
@Test
public void mergeAction_manualStopReasonSetButNotStopped_becomesStopped() {
DownloadAction downloadAction = createDownloadAction();
DownloadBuilder downloadBuilder =
new DownloadBuilder(downloadAction)
.setState(Download.STATE_COMPLETED)
.setManualStopReason(/* manualStopReason= */ 1);
Download download = downloadBuilder.build();
Download mergedDownload =
DownloadManager.mergeAction(download, downloadAction, download.manualStopReason);
Download expectedDownload = downloadBuilder.setState(Download.STATE_STOPPED).build();
assertEqualIgnoringTimeFields(mergedDownload, expectedDownload);
}
private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Exception {
if (downloadManager != null) {
releaseDownloadManager();
......@@ -486,6 +552,26 @@ public class DownloadManagerTest {
dummyMainThread.runTestOnMainThread(r);
}
private static void assertEqualIgnoringTimeFields(Download download, Download that) {
assertThat(download.action).isEqualTo(that.action);
assertThat(download.state).isEqualTo(that.state);
assertThat(download.failureReason).isEqualTo(that.failureReason);
assertThat(download.manualStopReason).isEqualTo(that.manualStopReason);
assertThat(download.getDownloadPercentage()).isEqualTo(that.getDownloadPercentage());
assertThat(download.getDownloadedBytes()).isEqualTo(that.getDownloadedBytes());
assertThat(download.getTotalBytes()).isEqualTo(that.getTotalBytes());
}
private static DownloadAction createDownloadAction() {
return new DownloadAction(
"id",
DownloadAction.TYPE_DASH,
Uri.parse("https://www.test.com/download"),
Collections.emptyList(),
/* customCacheKey= */ null,
/* data= */ null);
}
private final class DownloadRunner {
private final Uri uri;
......
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