diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index 71b3c27a2..f3421c8fd 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -272,6 +272,13 @@ public class QueueFragment extends Fragment { MenuItemUtils.refreshLockItem(getActivity(), menu); + // Show Lock Item and Sort Item only if queue is sorted manually + boolean sortedManually = UserPreferences.isQueueSortedManually(); + MenuItem lockItem = menu.findItem(R.id.queue_lock); + lockItem.setVisible(sortedManually); + MenuItem sortItem = menu.findItem(R.id.queue_sort); + sortItem.setVisible(sortedManually); + isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); } } @@ -317,37 +324,37 @@ public class QueueFragment extends Fragment { conDialog.createNewDialog().show(); return true; case R.id.queue_sort_episode_title_asc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.EPISODE_TITLE_ASC, true); + QueueSorter.sort(QueueSorter.Rule.EPISODE_TITLE_ASC, true); return true; case R.id.queue_sort_episode_title_desc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.EPISODE_TITLE_DESC, true); + QueueSorter.sort(QueueSorter.Rule.EPISODE_TITLE_DESC, true); return true; case R.id.queue_sort_date_asc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.DATE_ASC, true); + QueueSorter.sort(QueueSorter.Rule.DATE_ASC, true); return true; case R.id.queue_sort_date_desc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.DATE_DESC, true); + QueueSorter.sort(QueueSorter.Rule.DATE_DESC, true); return true; case R.id.queue_sort_duration_asc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.DURATION_ASC, true); + QueueSorter.sort(QueueSorter.Rule.DURATION_ASC, true); return true; case R.id.queue_sort_duration_desc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.DURATION_DESC, true); + QueueSorter.sort(QueueSorter.Rule.DURATION_DESC, true); return true; case R.id.queue_sort_feed_title_asc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.FEED_TITLE_ASC, true); + QueueSorter.sort(QueueSorter.Rule.FEED_TITLE_ASC, true); return true; case R.id.queue_sort_feed_title_desc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.FEED_TITLE_DESC, true); + QueueSorter.sort(QueueSorter.Rule.FEED_TITLE_DESC, true); return true; case R.id.queue_sort_random: - QueueSorter.sort(getActivity(), QueueSorter.Rule.RANDOM, true); + QueueSorter.sort(QueueSorter.Rule.RANDOM, true); return true; case R.id.queue_sort_smart_shuffle_asc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.SMART_SHUFFLE_ASC, true); + QueueSorter.sort(QueueSorter.Rule.SMART_SHUFFLE_ASC, true); return true; case R.id.queue_sort_smart_shuffle_desc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.SMART_SHUFFLE_DESC, true); + QueueSorter.sort(QueueSorter.Rule.SMART_SHUFFLE_DESC, true); return true; default: return false; @@ -661,5 +668,4 @@ public class QueueFragment extends Fragment { } }, error -> Log.e(TAG, Log.getStackTraceString(error))); } - } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java index e1d44f7d3..db852cee7 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java @@ -11,7 +11,12 @@ import android.widget.ListView; import android.widget.Toast; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.Permutor; +import de.danoeh.antennapod.core.util.QueueSorter; + import org.apache.commons.lang3.ArrayUtils; import java.util.List; @@ -89,6 +94,17 @@ public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat { if (Build.VERSION.SDK_INT >= 26) { findPreference(UserPreferences.PREF_EXPANDED_NOTIFICATION).setVisible(false); } + + findPreference(UserPreferences.PREF_QUEUE_SORT_ORDER) + .setOnPreferenceChangeListener((preference, newValue) -> { + UserPreferences.QueueSortOrder newSortOrder = UserPreferences.parseQueueSortOrder((String) newValue); + if (newSortOrder != UserPreferences.QueueSortOrder.MANUALLY) { + QueueSorter.Rule sortRule = QueueSorter.queueSortOrder2Rule(newSortOrder); + Permutor permutor = QueueSorter.getPermutor(sortRule); + DBWriter.reorderQueue(permutor, true); + } + return true; + }); } private void showDrawerPreferencesDialog() { diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java index 3f8d88af7..2886a7e33 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -68,16 +68,17 @@ public class FeedItemMenuHandler { } boolean hasMedia = selectedItem.getMedia() != null; boolean isPlaying = hasMedia && selectedItem.getState() == FeedItem.State.PLAYING; + boolean sortedManually = UserPreferences.isQueueSortedManually(); if (!isPlaying) { mi.setItemVisibility(R.id.skip_episode_item, false); } boolean isInQueue = selectedItem.isTagged(FeedItem.TAG_QUEUE); - if(queueAccess == null || queueAccess.size() == 0 || queueAccess.get(0) == selectedItem.getId()) { + if (queueAccess == null || queueAccess.size() == 0 || queueAccess.get(0) == selectedItem.getId() || !sortedManually) { mi.setItemVisibility(R.id.move_to_top_item, false); } - if(queueAccess == null || queueAccess.size() == 0 || queueAccess.get(queueAccess.size()-1) == selectedItem.getId()) { + if (queueAccess == null || queueAccess.size() == 0 || queueAccess.get(queueAccess.size()-1) == selectedItem.getId() || !sortedManually) { mi.setItemVisibility(R.id.move_to_bottom_item, false); } if (!isInQueue) { diff --git a/app/src/main/res/xml/preferences_user_interface.xml b/app/src/main/res/xml/preferences_user_interface.xml index 1d970a5f7..455838048 100644 --- a/app/src/main/res/xml/preferences_user_interface.xml +++ b/app/src/main/res/xml/preferences_user_interface.xml @@ -32,6 +32,14 @@ android:summary="@string/pref_nav_drawer_feed_counter_sum" android:defaultValue="0" app:useStockLayout="true"/> + queue, List events) { + if (UserPreferences.isQueueSortedManually()) { + // automatic sort order is disabled, don't change anything + return; + } + + // Sort queue by configured sort order + UserPreferences.QueueSortOrder sortOrder = UserPreferences.getQueueSortOrder(); + QueueSorter.Rule sortRule = QueueSorter.queueSortOrder2Rule(sortOrder); + Permutor permutor = QueueSorter.getPermutor(sortRule); + permutor.reorder(queue); + + // Replace ADDED events by a single SORTED event + events.clear(); + events.add(QueueEvent.sorted(queue)); + } + /** * Removes all FeedItem objects from the queue. * @@ -964,31 +989,8 @@ public class DBWriter { } /** - * Sort the FeedItems in the queue with the given Comparator. - * @param comparator FeedItem comparator - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - */ - public static Future sortQueue(final Comparator comparator, final boolean broadcastUpdate) { - return dbExec.submit(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - final List queue = DBReader.getQueue(adapter); - - if (queue != null) { - Collections.sort(queue, comparator); - adapter.setQueue(queue); - if (broadcastUpdate) { - EventBus.getDefault().post(QueueEvent.sorted(queue)); - } - } else { - Log.e(TAG, "sortQueue: Could not load queue"); - } - adapter.close(); - }); - } - - /** - * Similar to sortQueue, but allows more complex reordering by providing whole-queue context. + * Sort the FeedItems in the queue with the given Permutor. + * * @param permutor Encapsulates whole-Queue reordering logic. * @param broadcastUpdate true if this operation should trigger a * QueueUpdateBroadcast. This option should be set to false diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java b/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java index 8680b2d2e..8bc377ffa 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java @@ -1,7 +1,5 @@ package de.danoeh.antennapod.core.util; -import android.content.Context; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -11,6 +9,7 @@ import java.util.Map; 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.DBWriter; /** @@ -31,7 +30,30 @@ public class QueueSorter { SMART_SHUFFLE_DESC } - public static void sort(final Context context, final Rule rule, final boolean broadcastUpdate) { + /** + * Sorts the queue by the given rule and sends a broadcast update. + * + * @param rule Sort rule. + * @param broadcastUpdate Send broadcast update? + */ + public static void sort(Rule rule, boolean broadcastUpdate) { + Permutor permutor = getPermutor(rule); + if (permutor != null) { + DBWriter.reorderQueue(permutor, broadcastUpdate); + } + } + + /** + * Returns a Permutor that sorts a list appropriate to the given sort rule. + * + * @param rule Sort rule. + * @return Permutor that sorts a list appropriate to the given sort rule. null if the rule is unknown or null. + */ + public static Permutor getPermutor(Rule rule) { + if (rule == null) { + return null; + } + Comparator comparator = null; Permutor permutor = null; @@ -86,13 +108,44 @@ public class QueueSorter { case SMART_SHUFFLE_DESC: permutor = (queue) -> smartShuffle(queue, false); break; - default: } if (comparator != null) { - DBWriter.sortQueue(comparator, broadcastUpdate); - } else if (permutor != null) { - DBWriter.reorderQueue(permutor, broadcastUpdate); + final Comparator comparator2 = comparator; + permutor = (queue) -> Collections.sort(queue, comparator2); + } + return permutor; + } + + /** + * Converts a QueueSortOrder value to its corresponding Rule value. + * + * @param sortOrder Sort order. + * @return Rule value corresponding to the given sort order. null if the sort order is unknown or null. + */ + public static Rule queueSortOrder2Rule(UserPreferences.QueueSortOrder sortOrder) { + if (sortOrder == null) { + return null; + } + switch (sortOrder) { + case DATE_NEW_OLD: + return QueueSorter.Rule.DATE_DESC; + case DATE_OLD_NEW: + return QueueSorter.Rule.DATE_ASC; + case DURATION_SHORT_LONG: + return QueueSorter.Rule.DURATION_ASC; + case DURATION_LONG_SHORT: + return QueueSorter.Rule.DURATION_DESC; + case EPISODE_TITLE_A_Z: + return QueueSorter.Rule.EPISODE_TITLE_ASC; + case EPISODE_TITLE_Z_A: + return QueueSorter.Rule.EPISODE_TITLE_DESC; + case FEED_TITLE_A_Z: + return QueueSorter.Rule.FEED_TITLE_ASC; + case FEED_TITLE_Z_A: + return QueueSorter.Rule.FEED_TITLE_DESC; + default: + return null; } } diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index 8962daa00..7bc9368b5 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -285,6 +285,8 @@ Aufsteigend Absteigend Bitte bestätige, dass ALLE Episoden aus der Abspielliste entfernt werden sollen + Neu bis alt + Alt bis neu Flattr Anmeldung Drücke den Button unten, um den Authentifizierungsprozess zu starten. Du wirst zur Flattr-Anmeldeseite weitergeleitet. Hier wirst du gefragt, AntennaPod die Erlaubnis zu geben, Dinge zu flattrn. Nachdem du die Erlaubnis erteilt hast, kehrst du automatisch zu diesem Bildschirm zurück. @@ -414,6 +416,8 @@ Ändere die Reihenfolge deiner Abonnements Abonnement-Zähler einstellen Ändere die durch den Abonnementszähler angezeigten Informationen. Betrifft auch die Sortierung der Abonnements wenn \"Reihenfolge der Abonnements\" auf \"Zähler\" gesetzt ist. + Sortierung der Abspielliste einstellen + Ändere die Sortierreihenfolge der Episoden in der Abspielliste. Ändere das Aussehen von AntennaPod. Automatisches Herunterladen Konfiguriere das automatische Herunterladen von Episoden. @@ -686,6 +690,11 @@ Datum (alt \u2192 neu) Dauer (kurz \u2192 lang) Dauer (lang \u2192 kurz) + Episodentitel (A \u2192 Z) + Episodentitel (Z \u2192 A) + Podcasttitel (A \u2192 Z) + Podcasttitel (Z \u2192 A) + Manuell Gefällt dir AntennaPod? Wir würden uns freuen, wenn du dir kurz die Zeit nimmst, AntennaPod zu bewerten. diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 39d1c0a94..d5df4f646 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -187,6 +187,29 @@ 3 + + @string/sort_manually + @string/sort_date_new_old + @string/sort_date_old_new + @string/sort_duration_short_long + @string/sort_duration_long_short + @string/sort_episode_title_a_z + @string/sort_episode_title_z_a + @string/sort_feed_title_a_z + @string/sort_feed_title_z_a + + + MANUALLY + DATE_NEW_OLD + DATE_OLD_NEW + DURATION_SHORT_LONG + DURATION_LONG_SHORT + EPISODE_TITLE_A_Z + EPISODE_TITLE_Z_A + FEED_TITLE_A_Z + FEED_TITLE_Z_A + + @string/drawer_feed_counter_new_unplayed @string/drawer_feed_counter_new diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 2fbb65744..3d7cbfe10 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -438,6 +438,8 @@ Change the order of your subscriptions Set Subscription Counter Change the information displayed by the subscription counter. Also affects the sorting of subscriptions if \'Subscription Order\' is set to \'Counter\'. + Set Queue Sort Order + Change the sort order of the episodes in the queue. Change the appearance of AntennaPod. Automatic Download Configure the automatic download of episodes. @@ -735,6 +737,11 @@ Date (Old \u2192 New) Duration (Short \u2192 Long) Duration (Long \u2192 Short) + Episode title (A \u2192 Z) + Episode title (Z \u2192 A) + Podcast title (A \u2192 Z) + Podcast title (Z \u2192 A) + Manually Like AntennaPod? diff --git a/core/src/test/java/android/text/TextUtils.java b/core/src/test/java/android/text/TextUtils.java index c31234171..eda31c3b5 100644 --- a/core/src/test/java/android/text/TextUtils.java +++ b/core/src/test/java/android/text/TextUtils.java @@ -29,4 +29,13 @@ public class TextUtils { return false; } + /** + * Returns true if the string is null or has zero length. + * + * @param str The string to be examined, can be null. + * @return true if the string is null or has zero length. + */ + public static boolean isEmpty(CharSequence str) { + return str == null || str.length() == 0; + } } diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/QueueSorterTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/QueueSorterTest.java new file mode 100644 index 000000000..9e2bbf5d7 --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/util/QueueSorterTest.java @@ -0,0 +1,156 @@ +package de.danoeh.antennapod.core.util; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNull; + +/** + * Test class for QueueSorter. + */ +public class QueueSorterTest { + + @Test + public void testPermutorForRule_null() { + assertNull(QueueSorter.getPermutor(null)); + } + + @Test + public void testPermutorForRule_EPISODE_TITLE_ASC() { + Permutor permutor = QueueSorter.getPermutor(QueueSorter.Rule.EPISODE_TITLE_ASC); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting + } + + @Test + public void testPermutorForRule_EPISODE_TITLE_DESC() { + Permutor permutor = QueueSorter.getPermutor(QueueSorter.Rule.EPISODE_TITLE_DESC); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting + } + + @Test + public void testPermutorForRule_DATE_ASC() { + Permutor permutor = QueueSorter.getPermutor(QueueSorter.Rule.DATE_ASC); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting + } + + @Test + public void testPermutorForRule_DATE_DESC() { + Permutor permutor = QueueSorter.getPermutor(QueueSorter.Rule.DATE_DESC); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting + } + + @Test + public void testPermutorForRule_DURATION_ASC() { + Permutor permutor = QueueSorter.getPermutor(QueueSorter.Rule.DURATION_ASC); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting + } + + @Test + public void testPermutorForRule_DURATION_DESC() { + Permutor permutor = QueueSorter.getPermutor(QueueSorter.Rule.DURATION_DESC); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting + } + + @Test + public void testPermutorForRule_FEED_TITLE_ASC() { + Permutor permutor = QueueSorter.getPermutor(QueueSorter.Rule.FEED_TITLE_ASC); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting + } + + @Test + public void testPermutorForRule_FEED_TITLE_DESC() { + Permutor permutor = QueueSorter.getPermutor(QueueSorter.Rule.FEED_TITLE_DESC); + + List itemList = getTestList(); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting + } + + /** + * Generates a list with test data. + */ + private List getTestList() { + List itemList = new ArrayList<>(); + + Calendar calendar = Calendar.getInstance(); + calendar.set(2019, 0, 1); // January 1st + Feed feed1 = new Feed(null, null, "Feed title 1"); + FeedItem feedItem1 = new FeedItem(1, "Title 1", null, null, calendar.getTime(), 0, feed1); + FeedMedia feedMedia1 = new FeedMedia(0, feedItem1, 1000, 0, 0, null, null, null, true, null, 0, 0); + feedItem1.setMedia(feedMedia1); + itemList.add(feedItem1); + + calendar.set(2019, 2, 1); // March 1st + Feed feed2 = new Feed(null, null, "Feed title 3"); + FeedItem feedItem2 = new FeedItem(3, "Title 3", null, null, calendar.getTime(), 0, feed2); + FeedMedia feedMedia2 = new FeedMedia(0, feedItem2, 3000, 0, 0, null, null, null, true, null, 0, 0); + feedItem2.setMedia(feedMedia2); + itemList.add(feedItem2); + + calendar.set(2019, 1, 1); // February 1st + Feed feed3 = new Feed(null, null, "Feed title 2"); + FeedItem feedItem3 = new FeedItem(2, "Title 2", null, null, calendar.getTime(), 0, feed3); + FeedMedia feedMedia3 = new FeedMedia(0, feedItem3, 2000, 0, 0, null, null, null, true, null, 0, 0); + feedItem3.setMedia(feedMedia3); + itemList.add(feedItem3); + + return itemList; + } + + /** + * Checks if both lists have the same size and the same ID order. + * + * @param itemList Item list. + * @param ids List of IDs. + * @return true if both lists have the same size and the same ID order. + */ + private boolean checkIdOrder(List itemList, long... ids) { + if (itemList.size() != ids.length) { + return false; + } + + for (int i = 0; i < ids.length; i++) { + if (itemList.get(i).getId() != ids[i]) { + return false; + } + } + return true; + } +}