Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pause/resume a single download #4860 #5165

Closed
wants to merge 10 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public final class DownloadAction {
/** Type for SmoothStreaming downloads. */
public static final String TYPE_SS = "ss";

private static final int VERSION = 2;
private static final int VERSION = 3;

/**
* Deserializes an action from the {@code data}.
Expand Down Expand Up @@ -87,7 +87,7 @@ public static DownloadAction createDownloadAction(
List<StreamKey> keys,
@Nullable String customCacheKey,
@Nullable byte[] data) {
return new DownloadAction(type, uri, /* isRemoveAction= */ false, keys, customCacheKey, data);
return new DownloadAction(type, uri, /* isRemoveAction= */ false, false, keys, customCacheKey, data);
}

/**
Expand All @@ -103,6 +103,7 @@ public static DownloadAction createRemoveAction(
type,
uri,
/* isRemoveAction= */ true,
false,
Collections.emptyList(),
customCacheKey,
/* data= */ null);
Expand All @@ -114,6 +115,8 @@ public static DownloadAction createRemoveAction(
public final Uri uri;
/** Whether this is a remove action. If false, this is a download action. */
public final boolean isRemoveAction;
/** Whether this action is paused. */
public final boolean isPaused;
/**
* Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if this action
* is a remove action.
Expand All @@ -137,12 +140,14 @@ private DownloadAction(
String type,
Uri uri,
boolean isRemoveAction,
boolean isPaused,
List<StreamKey> keys,
@Nullable String customCacheKey,
@Nullable byte[] data) {
this.type = type;
this.uri = uri;
this.isRemoveAction = isRemoveAction;
this.isPaused = isPaused;
this.customCacheKey = customCacheKey;
if (isRemoveAction) {
Assertions.checkArgument(keys.isEmpty());
Expand Down Expand Up @@ -190,6 +195,7 @@ public boolean equals(@Nullable Object o) {
return type.equals(that.type)
&& uri.equals(that.uri)
&& isRemoveAction == that.isRemoveAction
&& isPaused == that.isPaused
&& keys.equals(that.keys)
&& Util.areEqual(customCacheKey, that.customCacheKey)
&& Arrays.equals(data, that.data);
Expand All @@ -200,6 +206,7 @@ public final int hashCode() {
int result = type.hashCode();
result = 31 * result + uri.hashCode();
result = 31 * result + (isRemoveAction ? 1 : 0);
result = 31 * result + (isPaused ? 1 : 0);
result = 31 * result + keys.hashCode();
result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0);
result = 31 * result + Arrays.hashCode(data);
Expand All @@ -220,6 +227,7 @@ public final void serializeToStream(OutputStream output) throws IOException {
dataOutputStream.writeInt(VERSION);
dataOutputStream.writeUTF(uri.toString());
dataOutputStream.writeBoolean(isRemoveAction);
dataOutputStream.writeBoolean(isPaused);
dataOutputStream.writeInt(data.length);
dataOutputStream.write(data);
dataOutputStream.writeInt(keys.size());
Expand All @@ -236,13 +244,36 @@ public final void serializeToStream(OutputStream output) throws IOException {
dataOutputStream.flush();
}

/**
* Create new action with {@link DownloadAction#isPaused} set to true.
*
* @return paused action
*/
final DownloadAction createPausedAction() {
return new DownloadAction(type, uri, isRemoveAction, true, keys, customCacheKey, data);
}

/**
* Create new action with {@link DownloadAction#isPaused} set to false.
*
* @return resumed action
*/
final DownloadAction createResumedAction() {
return new DownloadAction(type, uri, isRemoveAction, false, keys, customCacheKey, data);
}

private static DownloadAction readFromStream(DataInputStream input) throws IOException {
String type = input.readUTF();
int version = input.readInt();

Uri uri = Uri.parse(input.readUTF());
boolean isRemoveAction = input.readBoolean();

boolean isPaused = false;
if (version == 3) {
isPaused = input.readBoolean();
}

int dataLength = input.readInt();
byte[] data;
if (dataLength != 0) {
Expand Down Expand Up @@ -274,7 +305,7 @@ private static DownloadAction readFromStream(DataInputStream input) throws IOExc
customCacheKey = input.readBoolean() ? input.readUTF() : null;
}

return new DownloadAction(type, uri, isRemoveAction, keys, customCacheKey, data);
return new DownloadAction(type, uri, isRemoveAction, isPaused, keys, customCacheKey, data);
}

private static StreamKey readKey(String type, int version, DataInputStream input)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_CANCELED;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_COMPLETED;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_FAILED;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_PAUSED;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_QUEUED;
import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_STARTED;

import android.content.Context;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
Expand All @@ -30,6 +32,7 @@
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Documented;
Expand Down Expand Up @@ -186,6 +189,48 @@ public void stopDownloads() {
}
}

