Merge pull request #2714 from orionlee/enqueue_keep_inprogress_front_2652_respect_download_start_order_2448
Enqueue fixes: keep inprogress front, respect download start order
This commit is contained in:
commit
808f273c09
|
@ -0,0 +1,168 @@
|
|||
package de.test.antennapod.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
import org.awaitility.core.ConditionTimeoutException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.danoeh.antennapod.core.ClientConfig;
|
||||
import de.danoeh.antennapod.core.DBTasksCallbacks;
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackService;
|
||||
import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm;
|
||||
import de.danoeh.antennapod.core.storage.DBReader;
|
||||
import de.danoeh.antennapod.core.storage.DBTasks;
|
||||
import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm;
|
||||
import de.danoeh.antennapod.core.storage.PodDBAdapter;
|
||||
import de.danoeh.antennapod.core.util.playback.Playable;
|
||||
import de.test.antennapod.ui.UITestUtils;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class AutoDownloadTest {
|
||||
|
||||
private Context context;
|
||||
private UITestUtils stubFeedsServer;
|
||||
|
||||
private DBTasksCallbacks dbTasksCallbacksOrig;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
stubFeedsServer = new UITestUtils(context);;
|
||||
stubFeedsServer.setup();
|
||||
|
||||
dbTasksCallbacksOrig = ClientConfig.dbTasksCallbacks;
|
||||
|
||||
// create new database
|
||||
PodDBAdapter.init(context);
|
||||
PodDBAdapter.deleteDatabase();
|
||||
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||
adapter.open();
|
||||
adapter.close();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
stubFeedsServer.tearDown();
|
||||
|
||||
context.sendBroadcast(new Intent(PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
|
||||
Awaitility.await().until(() -> !PlaybackService.isRunning);
|
||||
|
||||
ClientConfig.dbTasksCallbacks = dbTasksCallbacksOrig;
|
||||
}
|
||||
|
||||
/**
|
||||
* A cross-functional test, ensuring playback's behavior works with Auto Download in boundary condition.
|
||||
*
|
||||
* Scenario:
|
||||
* - For setting enqueue location AFTER_CURRENTLY_PLAYING
|
||||
* - when playback of an episode is complete and the app advances to the next episode (continuous playback on)
|
||||
* - when automatic download kicks in,
|
||||
* - ensure the next episode is the current playing one, needed for AFTER_CURRENTLY_PLAYING enqueue location.
|
||||
*/
|
||||
@Test
|
||||
public void downloadsEnqueuedToAfterCurrent_CurrentAdvancedToNextOnPlaybackComplete() throws Exception {
|
||||
UserPreferences.setFollowQueue(true); // continuous playback
|
||||
|
||||
// Setup: feeds and queue
|
||||
// downloads 3 of them, leave some in new state (auto-downloadable)
|
||||
stubFeedsServer.addLocalFeedData(false);
|
||||
List<FeedItem> queue = DBReader.getQueue();
|
||||
assertTrue(queue.size() > 1);
|
||||
FeedItem item0 = queue.get(0);
|
||||
FeedItem item1 = queue.get(1);
|
||||
|
||||
// Setup: enable automatic download
|
||||
// it is not needed, as the actual automatic download is stubbed.
|
||||
StubDownloadAlgorithm stubDownloadAlgorithm = new StubDownloadAlgorithm();
|
||||
useDownloadAlgorithm(stubDownloadAlgorithm);
|
||||
|
||||
// Actual test
|
||||
// Play the first one in the queue
|
||||
playEpisode(item0);
|
||||
|
||||
try {
|
||||
// when playback is complete, advances to the next one, and auto download kicks in,
|
||||
// ensure that currently playing has been advanced to the next one by this point.
|
||||
Awaitility.await("advanced to the next episode")
|
||||
.atMost(6000, MILLISECONDS) // the test mp3 media is 3-second long. twice should be enough
|
||||
.until(() -> item1.equals(stubDownloadAlgorithm.getCurrentlyPlayingAtDownload()));
|
||||
} catch (ConditionTimeoutException cte) {
|
||||
FeedItem actual = stubDownloadAlgorithm.getCurrentlyPlayingAtDownload();
|
||||
fail("when auto download is triggered, the next episode should be playing: ("
|
||||
+ item1.getId() + ", " + item1.getTitle() + ") . "
|
||||
+ "Actual playing: ("
|
||||
+ (actual == null ? "" : actual.getId() + ", " + actual.getTitle()) + ")"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void playEpisode(@NonNull FeedItem item) {
|
||||
FeedMedia media = item.getMedia();
|
||||
DBTasks.playMedia(context, media, false, true, true);
|
||||
Awaitility.await("episode is playing")
|
||||
.atMost(1000, MILLISECONDS)
|
||||
.until(() -> item.equals(getCurrentlyPlaying()));
|
||||
}
|
||||
|
||||
private FeedItem getCurrentlyPlaying() {
|
||||
Playable playable = Playable.PlayableUtils.createInstanceFromPreferences(context);
|
||||
if (playable == null) {
|
||||
return null;
|
||||
}
|
||||
return ((FeedMedia)playable).getItem();
|
||||
}
|
||||
|
||||
private void useDownloadAlgorithm(final AutomaticDownloadAlgorithm downloadAlgorithm) {
|
||||
DBTasksCallbacks dbTasksCallbacksStub = new DBTasksCallbacks() {
|
||||
@Override
|
||||
public AutomaticDownloadAlgorithm getAutomaticDownloadAlgorithm() {
|
||||
return downloadAlgorithm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpisodeCleanupAlgorithm getEpisodeCacheCleanupAlgorithm() {
|
||||
return dbTasksCallbacksOrig.getEpisodeCacheCleanupAlgorithm();
|
||||
}
|
||||
};
|
||||
ClientConfig.dbTasksCallbacks = dbTasksCallbacksStub;
|
||||
}
|
||||
|
||||
private class StubDownloadAlgorithm implements AutomaticDownloadAlgorithm {
|
||||
@Nullable
|
||||
private FeedItem currentlyPlaying;
|
||||
|
||||
@Override
|
||||
public Runnable autoDownloadUndownloadedItems(Context context) {
|
||||
return () -> {
|
||||
if (currentlyPlaying == null) {
|
||||
currentlyPlaying = getCurrentlyPlaying();
|
||||
} else {
|
||||
throw new AssertionError("Stub automatic download should be invoked once and only once");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
FeedItem getCurrentlyPlayingAtDownload() {
|
||||
return currentlyPlaying;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,26 +2,31 @@ package de.test.antennapod.storage;
|
|||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.filters.SmallTest;
|
||||
import de.danoeh.antennapod.core.feed.Feed;
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.storage.DBReader;
|
||||
import de.danoeh.antennapod.core.storage.DBTasks;
|
||||
import de.danoeh.antennapod.core.storage.DBWriter;
|
||||
import de.danoeh.antennapod.core.storage.PodDBAdapter;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static de.danoeh.antennapod.core.util.FeedItemUtil.getIdList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
@ -179,4 +184,82 @@ public class DBTasksTest {
|
|||
lastDate = item.getPubDate();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddQueueItemsInDownload_EnqueueEnabled() throws Exception {
|
||||
// Setup test data / environment
|
||||
UserPreferences.setEnqueueDownloadedEpisodes(true);
|
||||
UserPreferences.setEnqueueLocation(UserPreferences.EnqueueLocation.BACK);
|
||||
|
||||
List<FeedItem> fis1 = createSavedFeed("Feed 1", 2).getItems();
|
||||
List<FeedItem> fis2 = createSavedFeed("Feed 2", 3).getItems();
|
||||
|
||||
DBWriter.addQueueItem(context, fis1.get(0), fis2.get(0)).get();
|
||||
// the first item fis1.get(0) is already in the queue
|
||||
FeedItem[] itemsToDownload = new FeedItem[]{ fis1.get(0), fis1.get(1), fis2.get(2), fis2.get(1) };
|
||||
|
||||
// Expectations:
|
||||
List<FeedItem> expectedEnqueued = Arrays.asList(fis1.get(1), fis2.get(2), fis2.get(1));
|
||||
List<FeedItem> expectedQueue = new ArrayList<>();
|
||||
expectedQueue.addAll(DBReader.getQueue());
|
||||
expectedQueue.addAll(expectedEnqueued);
|
||||
|
||||
// Run actual test and assert results
|
||||
List<? extends FeedItem> actualEnqueued =
|
||||
DBTasks.enqueueFeedItemsToDownload(context, itemsToDownload);
|
||||
|
||||
assertEqualsByIds("Only items not in the queue are enqueued", expectedEnqueued, actualEnqueued);
|
||||
assertEqualsByIds("Queue has new items appended", expectedQueue, DBReader.getQueue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddQueueItemsInDownload_EnqueueDisabled() throws Exception {
|
||||
// Setup test data / environment
|
||||
UserPreferences.setEnqueueDownloadedEpisodes(false);
|
||||
|
||||
List<FeedItem> fis1 = createSavedFeed("Feed 1", 2).getItems();
|
||||
List<FeedItem> fis2 = createSavedFeed("Feed 2", 3).getItems();
|
||||
|
||||
DBWriter.addQueueItem(context, fis1.get(0), fis2.get(0)).get();
|
||||
FeedItem[] itemsToDownload = new FeedItem[]{ fis1.get(0), fis1.get(1), fis2.get(2), fis2.get(1) };
|
||||
|
||||
// Expectations:
|
||||
List<FeedItem> expectedEnqueued = Collections.emptyList();
|
||||
List<FeedItem> expectedQueue = DBReader.getQueue();
|
||||
|
||||
// Run actual test and assert results
|
||||
List<? extends FeedItem> actualEnqueued =
|
||||
DBTasks.enqueueFeedItemsToDownload(context, itemsToDownload);
|
||||
|
||||
assertEqualsByIds("No item is enqueued", expectedEnqueued, actualEnqueued);
|
||||
assertEqualsByIds("Queue is unchanged", expectedQueue, DBReader.getQueue());
|
||||
}
|
||||
|
||||
private void assertEqualsByIds(String msg, List<? extends FeedItem> expected, List<? extends FeedItem> actual) {
|
||||
// assert only the IDs, so that any differences are easily to spot.
|
||||
List<Long> expectedIds = getIdList(expected);
|
||||
List<Long> actualIds = getIdList(actual);
|
||||
assertEquals(msg, expectedIds, actualIds);
|
||||
}
|
||||
|
||||
private Feed createSavedFeed(String title, int numFeedItems) {
|
||||
final Feed feed = new Feed("url", null, title);
|
||||
|
||||
if (numFeedItems > 0) {
|
||||
List<FeedItem> items = new ArrayList<>(numFeedItems);
|
||||
for (int i = 1; i <= numFeedItems; i++) {
|
||||
FeedItem item = new FeedItem(0, "item " + i + " of " + title, "id", "link",
|
||||
new Date(), FeedItem.UNPLAYED, feed);
|
||||
items.add(item);
|
||||
}
|
||||
feed.setItems(items);
|
||||
}
|
||||
|
||||
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||
adapter.open();
|
||||
adapter.setCompleteFeed(feed);
|
||||
adapter.close();
|
||||
return feed;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,12 +4,15 @@ import android.content.Context;
|
|||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.MediumTest;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
@ -29,9 +32,7 @@ import de.danoeh.antennapod.core.storage.DBReader;
|
|||
import de.danoeh.antennapod.core.storage.DBWriter;
|
||||
import de.danoeh.antennapod.core.storage.PodDBAdapter;
|
||||
import de.danoeh.antennapod.core.util.Consumer;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import de.danoeh.antennapod.core.util.FeedItemUtil;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
@ -475,11 +476,12 @@ public class DBWriterTest {
|
|||
assertFalse(OLD_DATE == media.getPlaybackCompletionDate().getTime());
|
||||
}
|
||||
|
||||
private Feed queueTestSetupMultipleItems(final int NUM_ITEMS) throws InterruptedException, ExecutionException, TimeoutException {
|
||||
private Feed queueTestSetupMultipleItems(final int numItems) throws InterruptedException, ExecutionException, TimeoutException {
|
||||
final Context context = getInstrumentation().getTargetContext();
|
||||
UserPreferences.setEnqueueLocation(UserPreferences.EnqueueLocation.BACK);
|
||||
Feed feed = new Feed("url", null, "title");
|
||||
feed.setItems(new ArrayList<>());
|
||||
for (int i = 0; i < NUM_ITEMS; i++) {
|
||||
for (int i = 0; i < numItems; i++) {
|
||||
FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.PLAYED, feed);
|
||||
feed.getItems().add(item);
|
||||
}
|
||||
|
@ -573,12 +575,16 @@ public class DBWriterTest {
|
|||
Cursor cursor = adapter.getQueueIDCursor();
|
||||
assertTrue(cursor.moveToFirst());
|
||||
assertTrue(cursor.getCount() == NUM_ITEMS);
|
||||
List<Long> expectedIds = FeedItemUtil.getIdList(feed.getItems());
|
||||
List<Long> actualIds = new ArrayList<>();
|
||||
for (int i = 0; i < NUM_ITEMS; i++) {
|
||||
assertTrue(cursor.moveToPosition(i));
|
||||
assertTrue(cursor.getLong(0) == feed.getItems().get(i).getId());
|
||||
actualIds.add(cursor.getLong(0));
|
||||
}
|
||||
cursor.close();
|
||||
adapter.close();
|
||||
assertEquals("Bulk add to queue: result order should be the same as the order given",
|
||||
expectedIds, actualIds);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -4,13 +4,14 @@ import android.content.Intent;
|
|||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import com.robotium.solo.Solo;
|
||||
import com.robotium.solo.Timeout;
|
||||
|
||||
import de.test.antennapod.EspressoTestUtils;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
@ -21,6 +22,7 @@ import java.util.concurrent.TimeUnit;
|
|||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.activity.PreferenceActivity;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation;
|
||||
import de.danoeh.antennapod.core.storage.APCleanupAlgorithm;
|
||||
import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm;
|
||||
import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm;
|
||||
|
@ -28,6 +30,7 @@ import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm;
|
|||
import de.danoeh.antennapod.fragment.EpisodesFragment;
|
||||
import de.danoeh.antennapod.fragment.QueueFragment;
|
||||
import de.danoeh.antennapod.fragment.SubscriptionFragment;
|
||||
import de.test.antennapod.EspressoTestUtils;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
|
@ -126,13 +129,19 @@ public class PreferencesTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testEnqueueAtFront() {
|
||||
public void testEnqueueLocation() {
|
||||
clickPreference(R.string.playback_pref);
|
||||
final boolean enqueueAtFront = UserPreferences.enqueueAtFront();
|
||||
clickPreference(R.string.pref_queueAddToFront_title);
|
||||
assertTrue(solo.waitForCondition(() -> enqueueAtFront != UserPreferences.enqueueAtFront(), Timeout.getLargeTimeout()));
|
||||
clickPreference(R.string.pref_queueAddToFront_title);
|
||||
assertTrue(solo.waitForCondition(() -> enqueueAtFront == UserPreferences.enqueueAtFront(), Timeout.getLargeTimeout()));
|
||||
doTestEnqueueLocation(R.string.enqueue_location_after_current, EnqueueLocation.AFTER_CURRENTLY_PLAYING);
|
||||
doTestEnqueueLocation(R.string.enqueue_location_front, EnqueueLocation.FRONT);
|
||||
doTestEnqueueLocation(R.string.enqueue_location_back, EnqueueLocation.BACK);
|
||||
}
|
||||
|
||||
private void doTestEnqueueLocation(@StringRes int optionResId, EnqueueLocation expected) {
|
||||
clickPreference(R.string.pref_enqueue_location_title);
|
||||
onView(withText(optionResId)).perform(click());
|
||||
assertTrue(solo.waitForCondition(
|
||||
() -> expected == UserPreferences.getEnqueueLocation(),
|
||||
Timeout.getLargeTimeout()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -4,8 +4,15 @@ import android.app.Activity;
|
|||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.collection.ArrayMap;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.activity.MediaplayerActivity;
|
||||
import de.danoeh.antennapod.activity.PreferenceActivity;
|
||||
|
@ -63,6 +70,43 @@ public class PlaybackPreferencesFragment extends PreferenceFragmentCompat {
|
|||
behaviour.setEntries(R.array.video_background_behavior_options_without_pip);
|
||||
behaviour.setEntryValues(R.array.video_background_behavior_values_without_pip);
|
||||
}
|
||||
|
||||
buildEnqueueLocationPreference();
|
||||
}
|
||||
|
||||
private void buildEnqueueLocationPreference() {
|
||||
final Resources res = requireActivity().getResources();
|
||||
final Map<String, String> options = new ArrayMap<>();
|
||||
{
|
||||
String[] keys = res.getStringArray(R.array.enqueue_location_values);
|
||||
String[] values = res.getStringArray(R.array.enqueue_location_options);
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
options.put(keys[i], values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
ListPreference pref = requirePreference(UserPreferences.PREF_ENQUEUE_LOCATION);
|
||||
pref.setSummary(res.getString(R.string.pref_enqueue_location_sum, options.get(pref.getValue())));
|
||||
|
||||
pref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if (!(newValue instanceof String)) {
|
||||
return false;
|
||||
}
|
||||
String newValStr = (String)newValue;
|
||||
pref.setSummary(res.getString(R.string.pref_enqueue_location_sum, options.get(newValStr)));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private <T extends Preference> T requirePreference(@NonNull CharSequence key) {
|
||||
// Possibly put it to a common method in abstract base class
|
||||
T result = findPreference(key);
|
||||
if (result == null) {
|
||||
throw new IllegalArgumentException("Preference with key '" + key + "' is not found");
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void buildSmartMarkAsPlayedPreference() {
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.preference.PreferenceManager;
|
|||
import de.danoeh.antennapod.BuildConfig;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation;
|
||||
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
|
||||
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
|
||||
|
||||
|
@ -75,6 +76,13 @@ public class PreferenceUpgrader {
|
|||
}
|
||||
|
||||
UserPreferences.setQueueLocked(false);
|
||||
|
||||
if (!prefs.contains(UserPreferences.PREF_ENQUEUE_LOCATION)) {
|
||||
final String keyOldPrefEnqueueFront = "prefQueueAddToFront";
|
||||
boolean enqueueAtFront = prefs.getBoolean(keyOldPrefEnqueueFront, false);
|
||||
EnqueueLocation enqueueLocation = enqueueAtFront ? EnqueueLocation.FRONT : EnqueueLocation.BACK;
|
||||
UserPreferences.setEnqueueLocation(enqueueLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,12 +90,13 @@
|
|||
android:key="prefEnqueueDownloaded"
|
||||
android:summary="@string/pref_enqueue_downloaded_summary"
|
||||
android:title="@string/pref_enqueue_downloaded_title" />
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:enabled="true"
|
||||
android:key="prefQueueAddToFront"
|
||||
android:summary="@string/pref_queueAddToFront_sum"
|
||||
android:title="@string/pref_queueAddToFront_title"/>
|
||||
<ListPreference
|
||||
android:defaultValue="BACK"
|
||||
android:entries="@array/enqueue_location_options"
|
||||
android:entryValues="@array/enqueue_location_values"
|
||||
android:key="prefEnqueueLocation"
|
||||
android:title="@string/pref_enqueue_location_title"
|
||||
app:useStockLayout="true"/>
|
||||
<SwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
|
|
|
@ -46,7 +46,7 @@ project.ext {
|
|||
|
||||
workManagerVersion = "1.0.1"
|
||||
espressoVersion = "3.2.0"
|
||||
awaitilityVersion = "3.1.2"
|
||||
awaitilityVersion = "3.1.6"
|
||||
commonsioVersion = "2.5"
|
||||
commonslangVersion = "3.6"
|
||||
commonstextVersion = "1.3"
|
||||
|
|
|
@ -4,12 +4,14 @@ import android.content.Context;
|
|||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
|
@ -23,8 +25,8 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import de.danoeh.antennapod.core.feed.MediaType;
|
||||
import de.danoeh.antennapod.core.R;
|
||||
import de.danoeh.antennapod.core.feed.MediaType;
|
||||
import de.danoeh.antennapod.core.service.download.ProxyConfig;
|
||||
import de.danoeh.antennapod.core.storage.APCleanupAlgorithm;
|
||||
import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm;
|
||||
|
@ -61,8 +63,6 @@ public class UserPreferences {
|
|||
public static final String PREF_BACK_BUTTON_BEHAVIOR = "prefBackButtonBehavior";
|
||||
private static final String PREF_BACK_BUTTON_GO_TO_PAGE = "prefBackButtonGoToPage";
|
||||
|
||||
// Queue
|
||||
private static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront";
|
||||
public static final String PREF_QUEUE_KEEP_SORTED = "prefQueueKeepSorted";
|
||||
public static final String PREF_QUEUE_KEEP_SORTED_ORDER = "prefQueueKeepSortedOrder";
|
||||
|
||||
|
@ -86,6 +86,7 @@ public class UserPreferences {
|
|||
|
||||
// Network
|
||||
private static final String PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded";
|
||||
public static final String PREF_ENQUEUE_LOCATION = "prefEnqueueLocation";
|
||||
public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall";
|
||||
private static final String PREF_MOBILE_UPDATE = "prefMobileUpdateTypes";
|
||||
public static final String PREF_EPISODE_CLEANUP = "prefEpisodeCleanup";
|
||||
|
@ -284,8 +285,33 @@ public class UserPreferences {
|
|||
return prefs.getBoolean(PREF_ENQUEUE_DOWNLOADED, true);
|
||||
}
|
||||
|
||||
public static boolean enqueueAtFront() {
|
||||
return prefs.getBoolean(PREF_QUEUE_ADD_TO_FRONT, false);
|
||||
@VisibleForTesting
|
||||
public static void setEnqueueDownloadedEpisodes(boolean enqueueDownloadedEpisodes) {
|
||||
prefs.edit()
|
||||
.putBoolean(PREF_ENQUEUE_DOWNLOADED, enqueueDownloadedEpisodes)
|
||||
.apply();
|
||||
}
|
||||
|
||||
public enum EnqueueLocation {
|
||||
BACK, FRONT, AFTER_CURRENTLY_PLAYING;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static EnqueueLocation getEnqueueLocation() {
|
||||
String valStr = prefs.getString(PREF_ENQUEUE_LOCATION, EnqueueLocation.BACK.name());
|
||||
try {
|
||||
return EnqueueLocation.valueOf(valStr);
|
||||
} catch (Throwable t) {
|
||||
// should never happen but just in case
|
||||
Log.e(TAG, "getEnqueueLocation: invalid value '" + valStr + "' Use default.", t);
|
||||
return EnqueueLocation.BACK;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setEnqueueLocation(@NonNull EnqueueLocation location) {
|
||||
prefs.edit()
|
||||
.putString(PREF_ENQUEUE_LOCATION, location.name())
|
||||
.apply();
|
||||
}
|
||||
|
||||
public static boolean isPauseOnHeadsetDisconnect() {
|
||||
|
@ -313,6 +339,14 @@ public class UserPreferences {
|
|||
return prefs.getBoolean(PREF_FOLLOW_QUEUE, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true to enable Continuous Playback
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static void setFollowQueue(boolean value) {
|
||||
prefs.edit().putBoolean(UserPreferences.PREF_FOLLOW_QUEUE, value).apply();
|
||||
}
|
||||
|
||||
public static boolean shouldSkipKeepEpisode() { return prefs.getBoolean(PREF_SKIP_KEEPS_EPISODE, true); }
|
||||
|
||||
public static boolean shouldFavoriteKeepEpisode() {
|
||||
|
|
|
@ -3,22 +3,22 @@ package de.danoeh.antennapod.core.service.download.handler;
|
|||
import android.content.Context;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
|
||||
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.service.download.DownloadRequest;
|
||||
import de.danoeh.antennapod.core.service.download.DownloadStatus;
|
||||
import de.danoeh.antennapod.core.storage.DBReader;
|
||||
import de.danoeh.antennapod.core.storage.DBTasks;
|
||||
import de.danoeh.antennapod.core.storage.DBWriter;
|
||||
import de.danoeh.antennapod.core.util.ChapterUtils;
|
||||
import de.danoeh.antennapod.core.util.DownloadError;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Handles a completed media download.
|
||||
*/
|
||||
|
@ -82,11 +82,6 @@ public class MediaDownloadedHandler implements Runnable {
|
|||
// to ensure subscribers will get the updated FeedMedia as well
|
||||
DBWriter.setFeedItem(item).get();
|
||||
}
|
||||
|
||||
if (item != null && UserPreferences.enqueueDownloadedEpisodes()
|
||||
&& !DBTasks.isInQueue(context, item.getId())) {
|
||||
DBWriter.addQueueItem(context, item).get();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "MediaHandlerThread was interrupted");
|
||||
} catch (ExecutionException e) {
|
||||
|
|
|
@ -7,6 +7,8 @@ import android.os.Looper;
|
|||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
@ -26,6 +28,7 @@ import de.danoeh.antennapod.core.feed.Feed;
|
|||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.feed.FeedPreferences;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.service.GpodnetSyncService;
|
||||
import de.danoeh.antennapod.core.service.download.DownloadStatus;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackService;
|
||||
|
@ -329,6 +332,15 @@ public final class DBTasks {
|
|||
|
||||
}.start();
|
||||
}
|
||||
// #2448: First, add to-download items to the queue before actual download
|
||||
// so that the resulting queue order is the same as when download is clicked
|
||||
try {
|
||||
enqueueFeedItemsToDownload(context, items);
|
||||
} catch (Throwable t) {
|
||||
throw new DownloadRequestException("Unexpected exception during enqueue before downloads", t);
|
||||
}
|
||||
|
||||
// Then, download them
|
||||
for (FeedItem item : items) {
|
||||
if (item.getMedia() != null
|
||||
&& !requester.isDownloadingFile(item.getMedia())
|
||||
|
@ -354,6 +366,25 @@ public final class DBTasks {
|
|||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static List<? extends FeedItem> enqueueFeedItemsToDownload(final Context context,
|
||||
FeedItem... items)
|
||||
throws InterruptedException, ExecutionException {
|
||||
List<FeedItem> itemsToEnqueue = new ArrayList<>();
|
||||
if (UserPreferences.enqueueDownloadedEpisodes()) {
|
||||
LongList queueIDList = DBReader.getQueueIDList();
|
||||
for (FeedItem item : items) {
|
||||
if (!queueIDList.contains(item.getId())) {
|
||||
itemsToEnqueue.add(item);
|
||||
}
|
||||
}
|
||||
DBWriter.addQueueItem(context,
|
||||
itemsToEnqueue.toArray(new FeedItem[0]))
|
||||
.get();
|
||||
}
|
||||
return itemsToEnqueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for undownloaded episodes in the queue or list of unread items and request a download if
|
||||
* 1. Network is available
|
||||
|
|
|
@ -7,10 +7,6 @@ import android.util.Log;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.danoeh.antennapod.core.event.DownloadLogEvent;
|
||||
import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
|
||||
import de.danoeh.antennapod.core.event.PlaybackHistoryEvent;
|
||||
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -25,10 +21,14 @@ import java.util.concurrent.Future;
|
|||
|
||||
import de.danoeh.antennapod.core.ClientConfig;
|
||||
import de.danoeh.antennapod.core.R;
|
||||
import de.danoeh.antennapod.core.event.DownloadLogEvent;
|
||||
import de.danoeh.antennapod.core.event.FavoritesEvent;
|
||||
import de.danoeh.antennapod.core.event.FeedItemEvent;
|
||||
import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
|
||||
import de.danoeh.antennapod.core.event.MessageEvent;
|
||||
import de.danoeh.antennapod.core.event.PlaybackHistoryEvent;
|
||||
import de.danoeh.antennapod.core.event.QueueEvent;
|
||||
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
|
||||
import de.danoeh.antennapod.core.feed.Feed;
|
||||
import de.danoeh.antennapod.core.feed.FeedEvent;
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
|
@ -45,6 +45,7 @@ import de.danoeh.antennapod.core.util.IntentUtils;
|
|||
import de.danoeh.antennapod.core.util.LongList;
|
||||
import de.danoeh.antennapod.core.util.Permutor;
|
||||
import de.danoeh.antennapod.core.util.SortOrder;
|
||||
import de.danoeh.antennapod.core.util.playback.Playable;
|
||||
|
||||
/**
|
||||
* Provides methods for writing data to AntennaPod's database.
|
||||
|
@ -314,37 +315,36 @@ public class DBWriter {
|
|||
public static Future<?> addQueueItem(final Context context, final boolean performAutoDownload,
|
||||
final long... itemIds) {
|
||||
return dbExec.submit(() -> {
|
||||
if (itemIds.length > 0) {
|
||||
if (itemIds.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||
adapter.open();
|
||||
final List<FeedItem> queue = DBReader.getQueue(adapter);
|
||||
|
||||
if (queue != null) {
|
||||
boolean queueModified = false;
|
||||
LongList markAsUnplayedIds = new LongList();
|
||||
List<QueueEvent> events = new ArrayList<>();
|
||||
List<FeedItem> updatedItems = new ArrayList<>();
|
||||
for (int i = 0; i < itemIds.length; i++) {
|
||||
if (!itemListContains(queue, itemIds[i])) {
|
||||
final FeedItem item = DBReader.getFeedItem(itemIds[i]);
|
||||
|
||||
|
||||
ItemEnqueuePositionCalculator positionCalculator =
|
||||
new ItemEnqueuePositionCalculator(UserPreferences.getEnqueueLocation());
|
||||
Playable currentlyPlaying = Playable.PlayableUtils.createInstanceFromPreferences(context);
|
||||
int insertPosition = positionCalculator.calcPosition(queue, currentlyPlaying);
|
||||
for (long itemId : itemIds) {
|
||||
if (!itemListContains(queue, itemId)) {
|
||||
final FeedItem item = DBReader.getFeedItem(itemId);
|
||||
if (item != null) {
|
||||
// add item to either front ot back of queue
|
||||
boolean addToFront = UserPreferences.enqueueAtFront();
|
||||
if (addToFront) {
|
||||
queue.add(i, item);
|
||||
events.add(QueueEvent.added(item, i));
|
||||
} else {
|
||||
queue.add(item);
|
||||
events.add(QueueEvent.added(item, queue.size() - 1));
|
||||
}
|
||||
queue.add(insertPosition, item);
|
||||
events.add(QueueEvent.added(item, insertPosition));
|
||||
|
||||
item.addTag(FeedItem.TAG_QUEUE);
|
||||
updatedItems.add(item);
|
||||
queueModified = true;
|
||||
if (item.isNew()) {
|
||||
markAsUnplayedIds.add(item.getId());
|
||||
}
|
||||
insertPosition++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -359,12 +359,10 @@ public class DBWriter {
|
|||
DBWriter.markItemPlayed(FeedItem.UNPLAYED, markAsUnplayedIds.toArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.close();
|
||||
if (performAutoDownload) {
|
||||
DBTasks.autodownloadUndownloadedItems(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -3,12 +3,13 @@ package de.danoeh.antennapod.core.storage;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -31,7 +32,7 @@ import de.danoeh.antennapod.core.util.URLChecker;
|
|||
* Sends download requests to the DownloadService. This class should always be used for starting downloads,
|
||||
* otherwise they won't work correctly.
|
||||
*/
|
||||
public class DownloadRequester {
|
||||
public class DownloadRequester implements DownloadStateProvider {
|
||||
private static final String TAG = "DownloadRequester";
|
||||
|
||||
private static final String FEED_DOWNLOADPATH = "cache/";
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package de.danoeh.antennapod.core.storage;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.danoeh.antennapod.core.feed.FeedFile;
|
||||
|
||||
/**
|
||||
* Allow callers to query the states of downloads, but not affect them.
|
||||
*/
|
||||
public interface DownloadStateProvider {
|
||||
/**
|
||||
* @return {@code true} if the named feedfile is in the downloads list
|
||||
*/
|
||||
boolean isDownloadingFile(@NonNull FeedFile item);
|
||||
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package de.danoeh.antennapod.core.storage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
import de.danoeh.antennapod.core.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation;
|
||||
import de.danoeh.antennapod.core.util.playback.Playable;
|
||||
|
||||
/**
|
||||
* @see DBWriter#addQueueItem(Context, boolean, long...) it uses the class to determine
|
||||
* the positions of the {@link FeedItem} in the queue.
|
||||
*/
|
||||
class ItemEnqueuePositionCalculator {
|
||||
|
||||
@NonNull
|
||||
private final EnqueueLocation enqueueLocation;
|
||||
|
||||
@VisibleForTesting
|
||||
DownloadStateProvider downloadStateProvider = DownloadRequester.getInstance();
|
||||
|
||||
public ItemEnqueuePositionCalculator(@NonNull EnqueueLocation enqueueLocation) {
|
||||
this.enqueueLocation = enqueueLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the position (0-based) that the item(s) should be inserted to the named queue.
|
||||
*
|
||||
* @param curQueue the queue to which the item is to be inserted
|
||||
* @param currentPlaying the currently playing media
|
||||
*/
|
||||
public int calcPosition(@NonNull List<FeedItem> curQueue, @Nullable Playable currentPlaying) {
|
||||
switch (enqueueLocation) {
|
||||
case BACK:
|
||||
return curQueue.size();
|
||||
case FRONT:
|
||||
// Return not necessarily 0, so that when a list of items are downloaded and enqueued
|
||||
// in succession of calls (e.g., users manually tapping download one by one),
|
||||
// the items enqueued are kept the same order.
|
||||
// Simply returning 0 will reverse the order.
|
||||
return getPositionOfFirstNonDownloadingItem(0, curQueue);
|
||||
case AFTER_CURRENTLY_PLAYING:
|
||||
int currentlyPlayingPosition = getCurrentlyPlayingPosition(curQueue, currentPlaying);
|
||||
return getPositionOfFirstNonDownloadingItem(
|
||||
currentlyPlayingPosition + 1, curQueue);
|
||||
default:
|
||||
throw new AssertionError("calcPosition() : unrecognized enqueueLocation option: " + enqueueLocation);
|
||||
}
|
||||
}
|
||||
|
||||
private int getPositionOfFirstNonDownloadingItem(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
|
||||
&& downloadStateProvider.isDownloadingFile(curItem.getMedia())) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getCurrentlyPlayingPosition(@NonNull List<FeedItem> curQueue,
|
||||
@Nullable Playable currentPlaying) {
|
||||
if (!(currentPlaying instanceof FeedMedia)) {
|
||||
return -1;
|
||||
}
|
||||
final long curPlayingItemId = ((FeedMedia) currentPlaying).getItem().getId();
|
||||
for (int i = 0; i < curQueue.size(); i++) {
|
||||
if (curPlayingItemId == curQueue.get(i).getId()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
package de.danoeh.antennapod.core.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.danoeh.antennapod.core.feed.FeedItem;
|
||||
|
@ -40,6 +43,15 @@ public class FeedItemUtil {
|
|||
return result;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static List<Long> getIdList(List<? extends FeedItem> items) {
|
||||
List<Long> result = new ArrayList<>();
|
||||
for (FeedItem item : items) {
|
||||
result.add(item.getId());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the link for the feed item for the purpose of Share. It fallbacks to
|
||||
* use the feed's link if the named feed item has no link.
|
||||
|
|
|
@ -93,6 +93,19 @@
|
|||
<item>@string/episode_cleanup_never</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="enqueue_location_options">
|
||||
<item>@string/enqueue_location_back</item>
|
||||
<item>@string/enqueue_location_front</item>
|
||||
<item>@string/enqueue_location_after_current</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="enqueue_location_values">
|
||||
<!-- MUST be the same as UserPreferences.EnqueueLocation enum -->
|
||||
<item>BACK</item>
|
||||
<item>FRONT</item>
|
||||
<item>AFTER_CURRENTLY_PLAYING</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="episode_cleanup_values">
|
||||
<item>-1</item>
|
||||
<item>0</item>
|
||||
|
|
|
@ -454,8 +454,11 @@
|
|||
<string name="pref_showDownloadReport_title">Show Download Report</string>
|
||||
<string name="pref_showDownloadReport_sum">If downloads fail, generate a report that shows the details of the failure.</string>
|
||||
<string name="pref_expand_notify_unsupport_toast">Android versions before 4.1 do not support expanded notifications.</string>
|
||||
<string name="pref_queueAddToFront_sum">Add new episodes to the front of the queue.</string>
|
||||
<string name="pref_queueAddToFront_title">Enqueue at Front</string>
|
||||
<string name="pref_enqueue_location_title">Enqueue Location</string>
|
||||
<string name="pref_enqueue_location_sum">Add episodes to: %1$s</string>
|
||||
<string name="enqueue_location_back">Back</string>
|
||||
<string name="enqueue_location_front">Front</string>
|
||||
<string name="enqueue_location_after_current">After current episode</string>
|
||||
<string name="pref_smart_mark_as_played_disabled">Disabled</string>
|
||||
<string name="pref_image_cache_size_title">Image Cache Size</string>
|
||||
<string name="pref_image_cache_size_sum">Size of the disk cache for images.</string>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package de.danoeh.antennapod.core.feed;
|
||||
|
||||
class FeedMother {
|
||||
public class FeedMother {
|
||||
public static final String IMAGE_URL = "http://example.com/image";
|
||||
|
||||
public static Feed anyFeed() {
|
||||
|
|
|
@ -0,0 +1,293 @@
|
|||
package de.danoeh.antennapod.core.storage;
|
||||
|
||||
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.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.feed.MediaType;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation;
|
||||
import de.danoeh.antennapod.core.util.playback.ExternalMedia;
|
||||
import de.danoeh.antennapod.core.util.playback.Playable;
|
||||
|
||||
import static de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation.AFTER_CURRENTLY_PLAYING;
|
||||
import static de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation.BACK;
|
||||
import static de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation.FRONT;
|
||||
import static de.danoeh.antennapod.core.util.CollectionTestUtil.concat;
|
||||
import static de.danoeh.antennapod.core.util.CollectionTestUtil.list;
|
||||
import static de.danoeh.antennapod.core.util.FeedItemUtil.getIdList;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.stub;
|
||||
|
||||
public class ItemEnqueuePositionCalculatorTest {
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class BasicTest {
|
||||
@Parameters(name = "{index}: case<{0}>, expected:{1}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"case default, i.e., add to the end",
|
||||
concat(QUEUE_DEFAULT_IDS, TFI_ID),
|
||||
BACK, QUEUE_DEFAULT},
|
||||
{"case option enqueue at front",
|
||||
concat(TFI_ID, QUEUE_DEFAULT_IDS),
|
||||
FRONT, QUEUE_DEFAULT},
|
||||
{"case empty queue, option default",
|
||||
list(TFI_ID),
|
||||
BACK, QUEUE_EMPTY},
|
||||
{"case empty queue, option enqueue at front",
|
||||
list(TFI_ID),
|
||||
FRONT, QUEUE_EMPTY},
|
||||
});
|
||||
}
|
||||
|
||||
@Parameter
|
||||
public String message;
|
||||
|
||||
@Parameter(1)
|
||||
public List<Long> idsExpected;
|
||||
|
||||
@Parameter(2)
|
||||
public EnqueueLocation options;
|
||||
|
||||
@Parameter(3)
|
||||
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 = createFeedItem(TFI_ID);
|
||||
doAddToQueueAndAssertResult(message,
|
||||
calculator, tFI, queue, getCurrentlyPlaying(),
|
||||
idsExpected);
|
||||
}
|
||||
|
||||
Playable getCurrentlyPlaying() { return null; }
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class AfterCurrentlyPlayingTest extends BasicTest {
|
||||
@Parameters(name = "{index}: case<{0}>, expected:{1}")
|
||||
public static Iterable<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"case option after currently playing",
|
||||
list(11L, TFI_ID, 12L, 13L, 14L),
|
||||
AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, 11L},
|
||||
{"case option after currently playing, currently playing in the middle of the queue",
|
||||
list(11L, 12L, 13L, TFI_ID, 14L),
|
||||
AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, 13L},
|
||||
{"case option after currently playing, currently playing is not in queue",
|
||||
concat(TFI_ID, QUEUE_DEFAULT_IDS),
|
||||
AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, 99L},
|
||||
{"case option after currently playing, no currentlyPlaying is null",
|
||||
concat(TFI_ID, QUEUE_DEFAULT_IDS),
|
||||
AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, ID_CURRENTLY_PLAYING_NULL},
|
||||
{"case option after currently playing, currentlyPlaying is externalMedia",
|
||||
concat(TFI_ID, QUEUE_DEFAULT_IDS),
|
||||
AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, ID_CURRENTLY_PLAYING_NOT_FEEDMEDIA},
|
||||
{"case empty queue, option after currently playing",
|
||||
list(TFI_ID),
|
||||
AFTER_CURRENTLY_PLAYING, QUEUE_EMPTY, ID_CURRENTLY_PLAYING_NULL},
|
||||
});
|
||||
}
|
||||
|
||||
@Parameter(4)
|
||||
public long idCurrentlyPlaying;
|
||||
|
||||
@Override
|
||||
Playable getCurrentlyPlaying() {
|
||||
return ItemEnqueuePositionCalculatorTest.getCurrentlyPlaying(idCurrentlyPlaying);
|
||||
}
|
||||
|
||||
private static Playable externalMedia() {
|
||||
return new ExternalMedia("http://example.com/episode.mp3", MediaType.AUDIO);
|
||||
}
|
||||
|
||||
private static final long ID_CURRENTLY_PLAYING_NULL = -1L;
|
||||
private static final long ID_CURRENTLY_PLAYING_NOT_FEEDMEDIA = -9999L;
|
||||
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class ItemEnqueuePositionCalculatorPreserveDownloadOrderTest {
|
||||
|
||||
/**
|
||||
* The test covers the use case that when user initiates multiple downloads in succession,
|
||||
* resulting in multiple addQueueItem() calls in succession.
|
||||
* the items in the queue will be in the same order as the the order user taps to download
|
||||
*/
|
||||
@Parameters(name = "{index}: case<{0}>")
|
||||
public static Iterable<Object[]> data() {
|
||||
// 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, 103L)),
|
||||
BACK, QUEUE_DEFAULT, ID_CURRENTLY_PLAYING_NULL},
|
||||
{"download order test, enqueue at front (currently playing has no effect)",
|
||||
concat(101L, QUEUE_DEFAULT_IDS),
|
||||
concat(list(101L, 102L), QUEUE_DEFAULT_IDS),
|
||||
concat(list(101L, 103L, 102L), QUEUE_DEFAULT_IDS),
|
||||
// ^ 103 is put ahead of 102, after 102 failed.
|
||||
// It is a limitation as the logic can't tell 102 download has failed
|
||||
// (as opposed to simply being enqueued)
|
||||
FRONT, QUEUE_DEFAULT, 11L}, // 11 is at the front, currently playing
|
||||
{"download order test, enqueue after currently playing",
|
||||
list(11L, 101L, 12L, 13L, 14L),
|
||||
list(11L, 101L, 102L, 12L, 13L, 14L),
|
||||
list(11L, 101L, 103L, 102L, 12L, 13L, 14L),
|
||||
AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, 11L} // 11 is at the front, currently playing
|
||||
});
|
||||
}
|
||||
|
||||
@Parameter
|
||||
public String message;
|
||||
|
||||
@Parameter(1)
|
||||
public List<Long> idsExpectedAfter101;
|
||||
|
||||
@Parameter(2)
|
||||
public List<Long> idsExpectedAfter102;
|
||||
|
||||
@Parameter(3)
|
||||
public List<Long> idsExpectedAfter103;
|
||||
|
||||
@Parameter(4)
|
||||
public EnqueueLocation options;
|
||||
|
||||
@Parameter(5)
|
||||
public List<FeedItem> queueInitial;
|
||||
|
||||
@Parameter(6)
|
||||
public long idCurrentlyPlaying;
|
||||
|
||||
@Test
|
||||
public void testQueueOrderWhenDownloading2Items() {
|
||||
|
||||
// Setup class under test
|
||||
//
|
||||
ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options);
|
||||
DownloadStateProvider stubDownloadStateProvider = mock(DownloadStateProvider.class);
|
||||
stub(stubDownloadStateProvider.isDownloadingFile(any(FeedMedia.class))).toReturn(false);
|
||||
calculator.downloadStateProvider = stubDownloadStateProvider;
|
||||
|
||||
// Setup initial data
|
||||
// A shallow copy, as the test code will manipulate the queue
|
||||
List<FeedItem> queue = new ArrayList<>(queueInitial);
|
||||
|
||||
// Test body
|
||||
Playable currentlyPlaying = getCurrentlyPlaying(idCurrentlyPlaying);
|
||||
// User clicks download on feed item 101
|
||||
FeedItem tFI101 = setAsDownloading(101, stubDownloadStateProvider, true);
|
||||
doAddToQueueAndAssertResult(message + " (1st download)",
|
||||
calculator, tFI101, queue, currentlyPlaying,
|
||||
idsExpectedAfter101);
|
||||
// Then user clicks download on feed item 102
|
||||
FeedItem tFI102 = setAsDownloading(102, stubDownloadStateProvider, true);
|
||||
doAddToQueueAndAssertResult(message + " (2nd download, it should preserve order of download)",
|
||||
calculator, tFI102, queue, currentlyPlaying,
|
||||
idsExpectedAfter102);
|
||||
// simulate download failure case for 102
|
||||
setAsDownloading(tFI102, stubDownloadStateProvider, false);
|
||||
// Then user clicks download on feed item 103
|
||||
FeedItem tFI103 = setAsDownloading(103, stubDownloadStateProvider, true);
|
||||
doAddToQueueAndAssertResult(message
|
||||
+ " (3rd download, with 2nd download failed; "
|
||||
+ "it should be behind 1st download (unless enqueueLocation is BACK)",
|
||||
calculator, tFI103, queue, currentlyPlaying,
|
||||
idsExpectedAfter103);
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static FeedItem setAsDownloading(int id, DownloadStateProvider stubDownloadStateProvider,
|
||||
boolean isDownloading) {
|
||||
FeedItem item = createFeedItem(id);
|
||||
FeedMedia media =
|
||||
new FeedMedia(item, "http://download.url.net/" + id
|
||||
, 100000 + id, "audio/mp3");
|
||||
media.setId(item.getId());
|
||||
item.setMedia(media);
|
||||
return setAsDownloading(item, stubDownloadStateProvider, isDownloading);
|
||||
}
|
||||
|
||||
private static FeedItem setAsDownloading(FeedItem item, DownloadStateProvider stubDownloadStateProvider,
|
||||
boolean isDownloading) {
|
||||
stub(stubDownloadStateProvider.isDownloadingFile(item.getMedia())).toReturn(isDownloading);
|
||||
return item;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
static void doAddToQueueAndAssertResult(String message,
|
||||
ItemEnqueuePositionCalculator calculator,
|
||||
FeedItem itemToAdd,
|
||||
List<FeedItem> queue,
|
||||
Playable currentlyPlaying,
|
||||
List<Long> idsExpected) {
|
||||
int posActual = calculator.calcPosition(queue, currentlyPlaying);
|
||||
queue.add(posActual, itemToAdd);
|
||||
assertEquals(message, idsExpected, getIdList(queue));
|
||||
}
|
||||
|
||||
static final List<FeedItem> QUEUE_EMPTY = Collections.unmodifiableList(Arrays.asList());
|
||||
|
||||
static final List<FeedItem> QUEUE_DEFAULT =
|
||||
Collections.unmodifiableList(Arrays.asList(
|
||||
createFeedItem(11), createFeedItem(12), createFeedItem(13), createFeedItem(14)));
|
||||
static final List<Long> QUEUE_DEFAULT_IDS =
|
||||
QUEUE_DEFAULT.stream().map(fi -> fi.getId()).collect(Collectors.toList());
|
||||
|
||||
|
||||
static Playable getCurrentlyPlaying(long idCurrentlyPlaying) {
|
||||
if (ID_CURRENTLY_PLAYING_NOT_FEEDMEDIA == idCurrentlyPlaying) {
|
||||
return externalMedia();
|
||||
}
|
||||
if (ID_CURRENTLY_PLAYING_NULL == idCurrentlyPlaying) {
|
||||
return null;
|
||||
}
|
||||
return createFeedItem(idCurrentlyPlaying).getMedia();
|
||||
}
|
||||
|
||||
static Playable externalMedia() {
|
||||
return new ExternalMedia("http://example.com/episode.mp3", MediaType.AUDIO);
|
||||
}
|
||||
|
||||
static final long ID_CURRENTLY_PLAYING_NULL = -1L;
|
||||
static final long ID_CURRENTLY_PLAYING_NOT_FEEDMEDIA = -9999L;
|
||||
|
||||
|
||||
static FeedItem createFeedItem(long id) {
|
||||
FeedItem item = new FeedItem(id, "Item" + id, "ItemId" + id, "url",
|
||||
new Date(), FeedItem.PLAYED, FeedMother.anyFeed());
|
||||
FeedMedia media = new FeedMedia(item, "download_url", 1234567, "audio/mpeg");
|
||||
media.setId(item.getId());
|
||||
item.setMedia(media);
|
||||
return item;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package de.danoeh.antennapod.core.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class CollectionTestUtil {
|
||||
|
||||
public static <T> List<? extends T> concat(T item, List<? extends T> list) {
|
||||
List<T> res = new ArrayList<>(list);
|
||||
res.add(0, item);
|
||||
return res;
|
||||
}
|
||||
|
||||
public static <T> List<? extends T> concat(List<? extends T> list, T item) {
|
||||
List<T> res = new ArrayList<>(list);
|
||||
res.add(item);
|
||||
return res;
|
||||
}
|
||||
|
||||
public 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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue