diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java
new file mode 100644
index 0000000000..63602c7641
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.offline.DownloadState.State;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+
+/** {@link DownloadIndex} related utility methods. */
+public final class DownloadIndexUtil {
+
+ /** An interface to provide custom download ids during ActionFile upgrade. */
+ public interface DownloadIdProvider {
+
+ /**
+ * Returns a custom download id for given action.
+ *
+ * @param downloadAction The action which is an id requested for.
+ * @return A custom download id for given action.
+ */
+ String getId(DownloadAction downloadAction);
+ }
+
+ private DownloadIndexUtil() {}
+
+ /**
+ * Upgrades an {@link ActionFile} to {@link DownloadIndex}.
+ *
+ *
This method shouldn't be called while {@link DownloadIndex} is used by {@link
+ * DownloadManager}.
+ *
+ * @param actionFile The action file to upgrade.
+ * @param downloadIndex Actions are converted to {@link DownloadState}s and stored in this index.
+ * @param downloadIdProvider A nullable custom download id provider.
+ * @throws IOException If there is an error during loading actions.
+ */
+ public static void upgradeActionFile(
+ ActionFile actionFile,
+ DownloadIndex downloadIndex,
+ @Nullable DownloadIdProvider downloadIdProvider)
+ throws IOException {
+ if (downloadIdProvider == null) {
+ downloadIdProvider = downloadAction -> downloadAction.id;
+ }
+ for (DownloadAction action : actionFile.load()) {
+ addAction(downloadIndex, downloadIdProvider.getId(action), action);
+ }
+ }
+
+ /**
+ * Converts a {@link DownloadAction} to {@link DownloadState} and stored in the given {@link
+ * DownloadIndex}.
+ *
+ *
This method shouldn't be called while {@link DownloadIndex} is used by {@link
+ * DownloadManager}.
+ *
+ * @param downloadIndex The action is converted to {@link DownloadState} and stored in this index.
+ * @param id A nullable custom download id which overwrites {@link DownloadAction#id}.
+ * @param action The action to be stored in {@link DownloadIndex}.
+ */
+ public static void addAction(
+ DownloadIndex downloadIndex, @Nullable String id, DownloadAction action) {
+ DownloadState downloadState = downloadIndex.getDownloadState(id != null ? id : action.id);
+ if (downloadState != null) {
+ downloadState = merge(downloadState, action);
+ } else {
+ downloadState = convert(action);
+ }
+ downloadIndex.putDownloadState(downloadState);
+ }
+
+ private static DownloadState merge(DownloadState downloadState, DownloadAction action) {
+ Assertions.checkArgument(action.type.equals(downloadState.type));
+ @State int newState;
+ if (action.isRemoveAction) {
+ newState = DownloadState.STATE_REMOVING;
+ } else {
+ if (downloadState.state == DownloadState.STATE_REMOVING
+ || downloadState.state == DownloadState.STATE_RESTARTING) {
+ newState = DownloadState.STATE_RESTARTING;
+ } else if (downloadState.state == DownloadState.STATE_STOPPED) {
+ newState = DownloadState.STATE_STOPPED;
+ } else {
+ newState = DownloadState.STATE_QUEUED;
+ }
+ }
+ HashSet keys = new HashSet<>(action.keys);
+ Collections.addAll(keys, downloadState.streamKeys);
+ StreamKey[] newKeys = keys.toArray(new StreamKey[0]);
+ return new DownloadState(
+ downloadState.id,
+ downloadState.type,
+ action.uri,
+ action.customCacheKey,
+ newState,
+ /* downloadPercentage= */ C.PERCENTAGE_UNSET,
+ downloadState.downloadedBytes,
+ /* totalBytes= */ C.LENGTH_UNSET,
+ downloadState.failureReason,
+ downloadState.stopFlags,
+ downloadState.startTimeMs,
+ downloadState.updateTimeMs,
+ newKeys,
+ action.data);
+ }
+
+ private static DownloadState convert(DownloadAction action) {
+ long currentTimeMs = System.currentTimeMillis();
+ return new DownloadState(
+ action.id,
+ action.type,
+ action.uri,
+ action.customCacheKey,
+ /* state= */ action.isRemoveAction
+ ? DownloadState.STATE_REMOVING
+ : DownloadState.STATE_QUEUED,
+ /* downloadPercentage= */ C.PERCENTAGE_UNSET,
+ /* downloadedBytes= */ 0,
+ /* totalBytes= */ C.LENGTH_UNSET,
+ DownloadState.FAILURE_REASON_NONE,
+ /* stopFlags= */ 0,
+ /* startTimeMs= */ currentTimeMs,
+ /* updateTimeMs= */ currentTimeMs,
+ action.keys.toArray(new StreamKey[0]),
+ action.data);
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java
new file mode 100644
index 0000000000..376c840296
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+import static com.google.android.exoplayer2.offline.DownloadAction.TYPE_DASH;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.util.Util;
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+/** Unit tests for {@link DownloadIndexUtil}. */
+@RunWith(RobolectricTestRunner.class)
+public class DownloadIndexUtilTest {
+
+ private DefaultDownloadIndex downloadIndex;
+ private File tempFile;
+
+ @Before
+ public void setUp() throws Exception {
+ tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest");
+ downloadIndex = new DefaultDownloadIndex(RuntimeEnvironment.application);
+ }
+
+ @After
+ public void tearDown() {
+ downloadIndex.release();
+ tempFile.delete();
+ }
+
+ @Test
+ public void addAction_nonExistingDownloadState_createsNewDownloadState() {
+ byte[] data = new byte[] {1, 2, 3, 4};
+ DownloadAction action =
+ DownloadAction.createDownloadAction(
+ TYPE_DASH,
+ Uri.parse("https://www.test.com/download"),
+ asList(
+ new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2),
+ new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5)),
+ /* customCacheKey= */ "key123",
+ data);
+
+ DownloadIndexUtil.addAction(downloadIndex, action.id, action);
+
+ assertDownloadIndexContainsAction(action, DownloadState.STATE_QUEUED);
+ }
+
+ @Test
+ public void addAction_existingDownloadState_createsMergedDownloadState() {
+ StreamKey streamKey1 =
+ new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5);
+ StreamKey streamKey2 =
+ new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2);
+ DownloadAction action1 =
+ DownloadAction.createDownloadAction(
+ TYPE_DASH,
+ Uri.parse("https://www.test.com/download1"),
+ asList(streamKey1),
+ /* customCacheKey= */ "key123",
+ new byte[] {1, 2, 3, 4});
+ DownloadAction action2 =
+ DownloadAction.createDownloadAction(
+ TYPE_DASH,
+ Uri.parse("https://www.test.com/download2"),
+ asList(streamKey2),
+ /* customCacheKey= */ "key123",
+ new byte[] {5, 4, 3, 2, 1});
+ DownloadIndexUtil.addAction(downloadIndex, action1.id, action1);
+
+ DownloadIndexUtil.addAction(downloadIndex, action2.id, action2);
+
+ DownloadState downloadState = downloadIndex.getDownloadState(action2.id);
+ assertThat(downloadState).isNotNull();
+ assertThat(downloadState.type).isEqualTo(action2.type);
+ assertThat(downloadState.cacheKey).isEqualTo(action2.customCacheKey);
+ assertThat(downloadState.customMetadata).isEqualTo(action2.data);
+ assertThat(downloadState.uri).isEqualTo(action2.uri);
+ assertThat(downloadState.streamKeys).isEqualTo(new StreamKey[] {streamKey2, streamKey1});
+ assertThat(downloadState.state).isEqualTo(DownloadState.STATE_QUEUED);
+ }
+
+ @Test
+ public void upgradeActionFile_createsDownloadStates() throws Exception {
+ ActionFile actionFile = new ActionFile(tempFile);
+ StreamKey streamKey1 =
+ new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5);
+ StreamKey streamKey2 =
+ new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2);
+ DownloadAction action1 =
+ DownloadAction.createDownloadAction(
+ TYPE_DASH,
+ Uri.parse("https://www.test.com/download1"),
+ asList(streamKey1),
+ /* customCacheKey= */ "key123",
+ new byte[] {1, 2, 3, 4});
+ DownloadAction action2 =
+ DownloadAction.createDownloadAction(
+ TYPE_DASH,
+ Uri.parse("https://www.test.com/download2"),
+ asList(streamKey2),
+ /* customCacheKey= */ "key234",
+ new byte[] {5, 4, 3, 2, 1});
+ actionFile.store(action1, action2);
+ DownloadAction action3 =
+ DownloadAction.createRemoveAction(
+ TYPE_DASH, Uri.parse("https://www.test.com/download3"), /* customCacheKey= */ "key345");
+ actionFile.store(action1, action2, action3);
+
+ DownloadIndexUtil.upgradeActionFile(actionFile, downloadIndex, /* downloadIdProvider= */ null);
+
+ assertDownloadIndexContainsAction(action1, DownloadState.STATE_QUEUED);
+ assertDownloadIndexContainsAction(action2, DownloadState.STATE_QUEUED);
+ assertDownloadIndexContainsAction(action3, DownloadState.STATE_REMOVING);
+ }
+
+ private void assertDownloadIndexContainsAction(DownloadAction action, int state) {
+ DownloadState downloadState = downloadIndex.getDownloadState(action.id);
+ assertThat(downloadState).isNotNull();
+ assertThat(downloadState.type).isEqualTo(action.type);
+ assertThat(downloadState.cacheKey).isEqualTo(action.customCacheKey);
+ assertThat(downloadState.customMetadata).isEqualTo(action.data);
+ assertThat(downloadState.uri).isEqualTo(action.uri);
+ assertThat(downloadState.streamKeys).isEqualTo(action.keys.toArray(new StreamKey[0]));
+ assertThat(downloadState.state).isEqualTo(state);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List asList(StreamKey... streamKeys) {
+ return Arrays.asList(streamKeys);
+ }
+}