diff options
author | orionlee <orionlee@yahoo.com> | 2019-10-04 13:06:29 -0700 |
---|---|---|
committer | orionlee <orionlee@yahoo.com> | 2019-11-05 12:33:58 -0800 |
commit | cd3d20d61338180bfa585fbf8fcc2b13261df709 (patch) | |
tree | 243e82df10dfc8c1199cf4486b85c47eeb66ee26 | |
parent | 2d1ee52014aa6b171733261a698129f5de3f4036 (diff) | |
download | AntennaPod-cd3d20d61338180bfa585fbf8fcc2b13261df709.zip |
refactor - move ItemEnqueuePositionCalculator to top-level per review.
4 files changed, 439 insertions, 430 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index 1ba58f3d3..57db6123c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -398,101 +398,6 @@ public class DBWriter { events.add(QueueEvent.sorted(queue)); } - @VisibleForTesting - static class ItemEnqueuePositionCalculator { - - public static class Options { - private boolean enqueueAtFront = false; - private boolean keepInProgressAtFront = false; - - public boolean isEnqueueAtFront() { - return enqueueAtFront; - } - - public Options setEnqueueAtFront(boolean enqueueAtFront) { - this.enqueueAtFront = enqueueAtFront; - return this; - } - - public boolean isKeepInProgressAtFront() { - return keepInProgressAtFront; - } - - public Options setKeepInProgressAtFront(boolean keepInProgressAtFront) { - this.keepInProgressAtFront = keepInProgressAtFront; - return this; - } - } - - private final @NonNull Options options; - - /** - * The logic needs to use {@link DownloadRequester#isDownloadingFile(FeedFile)} method only - */ - @VisibleForTesting - FeedFileDownloadStatusRequesterInterface requester = DownloadRequester.getInstance(); - - public ItemEnqueuePositionCalculator(@NonNull Options options) { - this.options = options; - } - - /** - * - * @param positionAmongToAdd Typically, the callers has a list of items to be inserted to - * the queue. This parameter indicates the position (0-based) of - * the item among the one to inserted. E.g., it is needed for - * enqueue at front option. - * - * @param item the item to be inserted - * @param curQueue the queue to which the item is to be inserted - * @return the position (0-based) the item should be inserted to the named queu - */ - public int calcPosition(int positionAmongToAdd, FeedItem item, List<FeedItem> curQueue) { - if (options.isEnqueueAtFront()) { - if (options.isKeepInProgressAtFront() && - curQueue.size() > 0 && - curQueue.get(0).getMedia() != null && - curQueue.get(0).getMedia().isInProgress()) { - // leave the front in progress item at the front - return getPositionOf1stNonDownloadingItem(positionAmongToAdd + 1, curQueue); - } else { // typical case - // return NOT 0, so that when a list of items are inserted, the items inserted - // keep the same order. Returning 0 will reverse the order - return getPositionOf1stNonDownloadingItem(positionAmongToAdd, curQueue); - } - } else { - return curQueue.size(); - } - } - - private int getPositionOf1stNonDownloadingItem(int startPosition, List<FeedItem> curQueue) { - final int curQueueSize = curQueue.size(); - for (int i = startPosition; i < curQueueSize; i++) { - if (!isItemAtPositionDownloading(i, curQueue)) { - return i; - } // else continue to search; - } - return curQueueSize; - } - - private boolean isItemAtPositionDownloading(int position, List<FeedItem> curQueue) { - FeedItem curItem; - try { - curItem = curQueue.get(position); - } catch (IndexOutOfBoundsException e) { - curItem = null; - } - - if (curItem != null && - curItem.getMedia() != null && - requester.isDownloadingFile(curItem.getMedia())) { - return true; - } else { - return false; - } - } - } - /** * Removes all FeedItem objects from the queue. */ diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java b/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java new file mode 100644 index 000000000..6e7843836 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java @@ -0,0 +1,109 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.util.List; + +import de.danoeh.antennapod.core.feed.FeedItem; + +/** + * @see DBWriter#addQueueItem(Context, boolean, long...) it uses the class to determine + * the positions of the {@link FeedItem} in the queue. + */ +class ItemEnqueuePositionCalculator { + + public static class Options { + private boolean enqueueAtFront = false; + private boolean keepInProgressAtFront = false; + + public boolean isEnqueueAtFront() { + return enqueueAtFront; + } + + public Options setEnqueueAtFront(boolean enqueueAtFront) { + this.enqueueAtFront = enqueueAtFront; + return this; + } + + public boolean isKeepInProgressAtFront() { + return keepInProgressAtFront; + } + + public Options setKeepInProgressAtFront(boolean keepInProgressAtFront) { + this.keepInProgressAtFront = keepInProgressAtFront; + return this; + } + } + + private final @NonNull + Options options; + + /** + * The logic needs to use {@link DownloadRequester#isDownloadingFile(FeedFile)} method only + */ + @VisibleForTesting + FeedFileDownloadStatusRequesterInterface requester = DownloadRequester.getInstance(); + + public ItemEnqueuePositionCalculator(@NonNull Options options) { + this.options = options; + } + + /** + * + * @param positionAmongToAdd Typically, the callers has a list of items to be inserted to + * the queue. This parameter indicates the position (0-based) of + * the item among the one to inserted. E.g., it is needed for + * enqueue at front option. + * + * @param item the item to be inserted + * @param curQueue the queue to which the item is to be inserted + * @return the position (0-based) the item should be inserted to the named queu + */ + public int calcPosition(int positionAmongToAdd, FeedItem item, List<FeedItem> curQueue) { + if (options.isEnqueueAtFront()) { + if (options.isKeepInProgressAtFront() && + curQueue.size() > 0 && + curQueue.get(0).getMedia() != null && + curQueue.get(0).getMedia().isInProgress()) { + // leave the front in progress item at the front + return getPositionOf1stNonDownloadingItem(positionAmongToAdd + 1, curQueue); + } else { // typical case + // return NOT 0, so that when a list of items are inserted, the items inserted + // keep the same order. Returning 0 will reverse the order + return getPositionOf1stNonDownloadingItem(positionAmongToAdd, curQueue); + } + } else { + return curQueue.size(); + } + } + + private int getPositionOf1stNonDownloadingItem(int startPosition, List<FeedItem> curQueue) { + final int curQueueSize = curQueue.size(); + for (int i = startPosition; i < curQueueSize; i++) { + if (!isItemAtPositionDownloading(i, curQueue)) { + return i; + } // else continue to search; + } + return curQueueSize; + } + + private boolean isItemAtPositionDownloading(int position, List<FeedItem> curQueue) { + FeedItem curItem; + try { + curItem = curQueue.get(position); + } catch (IndexOutOfBoundsException e) { + curItem = null; + } + + if (curItem != null && + curItem.getMedia() != null && + requester.isDownloadingFile(curItem.getMedia())) { + return true; + } else { + return false; + } + } +} diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DBWriterTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DBWriterTest.java deleted file mode 100644 index 46822de81..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DBWriterTest.java +++ /dev/null @@ -1,335 +0,0 @@ -package de.danoeh.antennapod.core.storage; - -import androidx.annotation.NonNull; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import de.danoeh.antennapod.core.feed.FeedFile; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.FeedMother; -import de.danoeh.antennapod.core.storage.DBWriter.ItemEnqueuePositionCalculator; -import de.danoeh.antennapod.core.storage.DBWriter.ItemEnqueuePositionCalculator.Options; - -import static org.junit.Assert.assertEquals; - -public class DBWriterTest { - - public static class ItemEnqueuePositionCalculatorTest { - - @RunWith(Parameterized.class) - public static class IEPCBasicTest { - @Parameters(name = "{index}: case<{0}>, expected:{1}") - public static Iterable<Object[]> data() { - Options optDefault = new Options(); - Options optEnqAtFront = new Options().setEnqueueAtFront(true); - - return Arrays.asList(new Object[][]{ - {"case default, i.e., add to the end", - concat(QUEUE_DEFAULT_IDS, TFI_ID), - optDefault, 0, QUEUE_DEFAULT}, - {"case default (2nd item)", - concat(QUEUE_DEFAULT_IDS, TFI_ID), - optDefault, 1, QUEUE_DEFAULT}, - {"case option enqueue at front", - concat(TFI_ID, QUEUE_DEFAULT_IDS), - optEnqAtFront, 0, QUEUE_DEFAULT}, - {"case option enqueue at front (2nd item)", - list(11L, TFI_ID, 12L, 13L, 14L), - optEnqAtFront, 1, QUEUE_DEFAULT}, - {"case empty queue, option default", - list(TFI_ID), - optDefault, 0, QUEUE_EMPTY}, - {"case empty queue, option enqueue at front", - list(TFI_ID), - optEnqAtFront, 0, QUEUE_EMPTY}, - }); - } - - @Parameter - public String message; - - @Parameter(1) - public List<Long> idsExpected; - - @Parameter(2) - public Options options; - - @Parameter(3) - public int posAmongAdded; // the position of feed item to be inserted among the list to be inserted. - - @Parameter(4) - public List<FeedItem> curQueue; - - - public static final long TFI_ID = 101; - - /** - * Add a FeedItem with ID {@link #TFI_ID} with the setup - */ - @Test - public void test() { - ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options); - - // shallow copy to which the test will add items - List<FeedItem> queue = new ArrayList<>(curQueue); - FeedItem tFI = tFI(TFI_ID); - doAddToQueueAndAssertResult(message, - calculator, posAmongAdded, tFI, queue, - idsExpected); - } - - } - - @RunWith(Parameterized.class) - public static class IEPCKeepInProgressAtFrontTest extends IEPCBasicTest { - @Parameters(name = "{index}: case<{0}>, expected:{1}") - public static Iterable<Object[]> data() { - Options optKeepInProgressAtFront = - new Options().setEnqueueAtFront(true).setKeepInProgressAtFront(true); - // edge case: keep in progress without enabling enqueue at front is meaningless - Options optKeepInProgressAtFrontWithNoEnqueueAtFront = - new Options().setKeepInProgressAtFront(true); - - return Arrays.asList(new Object[][]{ - {"case option keep in progress at front", - list(11L, TFI_ID, 12L, 13L), - optKeepInProgressAtFront, 0, QUEUE_FRONT_IN_PROGRESS}, - {"case option keep in progress at front (2nd item)", - list(11L, 12L, TFI_ID, 13L), - optKeepInProgressAtFront, 1, QUEUE_FRONT_IN_PROGRESS}, - {"case option keep in progress at front, front item not in progress", - concat(TFI_ID, QUEUE_DEFAULT_IDS), - optKeepInProgressAtFront, 0, QUEUE_DEFAULT}, - {"case option keep in progress at front, front item no media at all", - concat(TFI_ID, QUEUE_FRONT_NO_MEDIA_IDS), - optKeepInProgressAtFront, 0, QUEUE_FRONT_NO_MEDIA}, // No media should not cause any exception - {"case option keep in progress at front, but enqueue at front is disabled", - concat(QUEUE_FRONT_IN_PROGRESS_IDS, TFI_ID), - optKeepInProgressAtFrontWithNoEnqueueAtFront, 0, QUEUE_FRONT_IN_PROGRESS}, - {"case empty queue, option keep in progress at front", - list(TFI_ID), - optKeepInProgressAtFront, 0, QUEUE_EMPTY}, - }); - } - - private static final List<FeedItem> QUEUE_FRONT_IN_PROGRESS = Arrays.asList(tFI(11, 60000), tFI(12), tFI(13)); - private static final List<Long> QUEUE_FRONT_IN_PROGRESS_IDS = toIDs(QUEUE_FRONT_IN_PROGRESS); - - private static final List<FeedItem> QUEUE_FRONT_NO_MEDIA = Arrays.asList(tFINoMedia(11), tFI(12), tFI(13)); - private static final List<Long> QUEUE_FRONT_NO_MEDIA_IDS = toIDs(QUEUE_FRONT_NO_MEDIA); - - } - - @RunWith(Parameterized.class) - public static class ItemEnqueuePositionCalculatorPreserveDownloadOrderTest { - - @Parameters(name = "{index}: case<{0}>") - public static Iterable<Object[]> data() { - Options optDefault = new Options(); - Options optEnqAtFront = new Options().setEnqueueAtFront(true); - - // Attempts to make test more readable by showing the expected list of ids - // (rather than the expected positions) - return Arrays.asList(new Object[][] { - {"download order test, enqueue default", - concat(QUEUE_DEFAULT_IDS, 101L), - concat(QUEUE_DEFAULT_IDS, list(101L, 102L)), - concat(QUEUE_DEFAULT_IDS, list(101L, 102L, 201L)), - concat(QUEUE_DEFAULT_IDS, list(101L, 102L, 201L, 202L)), - optDefault, QUEUE_DEFAULT}, - {"download order test, enqueue at front", - concat(101L, QUEUE_DEFAULT_IDS), - concat(list(101L, 102L), QUEUE_DEFAULT_IDS), - concat(list(101L, 102L, 201L), QUEUE_DEFAULT_IDS), - concat(list(101L, 102L, 201L, 202L), QUEUE_DEFAULT_IDS), - optEnqAtFront, QUEUE_DEFAULT}, - }); - } - - @Parameter - public String message; - - @Parameter(1) - public List<Long> idsExpectedAfter101; - - @Parameter(2) - public List<Long> idsExpectedAfter102; - - // 2XX are for testing bulk insertion cases - @Parameter(3) - public List<Long> idsExpectedAfter201; - - @Parameter(4) - public List<Long> idsExpectedAfter202; - - @Parameter(5) - public Options options; - - @Parameter(6) - public List<FeedItem> queueInitial; - - @Test - public void testQueueOrderWhenDownloading2Items() { - - // Setup class under test - // - ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options); - MockDownloadRequester mockDownloadRequester = new MockDownloadRequester(); - calculator.requester = mockDownloadRequester; - - // Setup initial data - // A shallow copy, as the test code will manipulate the queue - List<FeedItem> queue = new ArrayList<>(queueInitial); - - - // Test body - - // User clicks download on feed item 101 - FeedItem tFI101 = tFI_isDownloading(101, mockDownloadRequester); - doAddToQueueAndAssertResult(message + " (1st download)", - calculator, 0, tFI101, queue, - idsExpectedAfter101); - - // Then user clicks download on feed item 102 - FeedItem tFI102 = tFI_isDownloading(102, mockDownloadRequester); - doAddToQueueAndAssertResult(message + " (2nd download, it should preserve order of download)", - calculator, 0, tFI102, queue, - idsExpectedAfter102); - - // Items 201 and 202 are added as part of a single DBWriter.addQueueItem() calls - - FeedItem tFI201 = tFI_isDownloading(201, mockDownloadRequester); - doAddToQueueAndAssertResult(message + " (bulk insertion, 1st item)", - calculator, 0, tFI201, queue, - idsExpectedAfter201); - - FeedItem tFI202 = tFI_isDownloading(202, mockDownloadRequester); - doAddToQueueAndAssertResult(message + " (bulk insertion, 2nd item)", - calculator, 1, tFI202, queue, - idsExpectedAfter202); - - // TODO: simulate download failure cases. - } - - - private static FeedItem tFI_isDownloading(int id, MockDownloadRequester requester) { - FeedItem item = tFI(id); - FeedMedia media = - new FeedMedia(item, "http://download.url.net/" + id - , 100000 + id, "audio/mp3"); - media.setId(item.getId()); - item.setMedia(media); - - requester.mockDownloadingFile(media, true); - - return item; - } - - private static class MockDownloadRequester implements FeedFileDownloadStatusRequesterInterface { - - private Map<Long, Boolean> downloadingByIds = new HashMap<>(); - - @Override - public synchronized boolean isDownloadingFile(@NonNull FeedFile item) { - return downloadingByIds.getOrDefault(item.getId(), false); - } - - // All other parent methods should not be called - - public void mockDownloadingFile(FeedFile item, boolean isDownloading) { - downloadingByIds.put(item.getId(), isDownloading); - } - } - } - - - // Common helpers: - // - common queue (of items) for tests - // - construct FeedItems for tests - - static void doAddToQueueAndAssertResult(String message, - ItemEnqueuePositionCalculator calculator, - int positionAmongAdd, - FeedItem itemToAdd, - List<FeedItem> queue, - List<Long> idsExpected) { - int posActual = calculator.calcPosition(positionAmongAdd, itemToAdd, queue); - queue.add(posActual, itemToAdd); - assertEquals(message, idsExpected, toIDs(queue)); - } - - static final List<FeedItem> QUEUE_EMPTY = Collections.unmodifiableList(Arrays.asList()); - - static final List<FeedItem> QUEUE_DEFAULT = Collections.unmodifiableList(Arrays.asList(tFI(11), tFI(12), tFI(13), tFI(14))); - static final List<Long> QUEUE_DEFAULT_IDS = QUEUE_DEFAULT.stream().map(fi -> fi.getId()).collect(Collectors.toList()); - - - static FeedItem tFI(long id) { - return tFI(id, -1); - } - - static FeedItem tFI(long id, int position) { - FeedItem item = tFINoMedia(id); - FeedMedia media = new FeedMedia(item, "download_url", 1234567, "audio/mpeg"); - media.setId(item.getId()); - item.setMedia(media); - - if (position >= 0) { - media.setPosition(position); - } - - return item; - } - - static FeedItem tFINoMedia(long id) { - FeedItem item = new FeedItem(id, "Item" + id, "ItemId" + id, "url", - new Date(), FeedItem.PLAYED, FeedMother.anyFeed()); - return item; - } - - // Collections helpers - - static <T> List<? extends T> concat(T item, List<? extends T> list) { - List<T> res = new ArrayList<>(list); - res.add(0, item); - return res; - } - - static <T> List<? extends T> concat(List<? extends T> list, T item) { - List<T> res = new ArrayList<>(list); - res.add(item); - return res; - } - - static <T> List<? extends T> concat(List<? extends T> list1, List<? extends T> list2) { - List<T> res = new ArrayList<>(list1); - res.addAll(list2); - return res; - } - - public static <T> List<T> list(T... a) { - return Arrays.asList(a); - } - - - static List<Long> toIDs(List<FeedItem> items) { - return items.stream().map(i->i.getId()).collect(Collectors.toList()); - } - - } - -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java new file mode 100644 index 000000000..1331df67d --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java @@ -0,0 +1,330 @@ +package de.danoeh.antennapod.core.storage; + +import androidx.annotation.NonNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import de.danoeh.antennapod.core.feed.FeedFile; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.FeedMother; +import de.danoeh.antennapod.core.storage.ItemEnqueuePositionCalculator.Options; + +import static org.junit.Assert.assertEquals; + +public class ItemEnqueuePositionCalculatorTest { + + @RunWith(Parameterized.class) + public static class IEPCBasicTest { + @Parameters(name = "{index}: case<{0}>, expected:{1}") + public static Iterable<Object[]> data() { + Options optDefault = new Options(); + Options optEnqAtFront = new Options().setEnqueueAtFront(true); + + return Arrays.asList(new Object[][]{ + {"case default, i.e., add to the end", + concat(QUEUE_DEFAULT_IDS, TFI_ID), + optDefault, 0, QUEUE_DEFAULT}, + {"case default (2nd item)", + concat(QUEUE_DEFAULT_IDS, TFI_ID), + optDefault, 1, QUEUE_DEFAULT}, + {"case option enqueue at front", + concat(TFI_ID, QUEUE_DEFAULT_IDS), + optEnqAtFront, 0, QUEUE_DEFAULT}, + {"case option enqueue at front (2nd item)", + list(11L, TFI_ID, 12L, 13L, 14L), + optEnqAtFront, 1, QUEUE_DEFAULT}, + {"case empty queue, option default", + list(TFI_ID), + optDefault, 0, QUEUE_EMPTY}, + {"case empty queue, option enqueue at front", + list(TFI_ID), + optEnqAtFront, 0, QUEUE_EMPTY}, + }); + } + + @Parameter + public String message; + + @Parameter(1) + public List<Long> idsExpected; + + @Parameter(2) + public Options options; + + @Parameter(3) + public int posAmongAdded; // the position of feed item to be inserted among the list to be inserted. + + @Parameter(4) + public List<FeedItem> curQueue; + + + public static final long TFI_ID = 101; + + /** + * Add a FeedItem with ID {@link #TFI_ID} with the setup + */ + @Test + public void test() { + ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options); + + // shallow copy to which the test will add items + List<FeedItem> queue = new ArrayList<>(curQueue); + FeedItem tFI = tFI(TFI_ID); + doAddToQueueAndAssertResult(message, + calculator, posAmongAdded, tFI, queue, + idsExpected); + } + + } + + @RunWith(Parameterized.class) + public static class IEPCKeepInProgressAtFrontTest extends IEPCBasicTest { + @Parameters(name = "{index}: case<{0}>, expected:{1}") + public static Iterable<Object[]> data() { + Options optKeepInProgressAtFront = + new Options().setEnqueueAtFront(true).setKeepInProgressAtFront(true); + // edge case: keep in progress without enabling enqueue at front is meaningless + Options optKeepInProgressAtFrontWithNoEnqueueAtFront = + new Options().setKeepInProgressAtFront(true); + + return Arrays.asList(new Object[][]{ + {"case option keep in progress at front", + list(11L, TFI_ID, 12L, 13L), + optKeepInProgressAtFront, 0, QUEUE_FRONT_IN_PROGRESS}, + {"case option keep in progress at front (2nd item)", + list(11L, 12L, TFI_ID, 13L), + optKeepInProgressAtFront, 1, QUEUE_FRONT_IN_PROGRESS}, + {"case option keep in progress at front, front item not in progress", + concat(TFI_ID, QUEUE_DEFAULT_IDS), + optKeepInProgressAtFront, 0, QUEUE_DEFAULT}, + {"case option keep in progress at front, front item no media at all", + concat(TFI_ID, QUEUE_FRONT_NO_MEDIA_IDS), + optKeepInProgressAtFront, 0, QUEUE_FRONT_NO_MEDIA}, // No media should not cause any exception + {"case option keep in progress at front, but enqueue at front is disabled", + concat(QUEUE_FRONT_IN_PROGRESS_IDS, TFI_ID), + optKeepInProgressAtFrontWithNoEnqueueAtFront, 0, QUEUE_FRONT_IN_PROGRESS}, + {"case empty queue, option keep in progress at front", + list(TFI_ID), + optKeepInProgressAtFront, 0, QUEUE_EMPTY}, + }); + } + + private static final List<FeedItem> QUEUE_FRONT_IN_PROGRESS = Arrays.asList(tFI(11, 60000), tFI(12), tFI(13)); + private static final List<Long> QUEUE_FRONT_IN_PROGRESS_IDS = toIDs(QUEUE_FRONT_IN_PROGRESS); + + private static final List<FeedItem> QUEUE_FRONT_NO_MEDIA = Arrays.asList(tFINoMedia(11), tFI(12), tFI(13)); + private static final List<Long> QUEUE_FRONT_NO_MEDIA_IDS = toIDs(QUEUE_FRONT_NO_MEDIA); + + } + + @RunWith(Parameterized.class) + public static class ItemEnqueuePositionCalculatorPreserveDownloadOrderTest { + + @Parameters(name = "{index}: case<{0}>") + public static Iterable<Object[]> data() { + Options optDefault = new Options(); + Options optEnqAtFront = new Options().setEnqueueAtFront(true); + + // Attempts to make test more readable by showing the expected list of ids + // (rather than the expected positions) + return Arrays.asList(new Object[][] { + {"download order test, enqueue default", + concat(QUEUE_DEFAULT_IDS, 101L), + concat(QUEUE_DEFAULT_IDS, list(101L, 102L)), + concat(QUEUE_DEFAULT_IDS, list(101L, 102L, 201L)), + concat(QUEUE_DEFAULT_IDS, list(101L, 102L, 201L, 202L)), + optDefault, QUEUE_DEFAULT}, + {"download order test, enqueue at front", + concat(101L, QUEUE_DEFAULT_IDS), + concat(list(101L, 102L), QUEUE_DEFAULT_IDS), + concat(list(101L, 102L, 201L), QUEUE_DEFAULT_IDS), + concat(list(101L, 102L, 201L, 202L), QUEUE_DEFAULT_IDS), + optEnqAtFront, QUEUE_DEFAULT}, + }); + } + + @Parameter + public String message; + + @Parameter(1) + public List<Long> idsExpectedAfter101; + + @Parameter(2) + public List<Long> idsExpectedAfter102; + + // 2XX are for testing bulk insertion cases + @Parameter(3) + public List<Long> idsExpectedAfter201; + + @Parameter(4) + public List<Long> idsExpectedAfter202; + + @Parameter(5) + public Options options; + + @Parameter(6) + public List<FeedItem> queueInitial; + + @Test + public void testQueueOrderWhenDownloading2Items() { + + // Setup class under test + // + ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options); + MockDownloadRequester mockDownloadRequester = new MockDownloadRequester(); + calculator.requester = mockDownloadRequester; + + // Setup initial data + // A shallow copy, as the test code will manipulate the queue + List<FeedItem> queue = new ArrayList<>(queueInitial); + + + // Test body + + // User clicks download on feed item 101 + FeedItem tFI101 = tFI_isDownloading(101, mockDownloadRequester); + doAddToQueueAndAssertResult(message + " (1st download)", + calculator, 0, tFI101, queue, + idsExpectedAfter101); + + // Then user clicks download on feed item 102 + FeedItem tFI102 = tFI_isDownloading(102, mockDownloadRequester); + doAddToQueueAndAssertResult(message + " (2nd download, it should preserve order of download)", + calculator, 0, tFI102, queue, + idsExpectedAfter102); + + // Items 201 and 202 are added as part of a single DBWriter.addQueueItem() calls + + FeedItem tFI201 = tFI_isDownloading(201, mockDownloadRequester); + doAddToQueueAndAssertResult(message + " (bulk insertion, 1st item)", + calculator, 0, tFI201, queue, + idsExpectedAfter201); + + FeedItem tFI202 = tFI_isDownloading(202, mockDownloadRequester); + doAddToQueueAndAssertResult(message + " (bulk insertion, 2nd item)", + calculator, 1, tFI202, queue, + idsExpectedAfter202); + + // TODO: simulate download failure cases. + } + + + private static FeedItem tFI_isDownloading(int id, MockDownloadRequester requester) { + FeedItem item = tFI(id); + FeedMedia media = + new FeedMedia(item, "http://download.url.net/" + id + , 100000 + id, "audio/mp3"); + media.setId(item.getId()); + item.setMedia(media); + + requester.mockDownloadingFile(media, true); + + return item; + } + + private static class MockDownloadRequester implements FeedFileDownloadStatusRequesterInterface { + + private Map<Long, Boolean> downloadingByIds = new HashMap<>(); + + @Override + public synchronized boolean isDownloadingFile(@NonNull FeedFile item) { + return downloadingByIds.getOrDefault(item.getId(), false); + } + + // All other parent methods should not be called + + public void mockDownloadingFile(FeedFile item, boolean isDownloading) { + downloadingByIds.put(item.getId(), isDownloading); + } + } + } + + + // Common helpers: + // - common queue (of items) for tests + // - construct FeedItems for tests + + static void doAddToQueueAndAssertResult(String message, + ItemEnqueuePositionCalculator calculator, + int positionAmongAdd, + FeedItem itemToAdd, + List<FeedItem> queue, + List<Long> idsExpected) { + int posActual = calculator.calcPosition(positionAmongAdd, itemToAdd, queue); + queue.add(posActual, itemToAdd); + assertEquals(message, idsExpected, toIDs(queue)); + } + + static final List<FeedItem> QUEUE_EMPTY = Collections.unmodifiableList(Arrays.asList()); + + static final List<FeedItem> QUEUE_DEFAULT = Collections.unmodifiableList(Arrays.asList(tFI(11), tFI(12), tFI(13), tFI(14))); + static final List<Long> QUEUE_DEFAULT_IDS = QUEUE_DEFAULT.stream().map(fi -> fi.getId()).collect(Collectors.toList()); + + + static FeedItem tFI(long id) { + return tFI(id, -1); + } + + static FeedItem tFI(long id, int position) { + FeedItem item = tFINoMedia(id); + FeedMedia media = new FeedMedia(item, "download_url", 1234567, "audio/mpeg"); + media.setId(item.getId()); + item.setMedia(media); + + if (position >= 0) { + media.setPosition(position); + } + + return item; + } + + static FeedItem tFINoMedia(long id) { + FeedItem item = new FeedItem(id, "Item" + id, "ItemId" + id, "url", + new Date(), FeedItem.PLAYED, FeedMother.anyFeed()); + return item; + } + + // Collections helpers + + static <T> List<? extends T> concat(T item, List<? extends T> list) { + List<T> res = new ArrayList<>(list); + res.add(0, item); + return res; + } + + static <T> List<? extends T> concat(List<? extends T> list, T item) { + List<T> res = new ArrayList<>(list); + res.add(item); + return res; + } + + static <T> List<? extends T> concat(List<? extends T> list1, List<? extends T> list2) { + List<T> res = new ArrayList<>(list1); + res.addAll(list2); + return res; + } + + static <T> List<T> list(T... a) { + return Arrays.asList(a); + } + + + static List<Long> toIDs(List<FeedItem> items) { + return items.stream().map(i->i.getId()).collect(Collectors.toList()); + } + +} |