diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadActionUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadActionUtil.java
new file mode 100644
index 0000000000..f722f9b59b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadActionUtil.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 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.util.Assertions;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashSet;
+
+/** {@link DownloadAction} related utility methods. */
+public class DownloadActionUtil {
+
+ private DownloadActionUtil() {}
+
+ /**
+ * Merge {@link DownloadAction}s in {@code actionQueue} to minimum number of actions.
+ *
+ *
All actions must have the same type and must be for the same media.
+ *
+ * @param actionQueue Queue of actions. Must not be empty.
+ * @return The first action in the queue.
+ */
+ public static DownloadAction mergeActions(ArrayDeque actionQueue) {
+ DownloadAction removeAction = null;
+ DownloadAction downloadAction = null;
+ HashSet keys = new HashSet<>();
+ boolean downloadAllTracks = false;
+ DownloadAction firstAction = Assertions.checkNotNull(actionQueue.peek());
+
+ while (!actionQueue.isEmpty()) {
+ DownloadAction action = actionQueue.remove();
+ Assertions.checkState(action.type.equals(firstAction.type));
+ Assertions.checkState(action.isSameMedia(firstAction));
+ if (action.isRemoveAction) {
+ removeAction = action;
+ downloadAction = null;
+ keys.clear();
+ downloadAllTracks = false;
+ } else {
+ if (!downloadAllTracks) {
+ if (action.keys.isEmpty()) {
+ downloadAllTracks = true;
+ keys.clear();
+ } else {
+ keys.addAll(action.keys);
+ }
+ }
+ downloadAction = action;
+ }
+ }
+
+ if (removeAction != null) {
+ actionQueue.add(removeAction);
+ }
+ if (downloadAction != null) {
+ actionQueue.add(
+ DownloadAction.createDownloadAction(
+ downloadAction.type,
+ downloadAction.uri,
+ new ArrayList<>(keys),
+ downloadAction.customCacheKey,
+ downloadAction.data));
+ }
+ return Assertions.checkNotNull(actionQueue.peek());
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadActionUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadActionUtilTest.java
new file mode 100644
index 0000000000..a494057a05
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadActionUtilTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2018 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import android.net.Uri;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link DownloadActionUtil} class. */
+@RunWith(RobolectricTestRunner.class)
+public class DownloadActionUtilTest {
+ private Uri uri1;
+ private Uri uri2;
+
+ @Before
+ public void setUp() throws Exception {
+ uri1 = Uri.parse("http://abc.com/media1");
+ uri2 = Uri.parse("http://abc.com/media2");
+ }
+
+ @Test
+ public void mergeActions_ifQueueEmpty_throwsException() {
+ try {
+ DownloadActionUtil.mergeActions(toActionQueue());
+ fail();
+ } catch (Exception e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void mergeActions_ifOneActionInQueue_returnsTheSameAction() {
+ DownloadAction action = createDownloadAction(uri1);
+
+ assertThat(DownloadActionUtil.mergeActions(toActionQueue(action))).isEqualTo(action);
+ }
+
+ @Test
+ public void mergeActions_ifActionsHaveDifferentType_throwsException() {
+ DownloadAction downloadAction1 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ null);
+ DownloadAction downloadAction2 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_DASH,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ null);
+ ArrayDeque actionQueue = toActionQueue(downloadAction1, downloadAction2);
+
+ try {
+ DownloadActionUtil.mergeActions(actionQueue);
+ fail();
+ } catch (Exception e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void mergeActions_ifActionsHaveDifferentCacheKeys_throwsException() {
+ DownloadAction downloadAction1 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ "cacheKey1",
+ /* data= */ null);
+ DownloadAction downloadAction2 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ "cacheKey2",
+ /* data= */ null);
+ ArrayDeque actionQueue = toActionQueue(downloadAction1, downloadAction2);
+
+ try {
+ DownloadActionUtil.mergeActions(actionQueue);
+ fail();
+ } catch (Exception e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void mergeActions_nullCacheKeyAndDifferentUrl_throwsException() {
+ DownloadAction downloadAction1 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ null);
+ DownloadAction downloadAction2 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri2,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ null);
+ ArrayDeque actionQueue = toActionQueue(downloadAction1, downloadAction2);
+
+ try {
+ DownloadActionUtil.mergeActions(actionQueue);
+ fail();
+ } catch (Exception e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void mergeActions_sameCacheKeyAndDifferentUrl_latterUrlUsed() {
+ DownloadAction downloadAction1 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ "cacheKey1",
+ /* data= */ null);
+ DownloadAction downloadAction2 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri2,
+ Collections.emptyList(),
+ /* customCacheKey= */ "cacheKey1",
+ /* data= */ null);
+ ArrayDeque actionQueue = toActionQueue(downloadAction1, downloadAction2);
+
+ DownloadActionUtil.mergeActions(actionQueue);
+
+ DownloadAction mergedAction = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(mergedAction.uri).isEqualTo(uri2);
+ }
+
+ @Test
+ public void mergeActions_differentData_latterDataUsed() {
+ byte[] data1 = "data1".getBytes();
+ DownloadAction downloadAction1 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ data1);
+ byte[] data2 = "data2".getBytes();
+ DownloadAction downloadAction2 =
+ DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri1,
+ Collections.emptyList(),
+ /* customCacheKey= */ null,
+ /* data= */ data2);
+ ArrayDeque actionQueue = toActionQueue(downloadAction1, downloadAction2);
+
+ DownloadActionUtil.mergeActions(actionQueue);
+
+ DownloadAction mergedAction = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(mergedAction.data).isEqualTo(data2);
+ }
+
+ @Test
+ public void mergeActions_ifRemoveActionLast_returnsRemoveAction() {
+ DownloadAction downloadAction = createDownloadAction(uri1);
+ DownloadAction removeAction = createRemoveAction(uri1);
+ ArrayDeque actionQueue = toActionQueue(downloadAction, removeAction);
+
+ DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(action).isEqualTo(removeAction);
+ assertThat(actionQueue).containsExactly(removeAction);
+ }
+
+ @Test
+ public void mergeActions_downloadActionAfterRemove_returnsRemoveKeepsDownload() {
+ DownloadAction removeAction = createRemoveAction(uri1);
+ DownloadAction downloadAction = createDownloadAction(uri1);
+ ArrayDeque actionQueue = toActionQueue(removeAction, downloadAction);
+
+ DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(action).isEqualTo(removeAction);
+ assertThat(actionQueue).containsExactly(removeAction, downloadAction);
+ }
+
+ @Test
+ public void mergeActions_downloadActionsAfterRemove_returnsRemoveMergesDownloads() {
+ DownloadAction removeAction = createRemoveAction(uri1);
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ DownloadAction downloadAction1 =
+ createDownloadAction(uri1, Collections.singletonList(streamKey1));
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ DownloadAction downloadAction2 =
+ createDownloadAction(uri1, Collections.singletonList(streamKey2));
+ ArrayDeque actionQueue =
+ toActionQueue(removeAction, downloadAction1, downloadAction2);
+ DownloadAction mergedDownloadAction =
+ createDownloadAction(uri1, Arrays.asList(streamKey1, streamKey2));
+
+ DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(action).isEqualTo(removeAction);
+ assertThat(actionQueue).containsExactly(removeAction, mergedDownloadAction);
+ }
+
+ @Test
+ public void mergeActions_actionBeforeRemove_ignoresActionBeforeRemove() {
+ DownloadAction removeAction = createRemoveAction(uri1);
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ DownloadAction downloadAction1 =
+ createDownloadAction(uri1, Collections.singletonList(streamKey1));
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ DownloadAction downloadAction2 =
+ createDownloadAction(uri1, Collections.singletonList(streamKey2));
+ StreamKey streamKey3 = new StreamKey(/* groupIndex= */ 2, /* trackIndex= */ 2);
+ DownloadAction downloadAction3 =
+ createDownloadAction(uri1, Collections.singletonList(streamKey3));
+ ArrayDeque actionQueue =
+ toActionQueue(downloadAction1, removeAction, downloadAction2, downloadAction3);
+ DownloadAction mergedDownloadAction =
+ createDownloadAction(uri1, Arrays.asList(streamKey2, streamKey3));
+
+ DownloadAction action = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(action).isEqualTo(removeAction);
+ assertThat(actionQueue).containsExactly(removeAction, mergedDownloadAction);
+ }
+
+ @Test
+ public void mergeActions_returnsMergedAction() {
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ StreamKey[] keys1 = new StreamKey[] {streamKey1};
+ StreamKey[] keys2 = new StreamKey[] {streamKey2};
+ StreamKey[] expectedKeys = new StreamKey[] {streamKey1, streamKey2};
+
+ doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
+ }
+
+ @Test
+ public void mergeActions_returnsUniqueKeys() {
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ StreamKey streamKey1Copy = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ StreamKey[] keys1 = new StreamKey[] {streamKey1};
+ StreamKey[] keys2 = new StreamKey[] {streamKey2, streamKey1Copy};
+ StreamKey[] expectedKeys = new StreamKey[] {streamKey1, streamKey2};
+
+ doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
+ }
+
+ @Test
+ public void mergeActions_ifFirstActionKeysEmpty_returnsEmptyKeys() {
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ StreamKey[] keys1 = new StreamKey[] {};
+ StreamKey[] keys2 = new StreamKey[] {streamKey2, streamKey1};
+ StreamKey[] expectedKeys = new StreamKey[] {};
+
+ doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
+ }
+
+ @Test
+ public void mergeActions_ifNotFirstActionKeysEmpty_returnsEmptyKeys() {
+ StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);
+ StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);
+ StreamKey[] keys1 = new StreamKey[] {streamKey2, streamKey1};
+ StreamKey[] keys2 = new StreamKey[] {};
+ StreamKey[] expectedKeys = new StreamKey[] {};
+
+ doTestMergeActionsReturnsMergedKeys(keys1, keys2, expectedKeys);
+ }
+
+ private void doTestMergeActionsReturnsMergedKeys(
+ StreamKey[] keys1, StreamKey[] keys2, StreamKey[] expectedKeys) {
+ DownloadAction action1 = createDownloadAction(uri1, Arrays.asList(keys1));
+ DownloadAction action2 = createDownloadAction(uri1, Arrays.asList(keys2));
+ ArrayDeque actionQueue = toActionQueue(action1, action2);
+
+ DownloadAction mergedAction = DownloadActionUtil.mergeActions(actionQueue);
+
+ assertThat(mergedAction.type).isEqualTo(action1.type);
+ assertThat(mergedAction.uri).isEqualTo(action1.uri);
+ assertThat(mergedAction.customCacheKey).isEqualTo(action1.customCacheKey);
+ assertThat(mergedAction.isRemoveAction).isEqualTo(action1.isRemoveAction);
+ assertThat(mergedAction.keys).containsExactly((Object[]) expectedKeys);
+ assertThat(actionQueue).containsExactly(mergedAction);
+ }
+
+ private ArrayDeque toActionQueue(DownloadAction... actions) {
+ return new ArrayDeque<>(Arrays.asList(actions));
+ }
+
+ private static DownloadAction createDownloadAction(Uri uri) {
+ return createDownloadAction(uri, Collections.emptyList());
+ }
+
+ private static DownloadAction createDownloadAction(Uri uri, List keys) {
+ return DownloadAction.createDownloadAction(
+ DownloadAction.TYPE_PROGRESSIVE, uri, keys, /* customCacheKey= */ null, /* data= */ null);
+ }
+
+ private static DownloadAction createRemoveAction(Uri uri) {
+ return DownloadAction.createRemoveAction(
+ DownloadAction.TYPE_PROGRESSIVE, uri, /* customCacheKey= */ null);
+ }
+}