diff --git a/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java b/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java index 8c628efd5..e31838671 100644 --- a/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java +++ b/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java @@ -11,15 +11,11 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.List; - import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.fragment.EpisodesFragment; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.ui.UITestUtils; @@ -70,7 +66,6 @@ public class ShareDialogTest { onView(withText(R.string.all_episodes_short_label)).perform(click()); Matcher allEpisodesMatcher; - final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); allEpisodesMatcher = Matchers.allOf(withId(android.R.id.list), isDisplayed(), hasMinimumChildCount(2)); onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000)); onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, click())); diff --git a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java index 419cf2096..f3bd61839 100644 --- a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java +++ b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java @@ -10,6 +10,7 @@ import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; +import de.danoeh.antennapod.core.feed.FeedItemFilter; import org.awaitility.Awaitility; import org.hamcrest.Matcher; import org.junit.After; @@ -252,7 +253,7 @@ public class PlaybackTest { onView(isRoot()).perform(waitForView(withText(R.string.all_episodes_short_label), 1000)); onView(withText(R.string.all_episodes_short_label)).perform(click()); - final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); + final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered()); Matcher allEpisodesMatcher = allOf(withId(android.R.id.list), isDisplayed(), hasMinimumChildCount(2)); onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000)); onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, clickChildViewWithId(R.id.secondaryActionButton))); @@ -287,7 +288,7 @@ public class PlaybackTest { uiTestUtils.addLocalFeedData(true); DBWriter.clearQueue().get(); activityTestRule.launchActivity(new Intent()); - final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); + final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered()); startLocalPlayback(); FeedMedia media = episodes.get(0).getMedia(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java index 4423a2ebe..612959c04 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java @@ -104,13 +104,12 @@ public class AllEpisodesFragment extends EpisodesListFragment { @NonNull @Override protected List loadData() { - return feedItemFilter.filter(DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE)); + return DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE, feedItemFilter); } @NonNull @Override protected List loadMoreData() { - return feedItemFilter.filter(DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, - EPISODES_PER_PAGE)); + return DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, feedItemFilter); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java index 8dae310ba..39f935bbe 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -383,6 +383,14 @@ public abstract class EpisodesListFragment extends Fragment { @NonNull protected abstract List loadData(); + /** + * Load a new page of data as defined by {@link #page} and {@link #EPISODES_PER_PAGE}. + * If the number of items returned is less than {@link #EPISODES_PER_PAGE}, + * it will be assumed that the underlying data is exhausted + * and this method will not be called again. + * + * @return The items from the next page of data + */ @NonNull protected abstract List loadMoreData(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java index 787f0e5e7..bd30a3953 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.feed; import android.text.TextUtils; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import de.danoeh.antennapod.core.storage.DBReader; @@ -12,77 +13,58 @@ import static de.danoeh.antennapod.core.feed.FeedItem.TAG_FAVORITE; public class FeedItemFilter { - private final String[] mProperties; + private final String[] properties; - private boolean showPlayed = false; - private boolean showUnplayed = false; - private boolean showPaused = false; - private boolean showNotPaused = false; - private boolean showQueued = false; - private boolean showNotQueued = false; - private boolean showDownloaded = false; - private boolean showNotDownloaded = false; - private boolean showHasMedia = false; - private boolean showNoMedia = false; - private boolean showIsFavorite = false; - private boolean showNotFavorite = false; + public final boolean showPlayed; + public final boolean showUnplayed; + public final boolean showPaused; + public final boolean showNotPaused; + public final boolean showQueued; + public final boolean showNotQueued; + public final boolean showDownloaded; + public final boolean showNotDownloaded; + public final boolean showHasMedia; + public final boolean showNoMedia; + public final boolean showIsFavorite; + public final boolean showNotFavorite; + + public static FeedItemFilter unfiltered() { + return new FeedItemFilter(""); + } public FeedItemFilter(String properties) { this(TextUtils.split(properties, ",")); } public FeedItemFilter(String[] properties) { - this.mProperties = properties; - for (String property : properties) { - // see R.arrays.feed_filter_values - switch (property) { - case "unplayed": - showUnplayed = true; - break; - case "paused": - showPaused = true; - break; - case "not_paused": - showNotPaused = true; - break; - case "played": - showPlayed = true; - break; - case "queued": - showQueued = true; - break; - case "not_queued": - showNotQueued = true; - break; - case "downloaded": - showDownloaded = true; - break; - case "not_downloaded": - showNotDownloaded = true; - break; - case "has_media": - showHasMedia = true; - break; - case "no_media": - showNoMedia = true; - break; - case "is_favorite": - showIsFavorite = true; - break; - case "not_favorite": - showNotFavorite = true; - break; - default: - break; - } - } + this.properties = properties; + + // see R.arrays.feed_filter_values + showUnplayed = hasProperty("unplayed"); + showPaused = hasProperty("paused"); + showNotPaused = hasProperty("not_paused"); + showPlayed = hasProperty("played"); + showQueued = hasProperty("queued"); + showNotQueued = hasProperty("not_queued"); + showDownloaded = hasProperty("downloaded"); + showNotDownloaded = hasProperty("not_downloaded"); + showHasMedia = hasProperty("has_media"); + showNoMedia = hasProperty("no_media"); + showIsFavorite = hasProperty("is_favorite"); + showNotFavorite = hasProperty("not_favorite"); + } + + private boolean hasProperty(String property) { + return Arrays.asList(properties).contains(property); } /** * Run a list of feed items through the filter. */ public List filter(List items) { - if(mProperties.length == 0) return items; + if (properties.length == 0) { + return items; + } List result = new ArrayList<>(); @@ -124,11 +106,10 @@ public class FeedItemFilter { } public String[] getValues() { - return mProperties.clone(); + return properties.clone(); } public boolean isShowDownloaded() { return showDownloaded; } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index 2bb0bc574..ee46f4e62 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -17,6 +17,7 @@ import java.util.Map; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedItemFilter; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.feed.SubscriptionsFilter; @@ -366,18 +367,19 @@ public final class DBReader { } /** - * Loads a list of FeedItems sorted by pubDate in descending order. + * Loads a filtered list of FeedItems sorted by pubDate in descending order. * * @param offset The first episode that should be loaded. * @param limit The maximum number of episodes that should be loaded. + * @param filter The filter describing which episodes to filter out. */ @NonNull - public static List getRecentlyPublishedEpisodes(int offset, int limit) { - Log.d(TAG, "getRecentlyPublishedEpisodes() called with: " + "offset = [" + offset + "]" + " limit = [" + limit + "]" ); + public static List getRecentlyPublishedEpisodes(int offset, int limit, FeedItemFilter filter) { + Log.d(TAG, "getRecentlyPublishedEpisodes() called with: offset=" + offset + ", limit=" + limit); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - try (Cursor cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit)) { + try (Cursor cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit, filter)) { List items = extractItemlistFromCursor(adapter, cursor); loadAdditionalFeedItemListData(items); return items; 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 4a4f94053..4a06829ce 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 @@ -16,6 +16,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import de.danoeh.antennapod.core.storage.mapper.FeedItemFilterQuery; import org.apache.commons.io.FileUtils; import java.io.File; @@ -30,6 +31,7 @@ import java.util.Set; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedItemFilter; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -115,14 +117,14 @@ public class PodDBAdapter { public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending"; // Table names - static final String TABLE_NAME_FEEDS = "Feeds"; - static final String TABLE_NAME_FEED_ITEMS = "FeedItems"; - static final String TABLE_NAME_FEED_IMAGES = "FeedImages"; - static final String TABLE_NAME_FEED_MEDIA = "FeedMedia"; - static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog"; - static final String TABLE_NAME_QUEUE = "Queue"; - static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters"; - static final String TABLE_NAME_FAVORITES = "Favorites"; + public static final String TABLE_NAME_FEEDS = "Feeds"; + public static final String TABLE_NAME_FEED_ITEMS = "FeedItems"; + public static final String TABLE_NAME_FEED_IMAGES = "FeedImages"; + public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia"; + public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog"; + public static final String TABLE_NAME_QUEUE = "Queue"; + public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters"; + public static final String TABLE_NAME_FAVORITES = "Favorites"; // SQL Statements for creating new tables private static final String TABLE_PRIMARY_KEY = KEY_ID @@ -1044,9 +1046,11 @@ public class PodDBAdapter { return db.rawQuery(query, null); } - public final Cursor getRecentlyPublishedItemsCursor(int offset, int limit) { - final String query = SELECT_FEED_ITEMS_AND_MEDIA - + "ORDER BY " + KEY_PUBDATE + " DESC LIMIT " + offset + ", " + limit; + public final Cursor getRecentlyPublishedItemsCursor(int offset, int limit, FeedItemFilter filter) { + String filterQuery = FeedItemFilterQuery.generateFrom(filter); + String whereClause = "".equals(filterQuery) ? "" : " WHERE " + filterQuery; + final String query = SELECT_FEED_ITEMS_AND_MEDIA + whereClause + + " ORDER BY " + KEY_PUBDATE + " DESC LIMIT " + offset + ", " + limit; return db.rawQuery(query, null); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java new file mode 100644 index 000000000..f6963b5ac --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java @@ -0,0 +1,76 @@ +package de.danoeh.antennapod.core.storage.mapper; + +import de.danoeh.antennapod.core.feed.FeedItemFilter; +import de.danoeh.antennapod.core.storage.PodDBAdapter; + +import java.util.ArrayList; +import java.util.List; + +public class FeedItemFilterQuery { + private FeedItemFilterQuery() { + // Must not be instantiated + } + + /** + * Express the filter using an SQL boolean statement that can be inserted into an SQL WHERE clause + * to yield output filtered according to the rules of this filter. + * + * @return An SQL boolean statement that matches the desired items, + * empty string if there is nothing to filter + */ + public static String generateFrom(FeedItemFilter filter) { + // The keys used within this method, but explicitly combined with their table + String keyRead = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_READ; + String keyPosition = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_POSITION; + String keyDownloaded = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_DOWNLOADED; + String keyMediaId = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_ID; + String keyItemId = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_ID; + String keyFeedItem = PodDBAdapter.KEY_FEEDITEM; + String tableQueue = PodDBAdapter.TABLE_NAME_QUEUE; + String tableFavorites = PodDBAdapter.TABLE_NAME_FAVORITES; + + List statements = new ArrayList<>(); + if (filter.showPlayed) { + statements.add(keyRead + " = 1 "); + } else if (filter.showUnplayed) { + statements.add(" NOT " + keyRead + " = 1 "); // Match "New" items (read = -1) as well + } + if (filter.showPaused) { + statements.add(" (" + keyPosition + " NOT NULL AND " + keyPosition + " > 0 " + ") "); + } else if (filter.showNotPaused) { + statements.add(" (" + keyPosition + " IS NULL OR " + keyPosition + " = 0 " + ") "); + } + if (filter.showQueued) { + statements.add(keyItemId + " IN (SELECT " + keyFeedItem + " FROM " + tableQueue + ") "); + } else if (filter.showNotQueued) { + statements.add(keyItemId + " NOT IN (SELECT " + keyFeedItem + " FROM " + tableQueue + ") "); + } + if (filter.showDownloaded) { + statements.add(keyDownloaded + " = 1 "); + } else if (filter.showNotDownloaded) { + statements.add(keyDownloaded + " = 0 "); + } + if (filter.showHasMedia) { + statements.add(keyMediaId + " NOT NULL "); + } else if (filter.showNoMedia) { + statements.add(keyMediaId + " IS NULL "); + } + if (filter.showIsFavorite) { + statements.add(keyItemId + " IN (SELECT " + keyFeedItem + " FROM " + tableFavorites + ") "); + } else if (filter.showNotFavorite) { + statements.add(keyItemId + " NOT IN (SELECT " + keyFeedItem + " FROM " + tableFavorites + ") "); + } + + if (statements.isEmpty()) { + return ""; + } + + StringBuilder query = new StringBuilder(" (" + statements.get(0)); + for (String r : statements.subList(1, statements.size())) { + query.append(" AND "); + query.append(r); + } + query.append(") "); + return query.toString(); + } +}