diff options
author | H. Lehmann <ByteHamster@users.noreply.github.com> | 2019-11-05 23:29:52 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-11-05 23:29:52 +0100 |
commit | 808f273c09c9aebabf7cf616318a37fb37acca42 (patch) | |
tree | dc9fff854cf5ed52b5ad21d0057d58428c245abb /core/src/main | |
parent | 363c3614f8ff80a1c48d0a59735656dde76b2da9 (diff) | |
parent | 89d76702c033eb81e92699cda3f058c2fc3c7d8c (diff) | |
download | AntennaPod-808f273c09c9aebabf7cf616318a37fb37acca42.zip |
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
Diffstat (limited to 'core/src/main')
10 files changed, 271 insertions, 71 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index be130c00f..ff2a2e1ca 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -4,11 +4,13 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; import android.preference.PreferenceManager; +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 android.text.TextUtils; -import android.util.Log; 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() { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java index cf5a84eea..7465b5b38 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java @@ -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) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index 9d37a5f2a..7dc53f8b3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -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 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 8f0626c5c..23d14fe87 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 @@ -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,57 +315,54 @@ public class DBWriter { public static Future<?> addQueueItem(final Context context, final boolean performAutoDownload, final long... itemIds) { return dbExec.submit(() -> { - if (itemIds.length > 0) { - 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]); - - - 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)); - } - item.addTag(FeedItem.TAG_QUEUE); - updatedItems.add(item); - queueModified = true; - if (item.isNew()) { - markAsUnplayedIds.add(item.getId()); - } - } - } - } - if (queueModified) { - applySortOrder(queue, events); - adapter.setQueue(queue); - for (QueueEvent event : events) { - EventBus.getDefault().post(event); - } - EventBus.getDefault().post(FeedItemEvent.updated(updatedItems)); - if (markAsUnplayedIds.size() > 0) { - DBWriter.markItemPlayed(FeedItem.UNPLAYED, markAsUnplayedIds.toArray()); + if (itemIds.length < 1) { + return; + } + + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + final List<FeedItem> queue = DBReader.getQueue(adapter); + + boolean queueModified = false; + LongList markAsUnplayedIds = new LongList(); + List<QueueEvent> events = new ArrayList<>(); + List<FeedItem> updatedItems = new ArrayList<>(); + 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) { + 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++; } } - adapter.close(); - if (performAutoDownload) { - DBTasks.autodownloadUndownloadedItems(context); + } + if (queueModified) { + applySortOrder(queue, events); + adapter.setQueue(queue); + for (QueueEvent event : events) { + EventBus.getDefault().post(event); + } + EventBus.getDefault().post(FeedItemEvent.updated(updatedItems)); + if (markAsUnplayedIds.size() > 0) { + DBWriter.markItemPlayed(FeedItem.UNPLAYED, markAsUnplayedIds.toArray()); } } + adapter.close(); + if (performAutoDownload) { + DBTasks.autodownloadUndownloadedItems(context); + } }); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java index 71f6845c5..c61abc168 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java @@ -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/"; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadStateProvider.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadStateProvider.java new file mode 100644 index 000000000..ece40353f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadStateProvider.java @@ -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); + +} 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..4b28d36b5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java @@ -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; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java index 8d77f0f24..5ae8dbcc7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java @@ -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. diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 2dd985d6a..3a6a45e6d 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -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> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 6541524fb..b8187a404 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -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> |