diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java index 09a8466e6..ebf59fea7 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTestUtils.java @@ -44,8 +44,8 @@ class DBTestUtils { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); for (int i = 0; i < numFeeds; i++) { - Feed f = new Feed(0, null, "feed " + i, null, "link" + i, "descr", null, null, - null, null, "id" + i, null, null, "url" + i, false, false, null, null, false); + Feed f = new Feed(0, null, "feed " + i, "link" + i, "descr", null, null, + null, null, "id" + i, null, null, "url" + i, false); f.setItems(new ArrayList<>()); for (int j = 0; j < numItems; j++) { FeedItem item = new FeedItem(0, "item " + j, "id" + j, "link" + j, new Date(), diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/IntraFeedSortDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/IntraFeedSortDialog.java new file mode 100644 index 000000000..2ee716c7c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/IntraFeedSortDialog.java @@ -0,0 +1,51 @@ +package de.danoeh.antennapod.dialog; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.util.SortOrder; + +public abstract class IntraFeedSortDialog { + + @Nullable + protected SortOrder currentSortOrder; + @NonNull + protected Context context; + + public IntraFeedSortDialog(@NonNull Context context, @Nullable SortOrder sortOrder) { + this.context = context; + this.currentSortOrder = sortOrder; + } + + public void openDialog() { + final String[] items = context.getResources().getStringArray(R.array.feed_episodes_sort_options); + final String[] valueStrs = context.getResources().getStringArray(R.array.feed_episodes_sort_values); + final SortOrder[] values = new SortOrder[valueStrs.length]; + for (int i = 0; i < valueStrs.length; i++) { + values[i] = SortOrder.valueOf(valueStrs[i]); + } + + int idxCurrentSort = -1; + for (int i = 0; i < values.length; i++) { + if (currentSortOrder == values[i]) { + idxCurrentSort = i; + break; + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.sort); + builder.setSingleChoiceItems(items, idxCurrentSort, (dialog, idxNewSort) -> { + updateSort(values[idxNewSort]); + dialog.dismiss(); + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } + + protected abstract void updateSort(@NonNull SortOrder sortOrder); +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index 0c33dce5a..076320754 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -5,10 +5,6 @@ import android.content.Context; import android.content.DialogInterface; import android.graphics.LightingColorFilter; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.fragment.app.ListFragment; -import androidx.core.view.MenuItemCompat; -import androidx.appcompat.widget.SearchView; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; @@ -23,12 +19,16 @@ import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.SearchView; +import androidx.core.view.MenuItemCompat; +import androidx.fragment.app.ListFragment; + import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.joanzapata.iconify.Iconify; import com.joanzapata.iconify.widget.IconTextView; -import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import org.apache.commons.lang3.Validate; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -45,6 +45,7 @@ import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.event.FeedItemEvent; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedEvent; @@ -59,6 +60,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.Optional; @@ -192,6 +194,7 @@ public class FeedItemlistFragment extends ListFragment { searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { @Override public boolean onMenuItemActionExpand(MenuItem item) { + menu.findItem(R.id.sort_items).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); menu.findItem(R.id.filter_items).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); menu.findItem(R.id.episode_actions).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); menu.findItem(R.id.refresh_item).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); @@ -629,6 +632,11 @@ public class FeedItemlistFragment extends ListFragment { FeedItemFilter filter = feed.getItemFilter(); feed.setItems(filter.filter(feed.getItems())); } + if (feed != null && feed.getSortOrder() != null) { + List feedItems = feed.getItems(); + FeedItemPermutors.getPermutor(feed.getSortOrder()).reorder(feedItems); + feed.setItems(feedItems); + } return Optional.ofNullable(feed); } 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 37277a7c4..c0552775f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -4,15 +4,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; -import com.google.android.material.snackbar.Snackbar; -import androidx.fragment.app.Fragment; -import androidx.core.view.MenuItemCompat; -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.appcompat.widget.SearchView; -import androidx.recyclerview.widget.SimpleItemAnimator; -import androidx.recyclerview.widget.ItemTouchHelper; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -24,9 +15,18 @@ import android.widget.CheckBox; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.SearchView; +import androidx.core.view.MenuItemCompat; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; + +import com.google.android.material.snackbar.Snackbar; import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; -import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -40,6 +40,7 @@ import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.event.FeedItemEvent; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.FeedItem; @@ -54,7 +55,6 @@ import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.QueueSorter; import de.danoeh.antennapod.core.util.SortOrder; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; @@ -372,7 +372,7 @@ public class QueueFragment extends Fragment { UserPreferences.setQueueKeepSorted(keepSortedNew); if (keepSortedNew) { SortOrder sortOrder = UserPreferences.getQueueKeepSortedOrder(); - QueueSorter.sort(sortOrder, true); + DBWriter.reorderQueue(sortOrder, true); if (recyclerAdapter != null) { recyclerAdapter.setLocked(true); } @@ -438,7 +438,7 @@ public class QueueFragment extends Fragment { */ private void setSortOrder(SortOrder sortOrder) { UserPreferences.setQueueKeepSortedOrder(sortOrder); - QueueSorter.sort(sortOrder, true); + DBWriter.reorderQueue(sortOrder, true); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java index f7aae8cde..e32deba27 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -7,6 +7,8 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.annotation.NonNull; + import org.apache.commons.lang3.StringUtils; import java.util.Set; @@ -19,7 +21,9 @@ import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.ShareUtils; +import de.danoeh.antennapod.core.util.SortOrder; import de.danoeh.antennapod.dialog.FilterDialog; +import de.danoeh.antennapod.dialog.IntraFeedSortDialog; /** * Handles interactions with the FeedItemMenu. @@ -65,6 +69,9 @@ public class FeedMenuHandler { case R.id.refresh_complete_item: DBTasks.forceRefreshCompleteFeed(context, selectedFeed); break; + case R.id.sort_items: + showSortDialog(context, selectedFeed); + break; case R.id.filter_items: showFilterDialog(context, selectedFeed); break; @@ -108,4 +115,17 @@ public class FeedMenuHandler { filterDialog.openDialog(); } + + + private static void showSortDialog(Context context, Feed selectedFeed) { + IntraFeedSortDialog sortDialog = new IntraFeedSortDialog(context, selectedFeed.getSortOrder()) { + @Override + protected void updateSort(@NonNull SortOrder sortOrder) { + selectedFeed.setSortOrder(sortOrder); + DBWriter.setFeedItemSortOrder(selectedFeed.getId(), sortOrder); + } + }; + sortDialog.openDialog(); + } + } diff --git a/app/src/main/res/menu/feedlist.xml b/app/src/main/res/menu/feedlist.xml index fdd0e01bc..13c019b65 100644 --- a/app/src/main/res/menu/feedlist.xml +++ b/app/src/main/res/menu/feedlist.xml @@ -2,6 +2,13 @@ + + - - + + 0, cursor.getString(indexNextPageLink), cursor.getString(indexHide), + SortOrder.fromCodeString(cursor.getString(indexSortOrder)), cursor.getInt(indexLastUpdateFailed) > 0 ); @@ -523,6 +536,19 @@ public class Feed extends FeedFile implements ImageResource { } } + @Nullable + public SortOrder getSortOrder() { + return sortOrder; + } + + public void setSortOrder(@Nullable SortOrder sortOrder) { + if (sortOrder != null && sortOrder.scope != SortOrder.Scope.INTRA_FEED) { + throw new IllegalArgumentException("The specified sortOrder " + sortOrder + + " is invalid. Only those with INTRA_FEED scope are allowed."); + } + this.sortOrder = sortOrder; + } + public boolean hasLastUpdateFailed() { return this.lastUpdateFailed; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java index b790faadf..15cdf92dc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java @@ -6,7 +6,8 @@ import org.apache.commons.lang3.builder.ToStringStyle; public class FeedEvent { public enum Action { - FILTER_CHANGED + FILTER_CHANGED, + SORT_ORDER_CHANGED } private final Action action; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java index 575b8d2ac..0c8f89348 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java @@ -294,6 +294,11 @@ class DBUpgrader { db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + PodDBAdapter.KEY_FEED_PLAYBACK_SPEED + " REAL DEFAULT " + SPEED_USE_GLOBAL); } + + if (oldVersion < 1070401) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_SORT_ORDER + " TEXT"); + } } } 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 9225155cf..32b04deea 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 @@ -5,6 +5,7 @@ import android.content.Context; 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.PlaybackHistoryEvent; @@ -38,10 +39,10 @@ import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.FeedItemPermutors; 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.QueueSorter; import de.danoeh.antennapod.core.util.SortOrder; /** @@ -387,7 +388,7 @@ public class DBWriter { // do not shuffle the list on every change return; } - Permutor permutor = QueueSorter.getPermutor(sortOrder); + Permutor permutor = FeedItemPermutors.getPermutor(sortOrder); permutor.reorder(queue); // Replace ADDED events by a single SORTED event @@ -847,14 +848,18 @@ public class DBWriter { } /** - * Sort the FeedItems in the queue with the given Permutor. + * Sort the FeedItems in the queue with the given the named sort order. * - * @param permutor Encapsulates whole-Queue reordering logic. * @param broadcastUpdate true if this operation should trigger a * QueueUpdateBroadcast. This option should be set to false * if the caller wants to avoid unexpected updates of the GUI. */ - public static Future reorderQueue(final Permutor permutor, final boolean broadcastUpdate) { + public static Future reorderQueue(@Nullable SortOrder sortOrder, final boolean broadcastUpdate) { + if (sortOrder == null) { + Log.w(TAG, "reorderQueue() - sortOrder is null. Do nothing."); + return dbExec.submit(() -> { }); + } + final Permutor permutor = FeedItemPermutors.getPermutor(sortOrder); return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); @@ -946,6 +951,19 @@ public class DBWriter { }); } + /** + * Set item sort order of the feed + * + */ + public static Future setFeedItemSortOrder(long feedId, @Nullable SortOrder sortOrder) { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedItemSortOrder(feedId, sortOrder); + adapter.close(); + EventBus.getDefault().post(new FeedEvent(FeedEvent.Action.SORT_ORDER_CHANGED, feedId)); + }); + } /** * Reset the statistics in DB diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java index 66f6d223d..a37f357ae 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -13,11 +13,11 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; import android.os.Build; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.Nullable; + import org.apache.commons.io.FileUtils; import java.io.File; @@ -35,8 +35,10 @@ import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.util.LongIntMap; +import de.danoeh.antennapod.core.util.SortOrder; import static de.danoeh.antennapod.core.feed.FeedPreferences.SPEED_USE_GLOBAL; +import static de.danoeh.antennapod.core.util.SortOrder.toCodeString; // TODO Remove media column from feeditem table @@ -101,6 +103,7 @@ public class PodDBAdapter { public static final String KEY_IS_PAGED = "is_paged"; public static final String KEY_NEXT_PAGE_LINK = "next_page_link"; public static final String KEY_HIDE = "hide"; + public static final String KEY_SORT_ORDER = "sort_order"; public static final String KEY_LAST_UPDATE_FAILED = "last_update_failed"; public static final String KEY_HAS_EMBEDDED_PICTURE = "has_embedded_picture"; public static final String KEY_LAST_PLAYED_TIME = "last_played_time"; @@ -138,6 +141,7 @@ public class PodDBAdapter { + KEY_IS_PAGED + " INTEGER DEFAULT 0," + KEY_NEXT_PAGE_LINK + " TEXT," + KEY_HIDE + " TEXT," + + KEY_SORT_ORDER + " TEXT," + KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0," + KEY_AUTO_DELETE_ACTION + " INTEGER DEFAULT 0," + KEY_FEED_PLAYBACK_SPEED + " REAL DEFAULT " + SPEED_USE_GLOBAL + ")"; @@ -234,6 +238,7 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_USERNAME, TABLE_NAME_FEEDS + "." + KEY_PASSWORD, TABLE_NAME_FEEDS + "." + KEY_HIDE, + TABLE_NAME_FEEDS + "." + KEY_SORT_ORDER, TABLE_NAME_FEEDS + "." + KEY_LAST_UPDATE_FAILED, TABLE_NAME_FEEDS + "." + KEY_AUTO_DELETE_ACTION, TABLE_NAME_FEEDS + "." + KEY_INCLUDE_FILTER, @@ -378,6 +383,7 @@ public class PodDBAdapter { } else { values.put(KEY_HIDE, ""); } + values.put(KEY_SORT_ORDER, toCodeString(feed.getSortOrder())); values.put(KEY_LAST_UPDATE_FAILED, feed.hasLastUpdateFailed()); if (feed.getId() == 0) { // Create new entry @@ -416,6 +422,12 @@ public class PodDBAdapter { db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feedId)}); } + public void setFeedItemSortOrder(long feedId, @Nullable SortOrder sortOrder) { + ContentValues values = new ContentValues(); + values.put(KEY_SORT_ORDER, toCodeString(sortOrder)); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feedId)}); + } + /** * Inserts or updates a media entry * @@ -1455,7 +1467,7 @@ public class PodDBAdapter { */ private static class PodDBHelper extends SQLiteOpenHelper { - private static final int VERSION = 1070400; + private static final int VERSION = 1070401; private final Context context; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java similarity index 71% rename from core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java rename to core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java index 0c21ca393..9bf9b9db0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java @@ -1,89 +1,58 @@ package de.danoeh.antennapod.core.util; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.storage.DBWriter; /** - * Provides method for sorting the queue according to rules. + * Provides method for sorting the a list of {@link FeedItem} according to rules. */ -public class QueueSorter { - - /** - * Sorts the queue by the given sort order and sends a broadcast update. - * - * @param sortOrder Sort order. - * @param broadcastUpdate Send broadcast update? - */ - public static void sort(SortOrder sortOrder, boolean broadcastUpdate) { - Permutor permutor = getPermutor(sortOrder); - if (permutor != null) { - DBWriter.reorderQueue(permutor, broadcastUpdate); - } - } +public class FeedItemPermutors { /** * Returns a Permutor that sorts a list appropriate to the given sort order. * - * @param sortOrder Sort order. - * @return Permutor that sorts a list appropriate to the given sort order. null if the order is unknown or null. + * @return Permutor that sorts a list appropriate to the given sort order. */ - public static Permutor getPermutor(SortOrder sortOrder) { - if (sortOrder == null) { - return null; - } + @NonNull + public static Permutor getPermutor(@NonNull SortOrder sortOrder) { Comparator comparator = null; Permutor permutor = null; switch (sortOrder) { case EPISODE_TITLE_A_Z: - comparator = (f1, f2) -> f1.getTitle().compareTo(f2.getTitle()); + comparator = (f1, f2) -> itemTitle(f1).compareTo(itemTitle(f2)); break; case EPISODE_TITLE_Z_A: - comparator = (f1, f2) -> f2.getTitle().compareTo(f1.getTitle()); + comparator = (f1, f2) -> itemTitle(f2).compareTo(itemTitle(f1)); break; case DATE_OLD_NEW: - comparator = (f1, f2) -> f1.getPubDate().compareTo(f2.getPubDate()); + comparator = (f1, f2) -> pubDate(f1).compareTo(pubDate(f2)); break; case DATE_NEW_OLD: - comparator = (f1, f2) -> f2.getPubDate().compareTo(f1.getPubDate()); + comparator = (f1, f2) -> pubDate(f2).compareTo(pubDate(f1)); break; case DURATION_SHORT_LONG: - comparator = (f1, f2) -> { - FeedMedia f1Media = f1.getMedia(); - FeedMedia f2Media = f2.getMedia(); - int duration1 = f1Media != null ? f1Media.getDuration() : -1; - int duration2 = f2Media != null ? f2Media.getDuration() : -1; - - if (duration1 == -1 || duration2 == -1) - return duration2 - duration1; - else - return duration1 - duration2; - }; + comparator = (f1, f2) -> Integer.compare(duration(f1), duration(f2)); break; case DURATION_LONG_SHORT: - comparator = (f1, f2) -> { - FeedMedia f1Media = f1.getMedia(); - FeedMedia f2Media = f2.getMedia(); - int duration1 = f1Media != null ? f1Media.getDuration() : -1; - int duration2 = f2Media != null ? f2Media.getDuration() : -1; - - return -1 * (duration1 - duration2); - }; + comparator = (f1, f2) -> Integer.compare(duration(f2), duration(f1)); break; case FEED_TITLE_A_Z: - comparator = (f1, f2) -> f1.getFeed().getTitle().compareTo(f2.getFeed().getTitle()); + comparator = (f1, f2) -> feedTitle(f1).compareTo(feedTitle(f2)); break; case FEED_TITLE_Z_A: - comparator = (f1, f2) -> f2.getFeed().getTitle().compareTo(f1.getFeed().getTitle()); + comparator = (f1, f2) -> feedTitle(f2).compareTo(feedTitle(f1)); break; case RANDOM: permutor = Collections::shuffle; @@ -103,6 +72,31 @@ public class QueueSorter { return permutor; } + // Null-safe accessors + + @NonNull + private static Date pubDate(@Nullable FeedItem item) { + return (item != null && item.getPubDate() != null) ? + item.getPubDate() : new Date(0); + } + + @NonNull + private static String itemTitle(@Nullable FeedItem item) { + return (item != null && item.getTitle() != null) ? + item.getTitle() : ""; + } + + private static int duration(@Nullable FeedItem item) { + return (item != null && item.getMedia() != null) ? + item.getMedia().getDuration() : 0; + } + + @NonNull + private static String feedTitle(@Nullable FeedItem item) { + return (item != null && item.getFeed() != null && item.getFeed().getTitle() != null) ? + item.getFeed().getTitle() : ""; + } + /** * Implements a reordering by pubdate that avoids consecutive episodes from the same feed in * the queue. diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/SortOrder.java b/core/src/main/java/de/danoeh/antennapod/core/util/SortOrder.java index ae6fceb47..96a18506d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/SortOrder.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/SortOrder.java @@ -1,20 +1,42 @@ package de.danoeh.antennapod.core.util; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import static de.danoeh.antennapod.core.util.SortOrder.Scope.INTER_FEED; +import static de.danoeh.antennapod.core.util.SortOrder.Scope.INTRA_FEED; + /** * Provides sort orders to sort a list of episodes. */ public enum SortOrder { - EPISODE_TITLE_A_Z, - EPISODE_TITLE_Z_A, - DATE_OLD_NEW, - DATE_NEW_OLD, - DURATION_SHORT_LONG, - DURATION_LONG_SHORT, - FEED_TITLE_A_Z, - FEED_TITLE_Z_A, - RANDOM, - SMART_SHUFFLE_OLD_NEW, - SMART_SHUFFLE_NEW_OLD; + DATE_OLD_NEW(1, INTRA_FEED), + DATE_NEW_OLD(2, INTRA_FEED), + EPISODE_TITLE_A_Z(3, INTRA_FEED), + EPISODE_TITLE_Z_A(4, INTRA_FEED), + DURATION_SHORT_LONG(5, INTRA_FEED), + DURATION_LONG_SHORT(6, INTRA_FEED), + FEED_TITLE_A_Z(101, INTER_FEED), + FEED_TITLE_Z_A(102, INTER_FEED), + RANDOM(103, INTER_FEED), + SMART_SHUFFLE_OLD_NEW(104, INTER_FEED), + SMART_SHUFFLE_NEW_OLD(105, INTER_FEED); + + public enum Scope { + INTRA_FEED, INTER_FEED; + } + + public final int code; + + @NonNull + public final Scope scope; + + SortOrder(int code, @NonNull Scope scope) { + this.code = code; + this.scope = scope; + } /** * Converts the string representation to its enum value. If the string value is unknown, @@ -27,4 +49,23 @@ public enum SortOrder { return defaultValue; } } + + @Nullable + public static SortOrder fromCodeString(@Nullable String codeStr) { + if (TextUtils.isEmpty(codeStr)) { + return null; + } + int code = Integer.parseInt(codeStr); + for (SortOrder sortOrder : values()) { + if (sortOrder.code == code) { + return sortOrder; + } + } + throw new IllegalArgumentException("Unsupported code: " + code); + } + + @Nullable + public static String toCodeString(@Nullable SortOrder sortOrder) { + return sortOrder != null ? Integer.toString(sortOrder.code) : null; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java index a96eda3c5..51fe2da78 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java @@ -7,7 +7,7 @@ import de.danoeh.antennapod.core.feed.FeedItem; /** Compares the pubDate of two FeedItems for sorting*/ public class FeedItemPubdateComparator implements Comparator { - /** Returns a new instance of this comparator in reverse order. + /** Returns a new instance of this comparator in reverse order. public static FeedItemPubdateComparator newInstance() { FeedItemPubdateComparator }*/ diff --git a/core/src/main/res/drawable-hdpi/ic_filter_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_filter_grey600_24dp.png deleted file mode 100644 index 0d2f6ccb4..000000000 Binary files a/core/src/main/res/drawable-hdpi/ic_filter_grey600_24dp.png and /dev/null differ diff --git a/core/src/main/res/drawable-hdpi/ic_filter_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_filter_white_24dp.png deleted file mode 100644 index ea6f9078e..000000000 Binary files a/core/src/main/res/drawable-hdpi/ic_filter_white_24dp.png and /dev/null differ diff --git a/core/src/main/res/drawable-mdpi/ic_filter_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_filter_grey600_24dp.png deleted file mode 100644 index b78e7d4d2..000000000 Binary files a/core/src/main/res/drawable-mdpi/ic_filter_grey600_24dp.png and /dev/null differ diff --git a/core/src/main/res/drawable-mdpi/ic_filter_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_filter_white_24dp.png deleted file mode 100644 index 59a2ec755..000000000 Binary files a/core/src/main/res/drawable-mdpi/ic_filter_white_24dp.png and /dev/null differ diff --git a/core/src/main/res/drawable-xhdpi/ic_filter_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_filter_grey600_24dp.png deleted file mode 100644 index 4c6a96eba..000000000 Binary files a/core/src/main/res/drawable-xhdpi/ic_filter_grey600_24dp.png and /dev/null differ diff --git a/core/src/main/res/drawable-xhdpi/ic_filter_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_filter_white_24dp.png deleted file mode 100644 index 9416c70ec..000000000 Binary files a/core/src/main/res/drawable-xhdpi/ic_filter_white_24dp.png and /dev/null differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_filter_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_filter_grey600_24dp.png deleted file mode 100644 index 5846ad140..000000000 Binary files a/core/src/main/res/drawable-xxhdpi/ic_filter_grey600_24dp.png and /dev/null differ diff --git a/core/src/main/res/drawable-xxhdpi/ic_filter_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_filter_white_24dp.png deleted file mode 100644 index 1263ae82e..000000000 Binary files a/core/src/main/res/drawable-xxhdpi/ic_filter_white_24dp.png and /dev/null differ diff --git a/core/src/main/res/drawable-xxxhdpi/ic_filter_grey600_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_filter_grey600_24dp.png deleted file mode 100644 index 3fe8eb4dd..000000000 Binary files a/core/src/main/res/drawable-xxxhdpi/ic_filter_grey600_24dp.png and /dev/null differ diff --git a/core/src/main/res/drawable-xxxhdpi/ic_filter_white_24dp.png b/core/src/main/res/drawable-xxxhdpi/ic_filter_white_24dp.png deleted file mode 100644 index cb2207f11..000000000 Binary files a/core/src/main/res/drawable-xxxhdpi/ic_filter_white_24dp.png and /dev/null differ diff --git a/core/src/main/res/drawable/ic_filter_grey600_24dp.xml b/core/src/main/res/drawable/ic_filter_grey600_24dp.xml new file mode 100644 index 000000000..dc164504c --- /dev/null +++ b/core/src/main/res/drawable/ic_filter_grey600_24dp.xml @@ -0,0 +1,7 @@ + + + diff --git a/core/src/main/res/drawable/ic_filter_white_24dp.xml b/core/src/main/res/drawable/ic_filter_white_24dp.xml new file mode 100644 index 000000000..4b1b88581 --- /dev/null +++ b/core/src/main/res/drawable/ic_filter_white_24dp.xml @@ -0,0 +1,7 @@ + + + diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 5e7eab1ea..2dd985d6a 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -254,6 +254,25 @@ is_favorite + + + @string/sort_date_new_old + @string/sort_date_old_new + @string/sort_title_a_z + @string/sort_title_z_a + @string/sort_duration_short_long + @string/sort_duration_long_short + + + + DATE_NEW_OLD + DATE_OLD_NEW + EPISODE_TITLE_A_Z + EPISODE_TITLE_Z_A + DURATION_SHORT_LONG + DURATION_LONG_SHORT + + 20 MiB 50 MiB diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/FeedTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/FeedTest.java index 4717041f4..88b342850 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/feed/FeedTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/feed/FeedTest.java @@ -3,10 +3,13 @@ package de.danoeh.antennapod.core.feed; import org.junit.Before; import org.junit.Test; +import de.danoeh.antennapod.core.util.SortOrder; + import static de.danoeh.antennapod.core.feed.FeedMother.anyFeed; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class FeedTest { @@ -59,6 +62,27 @@ public class FeedTest { feedImageWasUpdated(); } + @Test + public void testSetSortOrder_OnlyIntraFeedSortAllowed() throws Exception { + for (SortOrder sortOrder : SortOrder.values()) { + if (sortOrder.scope == SortOrder.Scope.INTRA_FEED) { + original.setSortOrder(sortOrder); // should be okay + } else { + try { + original.setSortOrder(sortOrder); + fail("SortOrder " + sortOrder + " should not be allowed on a feed"); + } catch (IllegalArgumentException iae) { + // expected exception + } + } + } + } + + @Test + public void testSetSortOrder_NullAllowed() throws Exception { + original.setSortOrder(null); // should be okay + } + private void feedHasNotChanged() { assertFalse(original.compareWithOther(changedFeed)); } diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/QueueSorterTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/FeedItemPermutorsTest.java similarity index 60% rename from core/src/test/java/de/danoeh/antennapod/core/util/QueueSorterTest.java rename to core/src/test/java/de/danoeh/antennapod/core/util/FeedItemPermutorsTest.java index 791b6a75b..ccaa77ae8 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/util/QueueSorterTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/util/FeedItemPermutorsTest.java @@ -10,22 +10,25 @@ 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.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertNull; /** - * Test class for QueueSorter. + * Test class for FeedItemPermutors. */ -public class QueueSorterTest { +public class FeedItemPermutorsTest { @Test - public void testPermutorForRule_null() { - assertNull(QueueSorter.getPermutor(null)); + public void testEnsureNonNullPermutors() { + for (SortOrder sortOrder : SortOrder.values()) { + assertNotNull("The permutor for SortOrder " + sortOrder + " is unexpectedly null", + FeedItemPermutors.getPermutor(sortOrder)); + } } @Test public void testPermutorForRule_EPISODE_TITLE_ASC() { - Permutor permutor = QueueSorter.getPermutor(SortOrder.EPISODE_TITLE_A_Z); + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.EPISODE_TITLE_A_Z); List itemList = getTestList(); assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting @@ -33,9 +36,22 @@ public class QueueSorterTest { assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting } + @Test + public void testPermutorForRule_EPISODE_TITLE_ASC_NullTitle() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.EPISODE_TITLE_A_Z); + + List itemList = getTestList(); + itemList.get(2) // itemId 2 + .setTitle(null); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting + } + + @Test public void testPermutorForRule_EPISODE_TITLE_DESC() { - Permutor permutor = QueueSorter.getPermutor(SortOrder.EPISODE_TITLE_Z_A); + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.EPISODE_TITLE_Z_A); List itemList = getTestList(); assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting @@ -45,7 +61,7 @@ public class QueueSorterTest { @Test public void testPermutorForRule_DATE_ASC() { - Permutor permutor = QueueSorter.getPermutor(SortOrder.DATE_OLD_NEW); + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DATE_OLD_NEW); List itemList = getTestList(); assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting @@ -53,9 +69,21 @@ public class QueueSorterTest { assertTrue(checkIdOrder(itemList, 1, 2, 3)); // after sorting } + @Test + public void testPermutorForRule_DATE_ASC_NulPubDatel() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DATE_OLD_NEW); + + List itemList = getTestList(); + itemList.get(2) // itemId 2 + .setPubDate(null); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting + } + @Test public void testPermutorForRule_DATE_DESC() { - Permutor permutor = QueueSorter.getPermutor(SortOrder.DATE_NEW_OLD); + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DATE_NEW_OLD); List itemList = getTestList(); assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting @@ -65,7 +93,7 @@ public class QueueSorterTest { @Test public void testPermutorForRule_DURATION_ASC() { - Permutor permutor = QueueSorter.getPermutor(SortOrder.DURATION_SHORT_LONG); + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DURATION_SHORT_LONG); List itemList = getTestList(); assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting @@ -75,7 +103,7 @@ public class QueueSorterTest { @Test public void testPermutorForRule_DURATION_DESC() { - Permutor permutor = QueueSorter.getPermutor(SortOrder.DURATION_LONG_SHORT); + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DURATION_LONG_SHORT); List itemList = getTestList(); assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting @@ -83,9 +111,21 @@ public class QueueSorterTest { assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting } + @Test + public void testPermutorForRule_DURATION_DESC_NullMedia() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.DURATION_LONG_SHORT); + + List itemList = getTestList(); + itemList.get(1) // itemId 3 + .setMedia(null); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting + } + @Test public void testPermutorForRule_FEED_TITLE_ASC() { - Permutor permutor = QueueSorter.getPermutor(SortOrder.FEED_TITLE_A_Z); + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.FEED_TITLE_A_Z); List itemList = getTestList(); assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting @@ -95,7 +135,7 @@ public class QueueSorterTest { @Test public void testPermutorForRule_FEED_TITLE_DESC() { - Permutor permutor = QueueSorter.getPermutor(SortOrder.FEED_TITLE_Z_A); + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.FEED_TITLE_Z_A); List itemList = getTestList(); assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting @@ -103,6 +143,18 @@ public class QueueSorterTest { assertTrue(checkIdOrder(itemList, 3, 2, 1)); // after sorting } + @Test + public void testPermutorForRule_FEED_TITLE_DESC_NullTitle() { + Permutor permutor = FeedItemPermutors.getPermutor(SortOrder.FEED_TITLE_Z_A); + + List itemList = getTestList(); + itemList.get(1) // itemId 3 + .getFeed().setTitle(null); + assertTrue(checkIdOrder(itemList, 1, 3, 2)); // before sorting + permutor.reorder(itemList); + assertTrue(checkIdOrder(itemList, 2, 1, 3)); // after sorting + } + /** * Generates a list with test data. */