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); + } +}