Order feeds by number of unread items (descending)

This commit is contained in:
Martin Fietz 2015-05-08 16:02:02 +02:00 committed by Martin Fietz
parent 406dab0a24
commit 6f5d23c557
9 changed files with 435 additions and 32 deletions

View File

@ -706,5 +706,10 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
public int getNumberOfUnreadItems() {
return (navDrawerData != null) ? navDrawerData.numUnreadItems : 0;
}
@Override
public int getNumberOfUnreadFeedItems(long feedId) {
return (navDrawerData != null) ? navDrawerData.numUnreadFeedItems.get(feedId) : 0;
}
};
}

View File

@ -511,6 +511,11 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity
return (navDrawerData != null) ? navDrawerData.numUnreadItems : 0;
}
@Override
public int getNumberOfUnreadFeedItems(long feedId) {
return (navDrawerData != null) ? navDrawerData.numUnreadFeedItems.get(feedId) : 0;
}
};
private void loadData() {

View File

@ -27,7 +27,6 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.fragment.AddFeedFragment;
import de.danoeh.antennapod.fragment.AllEpisodesFragment;
import de.danoeh.antennapod.fragment.DownloadsFragment;
@ -266,12 +265,13 @@ public class NavListAdapter extends BaseAdapter
holder.title.setText(feed.getTitle());
int feedUnreadItems = DBReader.getNumberOfUnreadItems(context, feed.getId());
if(feed.hasLastUpdateFailed()) {
holder.failure.setVisibility(View.VISIBLE);
} else {
holder.failure.setVisibility(View.GONE);
}
int feedUnreadItems = itemAccess.getNumberOfUnreadFeedItems(feed.getId());
if(feedUnreadItems > 0) {
holder.count.setVisibility(View.VISIBLE);
holder.count.setText(String.valueOf(feedUnreadItems));
@ -301,6 +301,7 @@ public class NavListAdapter extends BaseAdapter
int getSelectedItemIndex();
int getQueueSize();
int getNumberOfUnreadItems();
int getNumberOfUnreadFeedItems(long feedId);
}
}

View File

@ -43,9 +43,7 @@ import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
/**
* Shows unread or recently published episodes
@ -70,14 +68,13 @@ public class AllEpisodesFragment extends Fragment {
private TextView txtvEmpty;
private ProgressBar progLoading;
private List<FeedItem> unreadItems;
private List<FeedItem> recentItems;
private List<FeedItem> episodes;
private LongList queueAccess;
private List<Downloader> downloaderList;
private boolean itemsLoaded = false;
private boolean viewsCreated = false;
private boolean showOnlyNewEpisodes = false;
private final boolean showOnlyNewEpisodes;
private AtomicReference<MainActivity> activity = new AtomicReference<MainActivity>();
@ -225,7 +222,7 @@ public class AllEpisodesFragment extends Fragment {
if (itemsLoaded) {
MenuItem menuItem = menu.findItem(R.id.mark_all_read_item);
if (menuItem != null) {
menuItem.setVisible(unreadItems != null && !unreadItems.isEmpty());
menuItem.setVisible(episodes != null && !episodes.isEmpty());
}
}
}
@ -345,7 +342,7 @@ public class AllEpisodesFragment extends Fragment {
@Override
public int getCount() {
if (itemsLoaded) {
return (showOnlyNewEpisodes) ? unreadItems.size() : recentItems.size();
return episodes.size();
}
return 0;
}
@ -353,7 +350,7 @@ public class AllEpisodesFragment extends Fragment {
@Override
public FeedItem getItem(int position) {
if (itemsLoaded) {
return (showOnlyNewEpisodes) ? unreadItems.get(position) : recentItems.get(position);
return episodes.get(position);
}
return null;
}
@ -436,10 +433,17 @@ public class AllEpisodesFragment extends Fragment {
protected Object[] doInBackground(Void... params) {
Context context = activity.get();
if (context != null) {
return new Object[]{
DBReader.getUnreadItemsList(context),
DBReader.getRecentlyPublishedEpisodes(context, RECENT_EPISODES_LIMIT),
DBReader.getQueueIDList(context)};
if(showOnlyNewEpisodes) {
return new Object[] {
DBReader.getNewItemsList(context),
DBReader.getQueueIDList(context)
};
} else {
return new Object[]{
DBReader.getRecentlyPublishedEpisodes(context, RECENT_EPISODES_LIMIT),
DBReader.getQueueIDList(context)
};
}
} else {
return null;
}
@ -452,9 +456,8 @@ public class AllEpisodesFragment extends Fragment {
progLoading.setVisibility(View.GONE);
if (lists != null) {
unreadItems = (List<FeedItem>) lists[0];
recentItems = (List<FeedItem>) lists[1];
queueAccess = (LongList) lists[2];
episodes = (List<FeedItem>) lists[0];
queueAccess = (LongList) lists[1];
itemsLoaded = true;
if (viewsCreated && activity.get() != null) {
onFragmentLoaded();

View File

@ -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));
}
}

View File

@ -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<FeedItem> getNewItemsList(Context context) {
Log.d(TAG, "getNewItemsList()");
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
Cursor itemlistCursor = adapter.getNewItemsCursor();
List<FeedItem> 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<Feed> 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<Feed>() {
@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<Feed> feeds;
public int queueSize;
public int numUnreadItems;
public LongIntMap numUnreadFeedItems;
public NavDrawerData(List<Feed> feeds, int queueSize, int numUnreadItems) {
public NavDrawerData(List<Feed> feeds, int queueSize, int numUnreadItems,
LongIntMap numUnreadFeedItems) {
this.feeds = feeds;
this.queueSize = queueSize;
this.numUnreadItems = numUnreadItems;
this.numUnreadFeedItems = numUnreadFeedItems;
}
}
}

View File

@ -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;

View File

@ -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 <code>0</code>
* 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 <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* SparseLongArray stores.
*
* <p>The keys corresponding to indices in ascending order are guaranteed to
* be in ascending order, e.g., <code>keyAt(0)</code> will return the
* smallest key and <code>keyAt(size()-1)</code> will return the largest
* key.</p>
*/
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 <code>0...size()-1</code>, returns
* the value from the <code>index</code>th key-value mapping that this
* SparseLongArray stores.
*
* <p>The values corresponding to indices in ascending order are guaranteed
* to be associated with keys in ascending order, e.g.,
* <code>valueAt(0)</code> will return the value associated with the
* smallest key and <code>valueAt(size()-1)</code> will return the value
* associated with the largest key.</p>
*/
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();
}
}

View File

@ -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];