diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java new file mode 100644 index 0000000000..5de6bdf3e1 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2017 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.source; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; +import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; +import junit.framework.TestCase; + +/** + * Unit test for {@link ShuffleOrder}. + */ +public final class ShuffleOrderTest extends TestCase { + + public static final long RANDOM_SEED = 1234567890L; + + public void testDefaultShuffleOrder() { + assertShuffleOrderCorrectness(new DefaultShuffleOrder(0, RANDOM_SEED), 0); + assertShuffleOrderCorrectness(new DefaultShuffleOrder(1, RANDOM_SEED), 1); + assertShuffleOrderCorrectness(new DefaultShuffleOrder(5, RANDOM_SEED), 5); + for (int initialLength = 0; initialLength < 4; initialLength++) { + for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) { + testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 0); + testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 1); + testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 5); + } + } + testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0); + testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2); + testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4); + testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0); + } + + public void testUnshuffledShuffleOrder() { + assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(0), 0); + assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(1), 1); + assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(5), 5); + for (int initialLength = 0; initialLength < 4; initialLength++) { + for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) { + testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 0); + testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 1); + testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 5); + } + } + testCloneAndRemove(new UnshuffledShuffleOrder(5), 0); + testCloneAndRemove(new UnshuffledShuffleOrder(5), 2); + testCloneAndRemove(new UnshuffledShuffleOrder(5), 4); + testCloneAndRemove(new UnshuffledShuffleOrder(1), 0); + } + + public void testUnshuffledShuffleOrderIsUnshuffled() { + ShuffleOrder shuffleOrder = new UnshuffledShuffleOrder(5); + assertEquals(0, shuffleOrder.getFirstIndex()); + assertEquals(4, shuffleOrder.getLastIndex()); + for (int i = 0; i < 4; i++) { + assertEquals(i + 1, shuffleOrder.getNextIndex(i)); + } + } + + private static void assertShuffleOrderCorrectness(ShuffleOrder shuffleOrder, int length) { + assertEquals(length, shuffleOrder.getLength()); + if (length == 0) { + assertEquals(C.INDEX_UNSET, shuffleOrder.getFirstIndex()); + assertEquals(C.INDEX_UNSET, shuffleOrder.getLastIndex()); + } else { + int[] indices = new int[length]; + indices[0] = shuffleOrder.getFirstIndex(); + assertEquals(C.INDEX_UNSET, shuffleOrder.getPreviousIndex(indices[0])); + for (int i = 1; i < length; i++) { + indices[i] = shuffleOrder.getNextIndex(indices[i - 1]); + assertEquals(indices[i - 1], shuffleOrder.getPreviousIndex(indices[i])); + for (int j = 0; j < i; j++) { + assertTrue(indices[i] != indices[j]); + } + } + assertEquals(indices[length - 1], shuffleOrder.getLastIndex()); + assertEquals(C.INDEX_UNSET, shuffleOrder.getNextIndex(indices[length - 1])); + for (int i = 0; i < length; i++) { + assertTrue(indices[i] >= 0 && indices[i] < length); + } + } + } + + private static void testCloneAndInsert(ShuffleOrder shuffleOrder, int position, int count) { + ShuffleOrder newOrder = shuffleOrder.cloneAndInsert(position, count); + assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() + count); + // Assert all elements still have the relative same order + for (int i = 0; i < shuffleOrder.getLength(); i++) { + int expectedNextIndex = shuffleOrder.getNextIndex(i); + if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) { + expectedNextIndex += count; + } + int newNextIndex = newOrder.getNextIndex(i < position ? i : i + count); + while (newNextIndex >= position && newNextIndex < position + count) { + newNextIndex = newOrder.getNextIndex(newNextIndex); + } + assertEquals(expectedNextIndex, newNextIndex); + } + } + + private static void testCloneAndRemove(ShuffleOrder shuffleOrder, int position) { + ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(position); + assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - 1); + // Assert all elements still have the relative same order + for (int i = 0; i < shuffleOrder.getLength(); i++) { + if (i == position) { + continue; + } + int expectedNextIndex = shuffleOrder.getNextIndex(i); + if (expectedNextIndex == position) { + expectedNextIndex = shuffleOrder.getNextIndex(expectedNextIndex); + } + if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) { + expectedNextIndex--; + } + int newNextIndex = newOrder.getNextIndex(i < position ? i : i - 1); + assertEquals(expectedNextIndex, newNextIndex); + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java new file mode 100644 index 0000000000..4307fd2c19 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2017 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.source; + +import com.google.android.exoplayer2.C; +import java.util.Arrays; +import java.util.Random; + +/** + * Shuffled order of indices. + */ +public interface ShuffleOrder { + + /** + * The default {@link ShuffleOrder} implementation for random shuffle order. + */ + class DefaultShuffleOrder implements ShuffleOrder { + + private final Random random; + private final int[] shuffled; + private final int[] indexInShuffled; + + /** + * Creates an instance with a specified length. + * + * @param length The length of the shuffle order. + */ + public DefaultShuffleOrder(int length) { + this(length, new Random()); + } + + /** + * Creates an instance with a specified length and the specified random seed. Shuffle orders of + * the same length initialized with the same random seed are guaranteed to be equal. + * + * @param length The length of the shuffle order. + * @param randomSeed A random seed. + */ + public DefaultShuffleOrder(int length, long randomSeed) { + this(length, new Random(randomSeed)); + } + + private DefaultShuffleOrder(int length, Random random) { + this(createShuffledList(length, random), random); + } + + private DefaultShuffleOrder(int[] shuffled, Random random) { + this.shuffled = shuffled; + this.random = random; + this.indexInShuffled = new int[shuffled.length]; + for (int i = 0; i < shuffled.length; i++) { + indexInShuffled[shuffled[i]] = i; + } + } + + @Override + public int getLength() { + return shuffled.length; + } + + @Override + public int getNextIndex(int index) { + int shuffledIndex = indexInShuffled[index]; + return ++shuffledIndex < shuffled.length ? shuffled[shuffledIndex] : C.INDEX_UNSET; + } + + @Override + public int getPreviousIndex(int index) { + int shuffledIndex = indexInShuffled[index]; + return --shuffledIndex >= 0 ? shuffled[shuffledIndex] : C.INDEX_UNSET; + } + + @Override + public int getLastIndex() { + return shuffled.length > 0 ? shuffled[shuffled.length - 1] : C.INDEX_UNSET; + } + + @Override + public int getFirstIndex() { + return shuffled.length > 0 ? shuffled[0] : C.INDEX_UNSET; + } + + @Override + public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) { + int[] insertionPoints = new int[insertionCount]; + int[] insertionValues = new int[insertionCount]; + for (int i = 0; i < insertionCount; i++) { + insertionPoints[i] = random.nextInt(shuffled.length + 1); + int swapIndex = random.nextInt(i + 1); + insertionValues[i] = insertionValues[swapIndex]; + insertionValues[swapIndex] = i + insertionIndex; + } + Arrays.sort(insertionPoints); + int[] newShuffled = new int[shuffled.length + insertionCount]; + int indexInOldShuffled = 0; + int indexInInsertionList = 0; + for (int i = 0; i < shuffled.length + insertionCount; i++) { + if (indexInInsertionList < insertionCount + && indexInOldShuffled == insertionPoints[indexInInsertionList]) { + newShuffled[i] = insertionValues[indexInInsertionList++]; + } else { + newShuffled[i] = shuffled[indexInOldShuffled++]; + if (newShuffled[i] >= insertionIndex) { + newShuffled[i] += insertionCount; + } + } + } + return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong())); + } + + @Override + public ShuffleOrder cloneAndRemove(int removalIndex) { + int[] newShuffled = new int[shuffled.length - 1]; + boolean foundRemovedElement = false; + for (int i = 0; i < shuffled.length; i++) { + if (shuffled[i] == removalIndex) { + foundRemovedElement = true; + } else { + newShuffled[foundRemovedElement ? i - 1 : i] = shuffled[i] > removalIndex + ? shuffled[i] - 1 : shuffled[i]; + } + } + return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong())); + } + + private static int[] createShuffledList(int length, Random random) { + int[] shuffled = new int[length]; + for (int i = 0; i < length; i++) { + int swapIndex = random.nextInt(i + 1); + shuffled[i] = shuffled[swapIndex]; + shuffled[swapIndex] = i; + } + return shuffled; + } + + } + + /** + * A {@link ShuffleOrder} implementation which does not shuffle. + */ + final class UnshuffledShuffleOrder implements ShuffleOrder { + + private final int length; + + /** + * Creates an instance with a specified length. + * + * @param length The length of the shuffle order. + */ + public UnshuffledShuffleOrder(int length) { + this.length = length; + } + + @Override + public int getLength() { + return length; + } + + @Override + public int getNextIndex(int index) { + return ++index < length ? index : C.INDEX_UNSET; + } + + @Override + public int getPreviousIndex(int index) { + return --index >= 0 ? index : C.INDEX_UNSET; + } + + @Override + public int getLastIndex() { + return length > 0 ? length - 1 : C.INDEX_UNSET; + } + + @Override + public int getFirstIndex() { + return length > 0 ? 0 : C.INDEX_UNSET; + } + + @Override + public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) { + return new UnshuffledShuffleOrder(length + insertionCount); + } + + @Override + public ShuffleOrder cloneAndRemove(int removalIndex) { + return new UnshuffledShuffleOrder(length - 1); + } + + } + + /** + * Returns length of shuffle order. + */ + int getLength(); + + /** + * Returns the next index in the shuffle order. + * + * @param index An index. + * @return The index after {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the last + * element. + */ + int getNextIndex(int index); + + /** + * Returns the previous index in the shuffle order. + * + * @param index An index. + * @return The index before {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the first + * element. + */ + int getPreviousIndex(int index); + + /** + * Returns the last index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is + * empty. + */ + int getLastIndex(); + + /** + * Returns the first index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is + * empty. + */ + int getFirstIndex(); + + /** + * Return a copy of the shuffle order with newly inserted elements. + * + * @param insertionIndex The index in the unshuffled order at which elements are inserted. + * @param insertionCount The number of elements inserted at {@code insertionIndex}. + * @return A copy of this {@link ShuffleOrder} with newly inserted elements. + */ + ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount); + + /** + * Return a copy of the shuffle order with one element removed. + * + * @param removalIndex The index of the element in the unshuffled order which is to be removed. + * @return A copy of this {@link ShuffleOrder} without the removed element. + */ + ShuffleOrder cloneAndRemove(int removalIndex); + +}