/** Pause existing download action. Call {@link DownloadService#startWithResumeAction(Context, Class, DownloadAction, boolean)} to resume action. */
public void pauseDownload(DownloadAction action) {
Assertions.checkState(!released);
if (!downloadsStopped) {
for (int i = 0; i < activeDownloadTasks.size(); i++) {
Task task = activeDownloadTasks.get(i);
if (task.action.equals(action)) {
task.pause();
break;
}
}
logd("Download is pausing");
}
}

/**
* Deserializes an action from {@code actionData}, and calls {@link
* #handleAction(DownloadAction)}.
*
* @param actionData Serialized version of the action to be executed.
* @return The id of the newly created task.
* @throws IOException If an error occurs deserializing the action.
*/
public int handleAction(byte[] actionData) throws IOException {
Assertions.checkState(!released);
DownloadAction action = createDownloadAction(actionData);
return handleAction(action);
}

/**
* Deserializes an action from {@code actionData}, and calls {@link
* #handleResumeAction(DownloadAction)}.
*
* @param actionData Serialized version of the existing paused action to be executed.
* @throws IOException If an error occurs deserializing the action.
*/
public void handleResumeAction(byte[] actionData) throws IOException {
Assertions.checkState(!released);
DownloadAction action = createDownloadAction(actionData);
handleResumeAction(action);
}

/**
* Handles the given action. A task is created and added to the task queue. If it's a remove
* action then any download tasks for the same media are immediately canceled.
Expand All @@ -208,6 +253,28 @@ public int handleAction(DownloadAction action) {
return task.id;
}

/**
* Handles the given paused action. A resumed task is created and repalce paused task.
*
* @param action Existing paused action to be resumed.
*/
public void handleResumeAction(DownloadAction action) {
for (int i = 0; i < tasks.size(); i++) {
Task task = tasks.get(i);
if (task.isPaused() && task.action.equals(action)) {
Task toBeResumedTask = task.withResumeAction();
toBeResumedTask.resume();
tasks.set(i, toBeResumedTask);
saveActions();
notifyListenersTaskStateChange(toBeResumedTask);
maybeStartTasks();
notifyListenersTaskStateChange(toBeResumedTask);
break;
}
}
logd("Download is resuming");
}

