From 6f5d23c55743bd98800148c943880ce00d04441a Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Fri, 8 May 2015 16:02:02 +0200 Subject: Order feeds by number of unread items (descending) --- .../antennapod/core/util/LongLongMapTest.java | 59 +++++ .../danoeh/antennapod/core/storage/DBReader.java | 62 ++++- .../antennapod/core/storage/PodDBAdapter.java | 45 +++- .../de/danoeh/antennapod/core/util/LongIntMap.java | 252 +++++++++++++++++++++ .../de/danoeh/antennapod/core/util/LongList.java | 1 - 5 files changed, 404 insertions(+), 15 deletions(-) create mode 100644 core/src/androidTest/java/de/danoeh/antennapod/core/util/LongLongMapTest.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java (limited to 'core/src') diff --git a/core/src/androidTest/java/de/danoeh/antennapod/core/util/LongLongMapTest.java b/core/src/androidTest/java/de/danoeh/antennapod/core/util/LongLongMapTest.java new file mode 100644 index 000000000..d75cd5c77 --- /dev/null +++ b/core/src/androidTest/java/de/danoeh/antennapod/core/util/LongLongMapTest.java @@ -0,0 +1,59 @@ +package de.danoeh.antennapod.core.util; + +import android.test.AndroidTestCase; + +public class LongLongMapTest extends AndroidTestCase { + + public void testEmptyMap() { + LongIntMap map = new LongIntMap(); + assertEquals(0, map.size()); + assertEquals("LongLongMap{}", map.toString()); + assertEquals(0, map.get(42)); + assertEquals(-1, map.get(42, -1)); + assertEquals(false, map.delete(42)); + assertEquals(-1, map.indexOfKey(42)); + assertEquals(-1, map.indexOfValue(42)); + assertEquals(1, map.hashCode()); + } + + public void testSingleElement() { + LongIntMap map = new LongIntMap(); + map.put(17, 42); + assertEquals(1, map.size()); + assertEquals("LongLongMap{17=42}", map.toString()); + assertEquals(42, map.get(17)); + assertEquals(42, map.get(17, -1)); + assertEquals(0, map.indexOfKey(17)); + assertEquals(0, map.indexOfValue(42)); + assertEquals(true, map.delete(17)); + } + + public void testAddAndDelete() { + LongIntMap map = new LongIntMap(); + for(int i=0; i < 100; i++) { + map.put(i * 17, i * 42); + } + assertEquals(100, map.size()); + assertEquals(0, map.get(0)); + assertEquals(42, map.get(17)); + assertEquals(42, map.get(17, -1)); + assertEquals(1, map.indexOfKey(17)); + assertEquals(1, map.indexOfValue(42)); + for(int i=0; i < 100; i++) { + assertEquals(true, map.delete(i * 17)); + } + } + + public void testOverwrite() { + LongIntMap map = new LongIntMap(); + map.put(17, 42); + assertEquals(1, map.size()); + assertEquals("LongLongMap{17=42}", map.toString()); + assertEquals(42, map.get(17)); + map.put(17, 23); + assertEquals(1, map.size()); + assertEquals("LongLongMap{17=23}", map.toString()); + assertEquals(23, map.get(17)); + } + +} 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 4d93ab447..0958aa7b9 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 @@ -8,6 +8,7 @@ import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.List; @@ -23,6 +24,7 @@ import de.danoeh.antennapod.core.feed.SimpleChapter; import de.danoeh.antennapod.core.feed.VorbisCommentChapter; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.LongIntMap; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; @@ -489,6 +491,29 @@ public final class DBReader { return items; } + /** + * Loads a list of FeedItems that are considered new. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItems that are considered new. + */ + public static List getNewItemsList(Context context) { + Log.d(TAG, "getNewItemsList()"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getNewItemsCursor(); + List items = extractItemlistFromCursor(adapter, itemlistCursor); + itemlistCursor.close(); + + loadFeedDataOfFeedItemlist(context, items); + + adapter.close(); + + return items; + } + /** * Loads the IDs of the FeedItems whose 'read'-attribute is set to false. * @@ -966,15 +991,15 @@ public final class DBReader { } /** - * Returns the number of unread items. + * Returns a map containing the number of unread items per feed * * @param context A context that is used for opening a database connection. - * @return The number of unread items. + * @return The number of unread items per feed. */ - public static int getNumberOfUnreadItems(final Context context, long feedId) { + public static LongIntMap getNumberOfUnreadFeedItems(final Context context, long... feedIds) { PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); - final int result = adapter.getNumberOfUnreadItems(feedId); + final LongIntMap result = adapter.getNumberOfUnreadFeedItems(feedIds); adapter.close(); return result; } @@ -1103,9 +1128,31 @@ public final class DBReader { PodDBAdapter adapter = new PodDBAdapter(context); adapter.open(); List feeds = getFeedList(adapter); + long[] feedIds = new long[feeds.size()]; + for(int i=0; i < feeds.size(); i++) { + feedIds[i] = feeds.get(i).getId(); + } + final LongIntMap numUnreadFeedItems = adapter.getNumberOfUnreadFeedItems(feedIds); + Collections.sort(feeds, new Comparator() { + @Override + public int compare(Feed lhs, Feed rhs) { + long numUnreadLhs = numUnreadFeedItems.get(lhs.getId()); + Log.d(TAG, "feed with id " + lhs.getId() + " has " + numUnreadLhs + " unread items"); + long numUnreadRhs = numUnreadFeedItems.get(rhs.getId()); + Log.d(TAG, "feed with id " + rhs.getId() + " has " + numUnreadRhs + " unread items"); + if(numUnreadLhs > numUnreadRhs) { + // reverse natural order: podcast with most unplayed episodes first + return -1; + } else if(numUnreadLhs == numUnreadRhs) { + return 0; + } else { + return 1; + } + } + }); int queueSize = adapter.getQueueSize(); int numUnreadItems = adapter.getNumberOfUnreadItems(); - NavDrawerData result = new NavDrawerData(feeds, queueSize, numUnreadItems); + NavDrawerData result = new NavDrawerData(feeds, queueSize, numUnreadItems, numUnreadFeedItems); adapter.close(); return result; } @@ -1114,11 +1161,14 @@ public final class DBReader { public List feeds; public int queueSize; public int numUnreadItems; + public LongIntMap numUnreadFeedItems; - public NavDrawerData(List feeds, int queueSize, int numUnreadItems) { + public NavDrawerData(List feeds, int queueSize, int numUnreadItems, + LongIntMap numUnreadFeedItems) { this.feeds = feeds; this.queueSize = queueSize; this.numUnreadItems = numUnreadItems; + this.numUnreadFeedItems = numUnreadFeedItems; } } } 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 623ca377a..23b5391c7 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 @@ -27,6 +27,7 @@ import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.util.LongIntMap; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; ; @@ -181,8 +182,8 @@ public class PodDBAdapter { + KEY_PASSWORD + " TEXT," + KEY_IS_PAGED + " INTEGER DEFAULT 0," + KEY_NEXT_PAGE_LINK + " TEXT," - + KEY_HIDE + " TEXT)"; - + + KEY_HIDE + " TEXT," + + KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0)"; public static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE @@ -207,7 +208,8 @@ public class PodDBAdapter { + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT," + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER," + KEY_FEEDITEM + " INTEGER," - + KEY_PLAYED_DURATION + " INTEGER)"; + + KEY_PLAYED_DURATION + " INTEGER," + + KEY_AUTO_DOWNLOAD + " INTEGER)"; public static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE " + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE @@ -1065,7 +1067,27 @@ public class PodDBAdapter { Cursor c = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID}, KEY_READ + "=0", null, null, null, KEY_PUBDATE + " DESC"); return c; + } + /** + * Returns a cursor which contains all feed items that are considered new. + * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection. + */ + public final Cursor getNewItemsCursor() { + final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + + " LEFT OUTER JOIN " + TABLE_NAME_QUEUE + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM + + " WHERE " + + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed + + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded + + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played + + TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL"; // not in queue + Cursor c = db.rawQuery(query, null); + return c; } public final Cursor getRecentlyPublishedItemsCursor(int limit) { @@ -1223,13 +1245,20 @@ public class PodDBAdapter { return result; } - public final int getNumberOfUnreadItems(long feedId) { - final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_ITEMS + - " WHERE " + KEY_FEED + " = " + feedId + " AND " + KEY_READ + " = 0"; + public final LongIntMap getNumberOfUnreadFeedItems(long... feedIds) { + final String query = "SELECT " + KEY_FEED + ", COUNT(" + KEY_ID + ") AS count " + + " FROM " + TABLE_NAME_FEED_ITEMS + + " WHERE " + KEY_FEED + " IN (" + StringUtils.join(feedIds, ',') + ") " + + " AND " + KEY_READ + " = 0" + + " GROUP BY " + KEY_FEED; Cursor c = db.rawQuery(query, null); - int result = 0; + LongIntMap result = new LongIntMap(c.getCount()); if (c.moveToFirst()) { - result = c.getInt(0); + do { + long feedId = c.getLong(0); + int count = c.getInt(1); + result.put(feedId, count); + } while(c.moveToNext()); } c.close(); return result; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java b/core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java new file mode 100644 index 000000000..33fd252eb --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LongIntMap.java @@ -0,0 +1,252 @@ +package de.danoeh.antennapod.core.util; + + +/** + * Fast and memory efficient long to long map + */ +public class LongIntMap { + + private long[] keys; + private int[] values; + private int size; + + /** + * Creates a new LongLongMap containing no mappings. + */ + public LongIntMap() { + this(10); + } + + /** + * Creates a new SparseLongArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public LongIntMap(int initialCapacity) { + if(initialCapacity < 0) { + throw new IllegalArgumentException("initial capacity must be 0 or higher"); + } + keys = new long[initialCapacity]; + values = new int[initialCapacity]; + size = 0; + } + + /** + * Increases size of array if needed + */ + private void growIfNeeded() { + if (size == keys.length) { + // Resize. + long[] newKeysArray = new long[size * 3 / 2 + 10]; + int[] newValuesArray = new int[size * 3 / 2 + 10]; + System.arraycopy(keys, 0, newKeysArray, 0, size); + System.arraycopy(values, 0, newValuesArray, 0, size); + keys = newKeysArray; + values = newValuesArray; + } + } + + /** + * Gets the long mapped from the specified key, or 0 + * if no such mapping has been made. + */ + public int get(long key) { + return get(key, 0); + } + + /** + * Gets the long mapped from the specified key, or the specified value + * if no such mapping has been made. + */ + public int get(long key, int valueIfKeyNotFound) { + int index = indexOfKey(key); + if(index >= 0) { + return values[index]; + } else { + return valueIfKeyNotFound; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public boolean delete(long key) { + int index = indexOfKey(key); + + if (index >= 0) { + removeAt(index); + return true; + } else { + return false; + } + } + + /** + * Removes the mapping at the given index. + */ + public void removeAt(int index) { + System.arraycopy(keys, index + 1, keys, index, size - (index + 1)); + System.arraycopy(values, index + 1, values, index, size - (index + 1)); + size--; + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(long key, int value) { + int index = indexOfKey(key); + + if (index >= 0) { + values[index] = value; + } else { + growIfNeeded(); + keys[size] = key; + values[size] = value; + size++; + } + } + + /** + * Returns the number of key-value mappings that this SparseIntArray + * currently stores. + */ + public int size() { + return size; + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * SparseLongArray stores. + * + *

The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., keyAt(0) will return the + * smallest key and keyAt(size()-1) will return the largest + * key.

+ */ + public long keyAt(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException("n >= size()"); + } else if(index < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + return keys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * SparseLongArray stores. + * + *

The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * valueAt(0) will return the value associated with the + * smallest key and valueAt(size()-1) will return the value + * associated with the largest key.

+ */ + public int valueAt(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException("n >= size()"); + } else if(index < 0) { + throw new IndexOutOfBoundsException("n < 0"); + } + return values[index]; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(long key) { + for(int i=0; i < size; i++) { + if(keys[i] == key) { + return i; + } + } + return -1; + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(long value) { + for (int i = 0; i < size; i++) { + if (values[i] == value) { + return i; + } + } + return -1; + } + + /** + * Removes all key-value mappings from this SparseIntArray. + */ + public void clear() { + keys = new long[10]; + values = new int[10]; + size = 0; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (! (other instanceof LongIntMap)) { + return false; + } + LongIntMap otherMap = (LongIntMap) other; + if (size != otherMap.size) { + return false; + } + for (int i = 0; i < size; i++) { + if (keys[i] != otherMap.keys[i] || + values[i] != otherMap.values[i]) { + return false; + } + } + return true; + } + + @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 String toString() { + if (size() <= 0) { + return "LongLongMap{}"; + } + + StringBuilder buffer = new StringBuilder(size * 28); + buffer.append("LongLongMap{"); + for (int i=0; i < size; i++) { + if (i > 0) { + buffer.append(", "); + } + long key = keyAt(i); + buffer.append(key); + buffer.append('='); + long value = valueAt(i); + buffer.append(value); + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java b/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java index f5d0cab0c..8934f3272 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java @@ -32,7 +32,6 @@ public final class LongList { @Override public int hashCode() { - Arrays.hashCode(values); int hashCode = 1; for (int i = 0; i < size; i++) { long value = values[i]; -- cgit v1.2.3