summaryrefslogtreecommitdiff
path: root/storage/database/src/main
diff options
context:
space:
mode:
authorByteHamster <ByteHamster@users.noreply.github.com>2024-03-25 21:45:43 +0100
committerGitHub <noreply@github.com>2024-03-25 21:45:43 +0100
commit69b24699a36bc9c4ffc6200fbcf8069f7a94c1ab (patch)
tree5bfb94e00ff3d296412bc2624fbb43fc455d1471 /storage/database/src/main
parent15eab50223e7789c15e40f956ee1ba166209b11d (diff)
downloadAntennaPod-69b24699a36bc9c4ffc6200fbcf8069f7a94c1ab.zip
Move DBReader to :storage:database (#7027)
Diffstat (limited to 'storage/database/src/main')
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBReader.java860
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemPermutors.java200
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/LongList.java273
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/NavDrawerData.java105
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/Permutor.java17
-rw-r--r--storage/database/src/main/java/de/danoeh/antennapod/storage/database/StatisticsItem.java45
6 files changed, 1500 insertions, 0 deletions
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBReader.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBReader.java
new file mode 100644
index 000000000..89967c24b
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBReader.java
@@ -0,0 +1,860 @@
+package de.danoeh.antennapod.storage.database;
+
+import android.database.Cursor;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.collection.ArrayMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.danoeh.antennapod.model.feed.Chapter;
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedCounter;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.model.feed.FeedOrder;
+import de.danoeh.antennapod.model.feed.FeedPreferences;
+import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.model.feed.SubscriptionsFilter;
+import de.danoeh.antennapod.model.download.DownloadResult;
+import de.danoeh.antennapod.storage.database.mapper.ChapterCursorMapper;
+import de.danoeh.antennapod.storage.database.mapper.DownloadResultCursorMapper;
+import de.danoeh.antennapod.storage.database.mapper.FeedCursorMapper;
+import de.danoeh.antennapod.storage.database.mapper.FeedItemCursorMapper;
+import de.danoeh.antennapod.storage.database.mapper.FeedMediaCursorMapper;
+import de.danoeh.antennapod.storage.database.mapper.FeedPreferencesCursorMapper;
+
+/**
+ * Provides methods for reading data from the AntennaPod database.
+ * In general, all database calls in DBReader-methods are executed on the caller's thread.
+ * This means that the caller should make sure that DBReader-methods are not executed on the GUI-thread.
+ */
+public final class DBReader {
+
+ private static final String TAG = "DBReader";
+
+ /**
+ * Maximum size of the list returned by {@link #getDownloadLog()}.
+ */
+ private static final int DOWNLOAD_LOG_SIZE = 200;
+
+
+ private DBReader() {
+ }
+
+ /**
+ * Returns a list of Feeds, sorted alphabetically by their title.
+ *
+ * @return A list of Feeds, sorted alphabetically by their title. A Feed-object
+ * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list
+ * can be loaded separately with {@link #getFeedItemList(Feed)}.
+ */
+ @NonNull
+ public static List<Feed> getFeedList() {
+ Log.d(TAG, "Extracting Feedlist");
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try {
+ return getFeedList(adapter);
+ } finally {
+ adapter.close();
+ }
+ }
+
+ @NonNull
+ private static List<Feed> getFeedList(PodDBAdapter adapter) {
+ try (Cursor cursor = adapter.getAllFeedsCursor()) {
+ List<Feed> feeds = new ArrayList<>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ Feed feed = extractFeedFromCursorRow(cursor);
+ feeds.add(feed);
+ }
+ return feeds;
+ }
+ }
+
+ /**
+ * Returns a list with the download URLs of all feeds.
+ *
+ * @return A list of Strings with the download URLs of all feeds.
+ */
+ public static List<String> getFeedListDownloadUrls() {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getFeedCursorDownloadUrls()) {
+ List<String> result = new ArrayList<>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ String url = cursor.getString(1);
+ if (url != null && !url.startsWith(Feed.PREFIX_LOCAL_FOLDER)) {
+ result.add(url);
+ }
+ }
+ return result;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ /**
+ * Loads additional data in to the feed items from other database queries
+ *
+ * @param items the FeedItems who should have other data loaded
+ */
+ public static void loadAdditionalFeedItemListData(List<FeedItem> items) {
+ loadTagsOfFeedItemList(items);
+ loadFeedDataOfFeedItemList(items);
+ }
+
+ private static void loadTagsOfFeedItemList(List<FeedItem> items) {
+ LongList favoriteIds = getFavoriteIDList();
+ LongList queueIds = getQueueIDList();
+
+ for (FeedItem item : items) {
+ if (favoriteIds.contains(item.getId())) {
+ item.addTag(FeedItem.TAG_FAVORITE);
+ }
+ if (queueIds.contains(item.getId())) {
+ item.addTag(FeedItem.TAG_QUEUE);
+ }
+ }
+ }
+
+ /**
+ * Takes a list of FeedItems and loads their corresponding Feed-objects from the database.
+ * The feedID-attribute of a FeedItem must be set to the ID of its feed or the method will
+ * not find the correct feed of an item.
+ *
+ * @param items The FeedItems whose Feed-objects should be loaded.
+ */
+ private static void loadFeedDataOfFeedItemList(List<FeedItem> items) {
+ List<Feed> feeds = getFeedList();
+
+ Map<Long, Feed> feedIndex = new ArrayMap<>(feeds.size());
+ for (Feed feed : feeds) {
+ feedIndex.put(feed.getId(), feed);
+ }
+ for (FeedItem item : items) {
+ Feed feed = feedIndex.get(item.getFeedId());
+ if (feed == null) {
+ Log.w(TAG, "No match found for item with ID " + item.getId() + ". Feed ID was " + item.getFeedId());
+ feed = new Feed("", "", "Error: Item without feed");
+ }
+ item.setFeed(feed);
+ }
+ }
+
+ /**
+ * Loads the list of FeedItems for a certain Feed-object.
+ * This method should NOT be used if the FeedItems are not used.
+ *
+ * @param feed The Feed whose items should be loaded
+ * @return A list with the FeedItems of the Feed. The Feed-attribute of the FeedItems will already be set correctly.
+ * The method does NOT change the items-attribute of the feed.
+ */
+ public static List<FeedItem> getFeedItemList(final Feed feed) {
+ return getFeedItemList(feed, FeedItemFilter.unfiltered());
+ }
+
+ public static List<FeedItem> getFeedItemList(final Feed feed, final FeedItemFilter filter) {
+ return getFeedItemList(feed, filter, SortOrder.DATE_NEW_OLD);
+ }
+
+ public static List<FeedItem> getFeedItemList(final Feed feed, final FeedItemFilter filter, SortOrder sortOrder) {
+ Log.d(TAG, "getFeedItemList() called with: " + "feed = [" + feed + "]");
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getItemsOfFeedCursor(feed, filter)) {
+ List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
+ FeedItemPermutors.getPermutor(sortOrder).reorder(items);
+ feed.setItems(items);
+ for (FeedItem item : items) {
+ item.setFeed(feed);
+ }
+ return items;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ public static List<FeedItem> extractItemlistFromCursor(Cursor itemlistCursor) {
+ Log.d(TAG, "extractItemlistFromCursor() called with: " + "itemlistCursor = [" + itemlistCursor + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try {
+ return extractItemlistFromCursor(adapter, itemlistCursor);
+ } finally {
+ adapter.close();
+ }
+ }
+
+ @NonNull
+ private static List<FeedItem> extractItemlistFromCursor(PodDBAdapter adapter, Cursor cursor) {
+ List<FeedItem> result = new ArrayList<>(cursor.getCount());
+ if (cursor.moveToFirst()) {
+ int indexMediaId = cursor.getColumnIndexOrThrow(PodDBAdapter.SELECT_KEY_MEDIA_ID);
+ do {
+ FeedItem item = FeedItemCursorMapper.convert(cursor);
+ result.add(item);
+ if (!cursor.isNull(indexMediaId)) {
+ item.setMedia(FeedMediaCursorMapper.convert(cursor));
+ }
+ } while (cursor.moveToNext());
+ }
+ return result;
+ }
+
+ private static Feed extractFeedFromCursorRow(Cursor cursor) {
+ Feed feed = FeedCursorMapper.convert(cursor);
+ FeedPreferences preferences = FeedPreferencesCursorMapper.convert(cursor);
+ feed.setPreferences(preferences);
+ return feed;
+ }
+
+ @NonNull
+ public static List<FeedItem> getQueue(PodDBAdapter adapter) {
+ Log.d(TAG, "getQueue()");
+ try (Cursor cursor = adapter.getQueueCursor()) {
+ List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
+ loadAdditionalFeedItemListData(items);
+ return items;
+ }
+ }
+
+ /**
+ * Loads the IDs of the FeedItems in the queue. This method should be preferred over
+ * {@link #getQueue()} if the FeedItems of the queue are not needed.
+ *
+ * @return A list of IDs sorted by the same order as the queue.
+ */
+ public static LongList getQueueIDList() {
+ Log.d(TAG, "getQueueIDList() called");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try {
+ return getQueueIDList(adapter);
+ } finally {
+ adapter.close();
+ }
+ }
+
+ private static LongList getQueueIDList(PodDBAdapter adapter) {
+ try (Cursor cursor = adapter.getQueueIDCursor()) {
+ LongList queueIds = new LongList(cursor.getCount());
+ while (cursor.moveToNext()) {
+ queueIds.add(cursor.getLong(0));
+ }
+ return queueIds;
+ }
+ }
+
+ /**
+ * Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using
+ * {@link #getQueueIDList()} instead.
+ *
+ * @return A list of FeedItems sorted by the same order as the queue.
+ */
+ @NonNull
+ public static List<FeedItem> getQueue() {
+ Log.d(TAG, "getQueue() called");
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try {
+ return getQueue(adapter);
+ } finally {
+ adapter.close();
+ }
+ }
+
+ private static LongList getFavoriteIDList() {
+ Log.d(TAG, "getFavoriteIDList() called");
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getFavoritesIdsCursor(0, Integer.MAX_VALUE)) {
+ LongList favoriteIDs = new LongList(cursor.getCount());
+ while (cursor.moveToNext()) {
+ favoriteIDs.add(cursor.getLong(0));
+ }
+ return favoriteIDs;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ /**
+ *
+ * @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<FeedItem> getEpisodes(int offset, int limit, FeedItemFilter filter, SortOrder sortOrder) {
+ Log.d(TAG, "getRecentlyPublishedEpisodes() called with: offset=" + offset + ", limit=" + limit);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getEpisodesCursor(offset, limit, filter, sortOrder)) {
+ List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
+ loadAdditionalFeedItemListData(items);
+ return items;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ public static int getTotalEpisodeCount(FeedItemFilter filter) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getEpisodeCountCursor(filter)) {
+ if (cursor.moveToFirst()) {
+ return cursor.getInt(0);
+ }
+ return -1;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ public static List<FeedItem> getRandomEpisodes(int limit, int seed) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getRandomEpisodesCursor(limit, seed)) {
+ List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
+ loadAdditionalFeedItemListData(items);
+ return items;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ /**
+ * Loads the download log from the database.
+ *
+ * @return A list with DownloadStatus objects that represent the download log.
+ * The size of the returned list is limited by {@link #DOWNLOAD_LOG_SIZE}.
+ */
+ public static List<DownloadResult> getDownloadLog() {
+ Log.d(TAG, "getDownloadLog() called");
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE)) {
+ List<DownloadResult> downloadLog = new ArrayList<>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ downloadLog.add(DownloadResultCursorMapper.convert(cursor));
+ }
+ return downloadLog;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ /**
+ * Loads the download log for a particular feed from the database.
+ *
+ * @param feedId Feed id for which the download log is loaded
+ * @return A list with DownloadStatus objects that represent the feed's download log,
+ * newest events first.
+ */
+ public static List<DownloadResult> getFeedDownloadLog(long feedId) {
+ Log.d(TAG, "getFeedDownloadLog() called with: " + "feed = [" + feedId + "]");
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feedId)) {
+ List<DownloadResult> downloadLog = new ArrayList<>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ downloadLog.add(DownloadResultCursorMapper.convert(cursor));
+ }
+ return downloadLog;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ /**
+ * Loads a specific Feed from the database.
+ *
+ * @param feedId The ID of the Feed
+ * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the
+ * database and the items-attribute will be set correctly.
+ */
+ @Nullable
+ public static Feed getFeed(final long feedId) {
+ return getFeed(feedId, false);
+ }
+
+ /**
+ * Loads a specific Feed from the database.
+ *
+ * @param feedId The ID of the Feed
+ * @param filtered <code>true</code> if only the visible items should be loaded according to the feed filter.
+ * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the
+ * database and the items-attribute will be set correctly.
+ */
+ @Nullable
+ public static Feed getFeed(final long feedId, boolean filtered) {
+ Log.d(TAG, "getFeed() called with: " + "feedId = [" + feedId + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Feed feed = null;
+ try (Cursor cursor = adapter.getFeedCursor(feedId)) {
+ if (cursor.moveToNext()) {
+ feed = extractFeedFromCursorRow(cursor);
+ if (filtered) {
+ feed.setItems(getFeedItemList(feed, feed.getItemFilter()));
+ } else {
+ feed.setItems(getFeedItemList(feed));
+ }
+ } else {
+ Log.e(TAG, "getFeed could not find feed with id " + feedId);
+ }
+ return feed;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ @Nullable
+ private static FeedItem getFeedItem(final long itemId, PodDBAdapter adapter) {
+ Log.d(TAG, "Loading feeditem with id " + itemId);
+
+ FeedItem item = null;
+ try (Cursor cursor = adapter.getFeedItemCursor(Long.toString(itemId))) {
+ if (cursor.moveToNext()) {
+ List<FeedItem> list = extractItemlistFromCursor(adapter, cursor);
+ if (!list.isEmpty()) {
+ item = list.get(0);
+ loadAdditionalFeedItemListData(list);
+ }
+ }
+ return item;
+ }
+ }
+
+ /**
+ * Loads a specific FeedItem from the database. This method should not be used for loading more
+ * than one FeedItem because this method might query the database several times for each item.
+ *
+ * @param itemId The ID of the FeedItem
+ * @return The FeedItem or null if the FeedItem could not be found.
+ */
+ @Nullable
+ public static FeedItem getFeedItem(final long itemId) {
+ Log.d(TAG, "getFeedItem() called with: " + "itemId = [" + itemId + "]");
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try {
+ return getFeedItem(itemId, adapter);
+ } finally {
+ adapter.close();
+ }
+ }
+
+ /**
+ * Get next feed item in queue following a particular feeditem
+ *
+ * @param item The FeedItem
+ * @return The FeedItem next in queue or null if the FeedItem could not be found.
+ */
+ @Nullable
+ public static FeedItem getNextInQueue(FeedItem item) {
+ Log.d(TAG, "getNextInQueue() called with: " + "itemId = [" + item.getId() + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try {
+ FeedItem nextItem = null;
+ try (Cursor cursor = adapter.getNextInQueue(item)) {
+ List<FeedItem> list = extractItemlistFromCursor(adapter, cursor);
+ if (!list.isEmpty()) {
+ nextItem = list.get(0);
+ loadAdditionalFeedItemListData(list);
+ }
+ return nextItem;
+ } catch (Exception e) {
+ return null;
+ }
+ } finally {
+ adapter.close();
+ }
+ }
+
+ @NonNull
+ public static List<FeedItem> getPausedQueue(int limit) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getPausedQueueCursor(limit)) {
+ List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
+ loadAdditionalFeedItemListData(items);
+ return items;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ /**
+ * Loads a specific FeedItem from the database.
+ *
+ * @param guid feed item guid
+ * @param episodeUrl the feed item's url
+ * @return The FeedItem or null if the FeedItem could not be found.
+ * Does NOT load additional attributes like feed or queue state.
+ */
+ @Nullable
+ private static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl,
+ PodDBAdapter adapter) {
+ try (Cursor cursor = adapter.getFeedItemCursor(guid, episodeUrl)) {
+ if (!cursor.moveToNext()) {
+ return null;
+ }
+ List<FeedItem> list = extractItemlistFromCursor(adapter, cursor);
+ if (!list.isEmpty()) {
+ return list.get(0);
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Loads a specific FeedItem from the database.
+ *
+ * @param guid feed item guid
+ * @param episodeUrl the feed item's url
+ * @return The FeedItem or null if the FeedItem could not be found.
+ * Does NOT load additional attributes like feed or queue state.
+ */
+ public static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try {
+ return getFeedItemByGuidOrEpisodeUrl(guid, episodeUrl, adapter);
+ } finally {
+ adapter.close();
+ }
+ }
+
+ /**
+ * Loads shownotes information about a FeedItem.
+ *
+ * @param item The FeedItem
+ */
+ public static void loadDescriptionOfFeedItem(final FeedItem item) {
+ Log.d(TAG, "loadDescriptionOfFeedItem() called with: " + "item = [" + item + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getDescriptionOfItem(item)) {
+ if (cursor.moveToFirst()) {
+ int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION);
+ String description = cursor.getString(indexDescription);
+ item.setDescriptionIfLonger(description);
+ }
+ } finally {
+ adapter.close();
+ }
+ }
+
+ /**
+ * Loads the list of chapters that belongs to this FeedItem if available. This method overwrites
+ * any chapters that this FeedItem has. If no chapters were found in the database, the chapters
+ * reference of the FeedItem will be set to null.
+ *
+ * @param item The FeedItem
+ */
+ public static List<Chapter> loadChaptersOfFeedItem(final FeedItem item) {
+ Log.d(TAG, "loadChaptersOfFeedItem() called with: " + "item = [" + item + "]");
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try {
+ return loadChaptersOfFeedItem(adapter, item);
+ } finally {
+ adapter.close();
+ }
+ }
+
+ private static List<Chapter> loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) {
+ try (Cursor cursor = adapter.getSimpleChaptersOfFeedItemCursor(item)) {
+ int chaptersCount = cursor.getCount();
+ if (chaptersCount == 0) {
+ item.setChapters(null);
+ return null;
+ }
+ ArrayList<Chapter> chapters = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ chapters.add(ChapterCursorMapper.convert(cursor));
+ }
+ return chapters;
+ }
+ }
+
+ /**
+ * Searches the DB for a FeedMedia of the given id.
+ *
+ * @param mediaId The id of the object
+ * @return The found object, or null if it does not exist
+ */
+ @Nullable
+ public static FeedMedia getFeedMedia(final long mediaId) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+
+ try (Cursor mediaCursor = adapter.getSingleFeedMediaCursor(mediaId)) {
+ if (!mediaCursor.moveToFirst()) {
+ return null;
+ }
+
+ int indexFeedItem = mediaCursor.getColumnIndex(PodDBAdapter.KEY_FEEDITEM);
+ long itemId = mediaCursor.getLong(indexFeedItem);
+ FeedMedia media = FeedMediaCursorMapper.convert(mediaCursor);
+ FeedItem item = getFeedItem(itemId);
+ if (item != null) {
+ media.setItem(item);
+ item.setMedia(media);
+ }
+ return media;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ public static List<FeedItem> getFeedItemsWithUrl(List<String> urls) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor itemCursor = adapter.getFeedItemCursorByUrl(urls)) {
+ List<FeedItem> items = extractItemlistFromCursor(adapter, itemCursor);
+ loadAdditionalFeedItemListData(items);
+ return items;
+ } finally {
+ adapter.close();
+ }
+ }
+
+ public static class MonthlyStatisticsItem {
+ private int year = 0;
+ private int month = 0;
+ private long timePlayed = 0;
+
+ public int getYear() {
+ return year;
+ }
+
+ public void setYear(final int year) {
+ this.year = year;
+ }
+
+ public int getMonth() {
+ return month;
+ }
+
+ public void setMonth(final int month) {
+ this.month = month;
+ }
+
+ public long getTimePlayed() {
+ return timePlayed;
+ }
+
+ public void setTimePlayed(final long timePlayed) {
+ this.timePlayed = timePlayed;
+ }
+ }
+
+ @NonNull
+ public static List<MonthlyStatisticsItem> getMonthlyTimeStatistics() {
+ List<MonthlyStatisticsItem> months = new ArrayList<>();
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getMonthlyStatisticsCursor()) {
+ int indexMonth = cursor.getColumnIndexOrThrow("month");
+ int indexYear = cursor.getColumnIndexOrThrow("year");
+ int indexTotalDuration = cursor.getColumnIndexOrThrow("total_duration");
+ while (cursor.moveToNext()) {
+ MonthlyStatisticsItem item = new MonthlyStatisticsItem();
+ item.setMonth(Integer.parseInt(cursor.getString(indexMonth)));
+ item.setYear(Integer.parseInt(cursor.getString(indexYear)));
+ item.setTimePlayed(cursor.getLong(indexTotalDuration));
+ months.add(item);
+ }
+ }
+ adapter.close();
+ return months;
+ }
+
+ public static class StatisticsResult {
+ public List<StatisticsItem> feedTime = new ArrayList<>();
+ public long oldestDate = System.currentTimeMillis();
+ }
+
+ /**
+ * Searches the DB for statistics.
+ *
+ * @return The list of statistics objects
+ */
+ @NonNull
+ public static StatisticsResult getStatistics(boolean includeMarkedAsPlayed,
+ long timeFilterFrom, long timeFilterTo) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+
+ StatisticsResult result = new StatisticsResult();
+ try (Cursor cursor = adapter.getFeedStatisticsCursor(includeMarkedAsPlayed, timeFilterFrom, timeFilterTo)) {
+ int indexOldestDate = cursor.getColumnIndexOrThrow("oldest_date");
+ int indexNumEpisodes = cursor.getColumnIndexOrThrow("num_episodes");
+ int indexEpisodesStarted = cursor.getColumnIndexOrThrow("episodes_started");
+ int indexTotalTime = cursor.getColumnIndexOrThrow("total_time");
+ int indexPlayedTime = cursor.getColumnIndexOrThrow("played_time");
+ int indexNumDownloaded = cursor.getColumnIndexOrThrow("num_downloaded");
+ int indexDownloadSize = cursor.getColumnIndexOrThrow("download_size");
+
+ while (cursor.moveToNext()) {
+ Feed feed = extractFeedFromCursorRow(cursor);
+
+ long feedPlayedTime = Long.parseLong(cursor.getString(indexPlayedTime)) / 1000;
+ long feedTotalTime = Long.parseLong(cursor.getString(indexTotalTime)) / 1000;
+ long episodes = Long.parseLong(cursor.getString(indexNumEpisodes));
+ long episodesStarted = Long.parseLong(cursor.getString(indexEpisodesStarted));
+ long totalDownloadSize = Long.parseLong(cursor.getString(indexDownloadSize));
+ long episodesDownloadCount = Long.parseLong(cursor.getString(indexNumDownloaded));
+ long oldestDate = Long.parseLong(cursor.getString(indexOldestDate));
+
+ if (episodes > 0 && oldestDate < Long.MAX_VALUE) {
+ result.oldestDate = Math.min(result.oldestDate, oldestDate);
+ }
+
+ result.feedTime.add(new StatisticsItem(feed, feedTotalTime, feedPlayedTime, episodes,
+ episodesStarted, totalDownloadSize, episodesDownloadCount));
+ }
+ }
+ adapter.close();
+ return result;
+ }
+
+ public static long getTimeBetweenReleaseAndPlayback(long timeFilterFrom, long timeFilterTo) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ try (Cursor cursor = adapter.getTimeBetweenReleaseAndPlayback(timeFilterFrom, timeFilterTo)) {
+ cursor.moveToFirst();
+ long result = Long.parseLong(cursor.getString(0));
+ adapter.close();
+ return result;
+ }
+ }
+
+ /**
+ * Returns data necessary for displaying the navigation drawer. This includes
+ * the list of subscriptions, the number of items in the queue and the number of unread
+ * items.
+ */
+ @NonNull
+ public static NavDrawerData getNavDrawerData(@Nullable SubscriptionsFilter subscriptionsFilter,
+ FeedOrder feedOrder, FeedCounter feedCounter) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+
+ final Map<Long, Integer> feedCounters = adapter.getFeedCounters(feedCounter);
+ List<Feed> feeds = getFeedList(adapter);
+
+ if (subscriptionsFilter != null) {
+ feeds = subscriptionsFilter.filter(feeds, feedCounters);
+ }
+
+ Comparator<Feed> comparator;
+ switch (feedOrder) {
+ case COUNTER:
+ comparator = (lhs, rhs) -> {
+ long counterLhs = feedCounters.containsKey(lhs.getId()) ? feedCounters.get(lhs.getId()) : 0;
+ long counterRhs = feedCounters.containsKey(rhs.getId()) ? feedCounters.get(rhs.getId()) : 0;
+ if (counterLhs > counterRhs) {
+ // reverse natural order: podcast with most unplayed episodes first
+ return -1;
+ } else if (counterLhs == counterRhs) {
+ return lhs.getTitle().compareToIgnoreCase(rhs.getTitle());
+ } else {
+ return 1;
+ }
+ };
+ break;
+ case ALPHABETICAL:
+ comparator = (lhs, rhs) -> {
+ String t1 = lhs.getTitle();
+ String t2 = rhs.getTitle();
+ if (t1 == null) {
+ return 1;
+ } else if (t2 == null) {
+ return -1;
+ } else {
+ return t1.compareToIgnoreCase(t2);
+ }
+ };
+ break;
+ case MOST_PLAYED:
+ final Map<Long, Integer> playedCounters = adapter.getPlayedEpisodesCounters();
+ comparator = (lhs, rhs) -> {
+ long counterLhs = playedCounters.containsKey(lhs.getId()) ? playedCounters.get(lhs.getId()) : 0;
+ long counterRhs = playedCounters.containsKey(rhs.getId()) ? playedCounters.get(rhs.getId()) : 0;
+ if (counterLhs > counterRhs) {
+ // podcast with most played episodes first
+ return -1;
+ } else if (counterLhs == counterRhs) {
+ return lhs.getTitle().compareToIgnoreCase(rhs.getTitle());
+ } else {
+ return 1;
+ }
+ };
+ break;
+ default:
+ final Map<Long, Long> recentPubDates = adapter.getMostRecentItemDates();
+ comparator = (lhs, rhs) -> {
+ long dateLhs = recentPubDates.containsKey(lhs.getId()) ? recentPubDates.get(lhs.getId()) : 0;
+ long dateRhs = recentPubDates.containsKey(rhs.getId()) ? recentPubDates.get(rhs.getId()) : 0;
+ return Long.compare(dateRhs, dateLhs);
+ };
+ break;
+ }
+
+ Collections.sort(feeds, comparator);
+ final int queueSize = adapter.getQueueSize();
+ final int numNewItems = getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.NEW));
+ final int numDownloadedItems = getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED));
+
+ List<NavDrawerData.DrawerItem> items = new ArrayList<>();
+ Map<String, NavDrawerData.TagDrawerItem> folders = new HashMap<>();
+ for (Feed feed : feeds) {
+ for (String tag : feed.getPreferences().getTags()) {
+ int counter = feedCounters.containsKey(feed.getId()) ? feedCounters.get(feed.getId()) : 0;
+ NavDrawerData.FeedDrawerItem drawerItem = new NavDrawerData.FeedDrawerItem(feed, feed.getId(), counter);
+ if (FeedPreferences.TAG_ROOT.equals(tag)) {
+ items.add(drawerItem);
+ continue;
+ }
+ NavDrawerData.TagDrawerItem folder;
+ if (folders.containsKey(tag)) {
+ folder = folders.get(tag);
+ } else {
+ folder = new NavDrawerData.TagDrawerItem(tag);
+ folders.put(tag, folder);
+ }
+ drawerItem.id |= folder.id;
+ folder.children.add(drawerItem);
+ }
+ }
+ List<NavDrawerData.TagDrawerItem> foldersSorted = new ArrayList<>(folders.values());
+ Collections.sort(foldersSorted, (o1, o2) -> o1.getTitle().compareToIgnoreCase(o2.getTitle()));
+ items.addAll(foldersSorted);
+
+ NavDrawerData result = new NavDrawerData(items, queueSize, numNewItems, numDownloadedItems, feedCounters);
+ adapter.close();
+ return result;
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemPermutors.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemPermutors.java
new file mode 100644
index 000000000..a9c59e2bb
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemPermutors.java
@@ -0,0 +1,200 @@
+package de.danoeh.antennapod.storage.database;
+
+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.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.SortOrder;
+
+/**
+ * Provides method for sorting the a list of {@link FeedItem} according to rules.
+ */
+public class FeedItemPermutors {
+
+ /**
+ * Returns a Permutor that sorts a list appropriate to the given sort order.
+ *
+ * @return Permutor that sorts a list appropriate to the given sort order.
+ */
+ @NonNull
+ public static Permutor<FeedItem> getPermutor(@NonNull SortOrder sortOrder) {
+
+ Comparator<FeedItem> comparator = null;
+ Permutor<FeedItem> permutor = null;
+
+ switch (sortOrder) {
+ case EPISODE_TITLE_A_Z:
+ comparator = (f1, f2) -> itemTitle(f1).compareTo(itemTitle(f2));
+ break;
+ case EPISODE_TITLE_Z_A:
+ comparator = (f1, f2) -> itemTitle(f2).compareTo(itemTitle(f1));
+ break;
+ case DATE_OLD_NEW:
+ comparator = (f1, f2) -> pubDate(f1).compareTo(pubDate(f2));
+ break;
+ case DATE_NEW_OLD:
+ comparator = (f1, f2) -> pubDate(f2).compareTo(pubDate(f1));
+ break;
+ case DURATION_SHORT_LONG:
+ comparator = (f1, f2) -> Integer.compare(duration(f1), duration(f2));
+ break;
+ case DURATION_LONG_SHORT:
+ comparator = (f1, f2) -> Integer.compare(duration(f2), duration(f1));
+ break;
+ case EPISODE_FILENAME_A_Z:
+ comparator = (f1, f2) -> itemLink(f1).compareTo(itemLink(f2));
+ break;
+ case EPISODE_FILENAME_Z_A:
+ comparator = (f1, f2) -> itemLink(f2).compareTo(itemLink(f1));
+ break;
+ case FEED_TITLE_A_Z:
+ comparator = (f1, f2) -> feedTitle(f1).compareTo(feedTitle(f2));
+ break;
+ case FEED_TITLE_Z_A:
+ comparator = (f1, f2) -> feedTitle(f2).compareTo(feedTitle(f1));
+ break;
+ case RANDOM:
+ permutor = Collections::shuffle;
+ break;
+ case SMART_SHUFFLE_OLD_NEW:
+ permutor = (queue) -> smartShuffle(queue, true);
+ break;
+ case SMART_SHUFFLE_NEW_OLD:
+ permutor = (queue) -> smartShuffle(queue, false);
+ break;
+ case SIZE_SMALL_LARGE:
+ comparator = (f1, f2) -> Long.compare(size(f1), size(f2));
+ break;
+ case SIZE_LARGE_SMALL:
+ comparator = (f1, f2) -> Long.compare(size(f2), size(f1));
+ break;
+ case COMPLETION_DATE_NEW_OLD:
+ comparator = (f1, f2) -> f2.getMedia().getPlaybackCompletionDate()
+ .compareTo(f1.getMedia().getPlaybackCompletionDate());
+ break;
+ default:
+ throw new IllegalArgumentException("Permutor not implemented");
+ }
+
+ if (comparator != null) {
+ final Comparator<FeedItem> comparator2 = comparator;
+ permutor = (queue) -> Collections.sort(queue, comparator2);
+ }
+ 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().toLowerCase(Locale.getDefault()) : "";
+ }
+
+ private static int duration(@Nullable FeedItem item) {
+ return (item != null && item.getMedia() != null) ? item.getMedia().getDuration() : 0;
+ }
+
+ private static long size(@Nullable FeedItem item) {
+ return (item != null && item.getMedia() != null) ? item.getMedia().getSize() : 0;
+ }
+
+ @NonNull
+ private static String itemLink(@Nullable FeedItem item) {
+ return (item != null && item.getLink() != null)
+ ? item.getLink().toLowerCase(Locale.getDefault()) : "";
+ }
+
+ @NonNull
+ private static String feedTitle(@Nullable FeedItem item) {
+ return (item != null && item.getFeed() != null && item.getFeed().getTitle() != null)
+ ? item.getFeed().getTitle().toLowerCase(Locale.getDefault()) : "";
+ }
+
+ /**
+ * Implements a reordering by pubdate that avoids consecutive episodes from the same feed in
+ * the queue.
+ *
+ * A listener might want to hear episodes from any given feed in pubdate order, but would
+ * prefer a more balanced ordering that avoids having to listen to clusters of consecutive
+ * episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
+ *
+ * Assume the queue looks like this: `ABCDDEEEEEEEEEE`.
+ * This method first starts with a queue of the final size, where each slot is empty (null).
+ * It takes the podcast with most episodes (`E`) and places the episodes spread out in the queue: `EE_E_EE_E_EE_EE`.
+ * The podcast with the second-most number of episodes (`D`) is then
+ * placed spread-out in the *available* slots: `EE_EDEE_EDEE_EE`.
+ * This continues, until we end up with: `EEBEDEECEDEEAEE`.
+ *
+ * Note that episodes aren't strictly ordered in terms of pubdate, but episodes of each feed are.
+ *
+ * @param queue A (modifiable) list of FeedItem elements to be reordered.
+ * @param ascending {@code true} to use ascending pubdate in the reordering;
+ * {@code false} for descending.
+ */
+ private static void smartShuffle(List<FeedItem> queue, boolean ascending) {
+ // Divide FeedItems into lists by feed
+ Map<Long, List<FeedItem>> map = new HashMap<>();
+ for (FeedItem item : queue) {
+ Long id = item.getFeedId();
+ if (!map.containsKey(id)) {
+ map.put(id, new ArrayList<>());
+ }
+ map.get(id).add(item);
+ }
+
+ // Sort each individual list by PubDate (ascending/descending)
+ Comparator<FeedItem> itemComparator = ascending
+ ? (f1, f2) -> f1.getPubDate().compareTo(f2.getPubDate())
+ : (f1, f2) -> f2.getPubDate().compareTo(f1.getPubDate());
+ List<List<FeedItem>> feeds = new ArrayList<>();
+ for (Map.Entry<Long, List<FeedItem>> mapEntry : map.entrySet()) {
+ Collections.sort(mapEntry.getValue(), itemComparator);
+ feeds.add(mapEntry.getValue());
+ }
+
+ ArrayList<Integer> emptySlots = new ArrayList<>();
+ for (int i = 0; i < queue.size(); i++) {
+ queue.set(i, null);
+ emptySlots.add(i);
+ }
+
+ // Starting with the largest feed, place items spread out through the empty slots in the queue
+ Collections.sort(feeds, (f1, f2) -> Integer.compare(f2.size(), f1.size()));
+ for (List<FeedItem> feedItems : feeds) {
+ double spread = (double) emptySlots.size() / (feedItems.size() + 1);
+ Iterator<Integer> emptySlotIterator = emptySlots.iterator();
+ int skipped = 0;
+ int placed = 0;
+ while (emptySlotIterator.hasNext()) {
+ int nextEmptySlot = emptySlotIterator.next();
+ skipped++;
+ if (skipped >= spread * (placed + 1)) {
+ if (queue.get(nextEmptySlot) != null) {
+ throw new RuntimeException("Slot to be placed in not empty");
+ }
+ queue.set(nextEmptySlot, feedItems.get(placed));
+ emptySlotIterator.remove();
+ placed++;
+ if (placed == feedItems.size()) {
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/LongList.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/LongList.java
new file mode 100644
index 000000000..23ec9ade9
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/LongList.java
@@ -0,0 +1,273 @@
+package de.danoeh.antennapod.storage.database;
+
+import java.util.Arrays;
+
+/**
+ * Fast and memory efficient long list
+ */
+public final class LongList {
+
+ private long[] values;
+ private int size;
+
+ /**
+ * Constructs an empty instance with a default initial capacity.
+ */
+ public LongList() {
+ this(4);
+ }
+
+ /**
+ * Constructs an empty instance.
+ *
+ * @param initialCapacity {@code >= 0;} initial capacity of the list
+ */
+ public LongList(int initialCapacity) {
+ if(initialCapacity < 0) {
+ throw new IllegalArgumentException("initial capacity must be 0 or higher");
+ }
+ values = new long[initialCapacity];
+ size = 0;
+ }
+
+ public static LongList of(long... values) {
+ if(values == null || values.length == 0) {
+ return new LongList(0);
+ }
+ LongList result = new LongList(values.length);
+ for(long value : values) {
+ result.add(value);
+ }
+ return result;
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 1;
+ for (int i = 0; i < size; i++) {
+ long value = values[i];
+ hashCode = 31 * hashCode + (int)(value ^ (value >>> 32));
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (! (other instanceof LongList)) {
+ return false;
+ }
+ LongList otherList = (LongList) other;
+ if (size != otherList.size) {
+ return false;
+ }
+ for (int i = 0; i < size; i++) {
+ if (values[i] != otherList.values[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(size * 5 + 10);
+ sb.append("LongList{");
+ for (int i = 0; i < size; i++) {
+ if (i != 0) {
+ sb.append(", ");
+ }
+ sb.append(values[i]);
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Gets the number of elements in this list.
+ */
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Gets the indicated value.
+ *
+ * @param n {@code >= 0, < size();} which element
+ * @return the indicated element's value
+ */
+ public long get(int n) {
+ if (n >= size) {
+ throw new IndexOutOfBoundsException("n >= size()");
+ } else if(n < 0) {
+ throw new IndexOutOfBoundsException("n < 0");
+ }
+ return values[n];
+ }
+
+ /**
+ * Sets the value at the given index.
+ *
+ * @param index the index at which to put the specified object.
+ * @param value the object to add.
+ * @return the previous element at the index.
+ */
+ public long set(int index, long value) {
+ if (index >= size) {
+ throw new IndexOutOfBoundsException("n >= size()");
+ } else if(index < 0) {
+ throw new IndexOutOfBoundsException("n < 0");
+ }
+ long result = values[index];
+ values[index] = value;
+ return result;
+ }
+
+ /**
+ * Adds an element to the end of the list. This will increase the
+ * list's capacity if necessary.
+ *
+ * @param value the value to add
+ */
+ public void add(long value) {
+ growIfNeeded();
+ values[size++] = value;
+ }
+
+ /**
+ * Inserts element into specified index, moving elements at and above
+ * that index up one. May not be used to insert at an index beyond the
+ * current size (that is, insertion as a last element is legal but
+ * no further).
+ *
+ * @param n {@code >= 0, <=size();} index of where to insert
+ * @param value value to insert
+ */
+ public void insert(int n, int value) {
+ if (n > size) {
+ throw new IndexOutOfBoundsException("n > size()");
+ } else if(n < 0) {
+ throw new IndexOutOfBoundsException("n < 0");
+ }
+
+ growIfNeeded();
+
+ System.arraycopy(values, n, values, n+1, size - n);
+ values[n] = value;
+ size++;
+ }
+
+ /**
+ * Removes value from this list.
+ *
+ * @param value value to remove
+ * return {@code true} if the value was removed, {@code false} otherwise
+ */
+ public boolean remove(long value) {
+ for (int i = 0; i < size; i++) {
+ if (values[i] == value) {
+ size--;
+ System.arraycopy(values, i+1, values, i, size-i);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes values from this list.
+ *
+ * @param values values to remove
+ */
+ public void removeAll(long[] values) {
+ for(long value : values) {
+ remove(value);
+ }
+ }
+
+ /**
+ * Removes values from this list.
+ *
+ * @param list List with values to remove
+ */
+ public void removeAll(LongList list) {
+ for(long value : list.values) {
+ remove(value);
+ }
+ }
+
+ /**
+ * Removes an element at a given index, shifting elements at greater
+ * indicies down one.
+ *
+ * @param index index of element to remove
+ */
+ public void removeIndex(int index) {
+ if (index >= size) {
+ throw new IndexOutOfBoundsException("n >= size()");
+ } else if(index < 0) {
+ throw new IndexOutOfBoundsException("n < 0");
+ }
+ size--;
+ System.arraycopy(values, index + 1, values, index, size - index);
+ }
+
+ /**
+ * Increases size of array if needed
+ */
+ private void growIfNeeded() {
+ if (size == values.length) {
+ // Resize.
+ long[] newArray = new long[size * 3 / 2 + 10];
+ System.arraycopy(values, 0, newArray, 0, size);
+ values = newArray;
+ }
+ }
+
+ /**
+ * Returns the index of the given value, or -1 if the value does not
+ * appear in the list.
+ *
+ * @param value value to find
+ * @return index of value or -1
+ */
+ public int indexOf(long value) {
+ for (int i = 0; i < size; i++) {
+ if (values[i] == value) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Removes all values from this list.
+ */
+ public void clear() {
+ values = new long[4];
+ size = 0;
+ }
+
+
+ /**
+ * Returns true if the given value is contained in the list
+ *
+ * @param value value to look for
+ * @return {@code true} if this list contains {@code value}, {@code false} otherwise
+ */
+ public boolean contains(long value) {
+ return indexOf(value) >= 0;
+ }
+
+ /**
+ * Returns an array with a copy of this list's values
+ *
+ * @return array with a copy of this list's values
+ */
+ public long[] toArray() {
+ return Arrays.copyOf(values, size);
+
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/NavDrawerData.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/NavDrawerData.java
new file mode 100644
index 000000000..d5856c10a
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/NavDrawerData.java
@@ -0,0 +1,105 @@
+package de.danoeh.antennapod.storage.database;
+
+import de.danoeh.antennapod.model.feed.Feed;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class NavDrawerData {
+ public final List<DrawerItem> items;
+ public final int queueSize;
+ public final int numNewItems;
+ public final int numDownloadedItems;
+ public final Map<Long, Integer> feedCounters;
+
+ public NavDrawerData(List<DrawerItem> feeds,
+ int queueSize,
+ int numNewItems,
+ int numDownloadedItems,
+ Map<Long, Integer> feedIndicatorValues) {
+ this.items = feeds;
+ this.queueSize = queueSize;
+ this.numNewItems = numNewItems;
+ this.numDownloadedItems = numDownloadedItems;
+ this.feedCounters = feedIndicatorValues;
+ }
+
+ public abstract static class DrawerItem {
+ public enum Type {
+ TAG, FEED
+ }
+
+ public final Type type;
+ private int layer;
+ public long id;
+
+ public DrawerItem(Type type, long id) {
+ this.type = type;
+ this.id = id;
+ }
+
+ public int getLayer() {
+ return layer;
+ }
+
+ public void setLayer(int layer) {
+ this.layer = layer;
+ }
+
+ public abstract String getTitle();
+
+ public abstract int getCounter();
+ }
+
+ public static class TagDrawerItem extends DrawerItem {
+ public final List<DrawerItem> children = new ArrayList<>();
+ private final String name;
+ private boolean isOpen;
+
+ public TagDrawerItem(String name) {
+ // Keep IDs >0 but make room for many feeds
+ super(DrawerItem.Type.TAG, Math.abs((long) name.hashCode()) << 20);
+ this.name = name;
+ }
+
+ public String getTitle() {
+ return name;
+ }
+
+ public boolean isOpen() {
+ return isOpen;
+ }
+
+ public void setOpen(final boolean open) {
+ isOpen = open;
+ }
+
+ public int getCounter() {
+ int sum = 0;
+ for (DrawerItem item : children) {
+ sum += item.getCounter();
+ }
+ return sum;
+ }
+ }
+
+ public static class FeedDrawerItem extends DrawerItem {
+ public final Feed feed;
+ public final int counter;
+
+ public FeedDrawerItem(Feed feed, long id, int counter) {
+ super(DrawerItem.Type.FEED, id);
+ this.feed = feed;
+ this.counter = counter;
+ }
+
+ public String getTitle() {
+ return feed.getTitle();
+ }
+
+ public int getCounter() {
+ return counter;
+ }
+ }
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/Permutor.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/Permutor.java
new file mode 100644
index 000000000..b3704f780
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/Permutor.java
@@ -0,0 +1,17 @@
+package de.danoeh.antennapod.storage.database;
+
+import java.util.List;
+
+/**
+ * Interface for passing around list permutor method. This is used for cases where a simple comparator
+ * won't work (e.g. Random, Smart Shuffle, etc).
+ *
+ * @param <E> the type of elements in the list
+ */
+public interface Permutor<E> {
+ /**
+ * Reorders the specified list.
+ * @param queue A (modifiable) list of elements to be reordered
+ */
+ void reorder(List<E> queue);
+}
diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/StatisticsItem.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/StatisticsItem.java
new file mode 100644
index 000000000..27c5e3bdd
--- /dev/null
+++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/StatisticsItem.java
@@ -0,0 +1,45 @@
+package de.danoeh.antennapod.storage.database;
+
+import de.danoeh.antennapod.model.feed.Feed;
+
+public class StatisticsItem {
+ public final Feed feed;
+ public final long time;
+
+ /**
+ * Respects speed, listening twice, ...
+ */
+ public final long timePlayed;
+
+ /**
+ * Number of episodes.
+ */
+ public final long episodes;
+
+ /**
+ * Episodes that are actually played.
+ */
+ public final long episodesStarted;
+
+ /**
+ * Simply sums up the size of download podcasts.
+ */
+ public final long totalDownloadSize;
+
+ /**
+ * Stores the number of episodes downloaded.
+ */
+ public final long episodesDownloadCount;
+
+ public StatisticsItem(Feed feed, long time, long timePlayed,
+ long episodes, long episodesStarted,
+ long totalDownloadSize, long episodesDownloadCount) {
+ this.feed = feed;
+ this.time = time;
+ this.timePlayed = timePlayed;
+ this.episodes = episodes;
+ this.episodesStarted = episodesStarted;
+ this.totalDownloadSize = totalDownloadSize;
+ this.episodesDownloadCount = episodesDownloadCount;
+ }
+}