/** Returns the number of tasks. */
public int getTaskCount() {
Assertions.checkState(!released);
Expand Down Expand Up @@ -374,6 +441,11 @@ private void onTaskStateChange(Task task) {
tasks.remove(task);
saveActions();
}
if (task.isPaused()) {
Task pausedTask = task.withPauseAction();
replaceTask(task, pausedTask);
saveActions();
}
if (stopped) {
maybeStartTasks();
maybeNotifyListenersIdle();
Expand All @@ -383,6 +455,7 @@ private void onTaskStateChange(Task task) {
private void notifyListenersTaskStateChange(Task task) {
logd("Task state is changed", task);
TaskState taskState = task.getTaskState();

for (Listener listener : listeners) {
listener.onTaskStateChanged(this, taskState);
}
Expand Down Expand Up @@ -451,6 +524,16 @@ private void saveActions() {
});
}

private DownloadAction createDownloadAction(byte[] actionData) throws IOException {
ByteArrayInputStream input = new ByteArrayInputStream(actionData);
return DownloadAction.deserializeFromStream(input);
}

private void replaceTask(Task oldTask, Task newTask) {
int index = tasks.indexOf(oldTask);
tasks.set(index, newTask);
}

private static void logd(String message) {
if (DEBUG) {
Log.d(TAG, message);
Expand All @@ -466,30 +549,32 @@ public static final class TaskState {

/**
* Task states. One of {@link #STATE_QUEUED}, {@link #STATE_STARTED}, {@link #STATE_COMPLETED},
* {@link #STATE_CANCELED} or {@link #STATE_FAILED}.
* {@link #STATE_PAUSED}, {@link #STATE_CANCELED} or {@link #STATE_FAILED}.
*
* <p>Transition diagram:
*
* <pre>
* ┌────────┬─────→ canceled
* queued ↔ started ┬→ completed
* └→ failed
* ┌────────┬─────→ canceled
* ┌→ queued ↔ started ┬→ completed
* └─ paused ←────┘ └→ failed
* </pre>
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_QUEUED, STATE_STARTED, STATE_COMPLETED, STATE_CANCELED, STATE_FAILED})
@IntDef({STATE_QUEUED, STATE_STARTED, STATE_COMPLETED, STATE_PAUSED, STATE_CANCELED, STATE_FAILED})
public @interface State {}
/** The task is waiting to be started. */
public static final int STATE_QUEUED = 0;
/** The task is currently started. */
public static final int STATE_STARTED = 1;
/** The task completed. */
public static final int STATE_COMPLETED = 2;
/** The task paused. */
public static final int STATE_PAUSED = 3;
/** The task was canceled. */
public static final int STATE_CANCELED = 3;
public static final int STATE_CANCELED = 4;
/** The task failed. */
public static final int STATE_FAILED = 4;
public static final int STATE_FAILED = 5;

/** Returns the state string for the given state value. */
public static String getStateString(@State int state) {
Expand All @@ -500,6 +585,8 @@ public static String getStateString(@State int state) {
return "STARTED";
case STATE_COMPLETED:
return "COMPLETED";
case STATE_PAUSED:
return "PAUSED";
case STATE_CANCELED:
return "CANCELED";
case STATE_FAILED:
Expand Down Expand Up @@ -549,7 +636,7 @@ private static final class Task implements Runnable {
/** Target states for the download thread. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_COMPLETED, STATE_QUEUED, STATE_CANCELED})
@IntDef({STATE_COMPLETED, STATE_QUEUED, STATE_PAUSED, STATE_CANCELED})
public @interface TargetState {}

private final int id;
Expand Down Expand Up @@ -580,7 +667,11 @@ private Task(
this.downloaderFactory = downloaderFactory;
this.action = action;
this.minRetryCount = minRetryCount;
state = STATE_QUEUED;
if (action.isPaused) {
state = STATE_PAUSED;
} else {
state = STATE_QUEUED;
}
targetState = STATE_COMPLETED;
}

Expand All @@ -604,12 +695,29 @@ public boolean isStarted() {
return state == STATE_STARTED;
}

/** Returns whether the task is paused. */
public boolean isPaused() {
return state == STATE_PAUSED;
}

/** Return new task with pause action. */
private Task withPauseAction() {
return withAction(action.createPausedAction());
}

/** Return new task with resume action. */
private Task withResumeAction() {
return withAction(action.createResumedAction());
}

@Override
public String toString() {
return action.type
+ ' '
+ (action.isRemoveAction ? "remove" : "download")
+ ' '
+ (action.isPaused ? "paused" : "not paused")
+ ' '
+ TaskState.getStateString(state)
+ ' '
+ TaskState.getStateString(targetState);
Expand All @@ -633,7 +741,7 @@ public void start() {
public void cancel() {
if (state == STATE_STARTED) {
stopDownloadThread(STATE_CANCELED);
} else if (state == STATE_QUEUED) {
} else if (state == STATE_QUEUED || state == STATE_PAUSED) {
state = STATE_CANCELED;
downloadManager.handler.post(() -> downloadManager.onTaskStateChange(this));
}
Expand All @@ -645,6 +753,30 @@ public void stop() {
}
}

private void pause() {
if (state == STATE_STARTED && targetState == STATE_COMPLETED) {
logd("Pausing", this);
stopDownloadThread(STATE_PAUSED);
}
}

private void resume() {
if (state == STATE_PAUSED) {
state = STATE_QUEUED;
targetState = STATE_COMPLETED;
}
}

private Task withAction(DownloadAction action){
Task task = new Task(id, downloadManager, downloaderFactory, action, minRetryCount);
task.downloader = downloader;
task.thread = thread;
task.error = error;
task.state = state;
task.targetState = state;
return task;
}

// Internal methods running on the main thread.

private void stopDownloadThread(@TargetState int targetState) {
Expand Down
Loading