diff --git a/clients/android/NewsBlur/res/drawable/logo_monochrome.png b/clients/android/NewsBlur/res/drawable/logo_monochrome.png new file mode 100755 index 000000000..24562cc29 Binary files /dev/null and b/clients/android/NewsBlur/res/drawable/logo_monochrome.png differ diff --git a/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java b/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java index bb7ebc105..624169721 100644 --- a/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java +++ b/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java @@ -977,6 +977,18 @@ public class BlurDatabaseHelper { return c; } + public Cursor getNotifyStoriesCursor() { + return rawQuery(DatabaseConstants.NOTIFY_STORY_QUERY_BASE, null, null); + } + + public void markNotifications() { + synchronized (RW_MUTEX) { + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_NOTIFIED, 1); + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_NOTIFY + " > 0", null); + } + } + public Loader getActiveStoriesLoader(final FeedSet fs) { final StoryOrder order = PrefsUtils.getStoryOrder(context, fs); return new QueryCursorLoader(context) { @@ -1003,7 +1015,7 @@ public class BlurDatabaseHelper { // stories aren't actually queried directly via the FeedSet and filters set in the UI. rather, // those filters are use to push live or cached story hashes into the reading session table, and // those hashes are used to pull story data from the story table - StringBuilder q = new StringBuilder(DatabaseConstants.STORY_QUERY_BASE); + StringBuilder q = new StringBuilder(DatabaseConstants.SESSION_STORY_QUERY_BASE); if (fs.isAllRead()) { q.append(" ORDER BY " + DatabaseConstants.READ_STORY_ORDER); diff --git a/clients/android/NewsBlur/src/com/newsblur/database/DatabaseConstants.java b/clients/android/NewsBlur/src/com/newsblur/database/DatabaseConstants.java index e4dfd75ea..0aee2dc31 100644 --- a/clients/android/NewsBlur/src/com/newsblur/database/DatabaseConstants.java +++ b/clients/android/NewsBlur/src/com/newsblur/database/DatabaseConstants.java @@ -97,6 +97,8 @@ public class DatabaseConstants { public static final String STORY_LAST_READ_DATE = "last_read_date"; public static final String STORY_SEARCH_HIT = "search_hit"; public static final String STORY_THUMBNAIL_URL = "thumbnail_url"; + public static final String STORY_NOTIFY = "notify"; + public static final String STORY_NOTIFIED = "notified"; public static final String READING_SESSION_TABLE = "reading_session"; public static final String READING_SESSION_STORY_HASH = "session_story_hash"; @@ -233,6 +235,8 @@ public class DatabaseConstants { STORY_PERMALINK + TEXT + ", " + STORY_READ + INTEGER + ", " + STORY_STARRED + INTEGER + ", " + + STORY_NOTIFY + INTEGER + ", " + + STORY_NOTIFIED + INTEGER + ", " + STORY_STARRED_DATE + INTEGER + ", " + STORY_TITLE + TEXT + ", " + STORY_IMAGE_URLS + TEXT + ", " + @@ -291,24 +295,36 @@ public class DatabaseConstants { STORY_INTELLIGENCE_AUTHORS, STORY_INTELLIGENCE_FEED, STORY_INTELLIGENCE_TAGS, STORY_INTELLIGENCE_TOTAL, STORY_INTELLIGENCE_TITLE, STORY_PERMALINK, STORY_READ, STORY_STARRED, STORY_STARRED_DATE, STORY_TAGS, STORY_USER_TAGS, STORY_TITLE, STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_HASH, - STORY_LAST_READ_DATE, STORY_THUMBNAIL_URL, + STORY_LAST_READ_DATE, STORY_THUMBNAIL_URL, STORY_NOTIFY, STORY_NOTIFIED, }; private static final String STORY_COLUMNS = TextUtils.join(",", BASE_STORY_COLUMNS) + ", " + FEED_TITLE + ", " + FEED_FAVICON_URL + ", " + FEED_FAVICON_COLOR + ", " + FEED_FAVICON_BORDER + ", " + FEED_FAVICON_FADE + ", " + FEED_FAVICON_TEXT; - public static final String STORY_QUERY_BASE = + public static final String STORY_QUERY_BASE_1 = "SELECT " + STORY_COLUMNS + " FROM " + STORY_TABLE + " INNER JOIN " + FEED_TABLE + " ON " + STORY_TABLE + "." + STORY_FEED_ID + " = " + FEED_TABLE + "." + FEED_ID + - " WHERE " + STORY_HASH + " IN (" + - " SELECT DISTINCT " + READING_SESSION_STORY_HASH + - " FROM " + READING_SESSION_TABLE + ")" + + " WHERE "; + public static final String STORY_QUERY_BASE_2 = " GROUP BY " + STORY_HASH; + public static final String SESSION_STORY_QUERY_BASE = + STORY_QUERY_BASE_1 + + STORY_HASH + " IN (" + + " SELECT DISTINCT " + READING_SESSION_STORY_HASH + + " FROM " + READING_SESSION_TABLE + + ")" + + STORY_QUERY_BASE_2; + + public static final String NOTIFY_STORY_QUERY_BASE = + STORY_QUERY_BASE_1 + + STORY_NOTIFY + " > 0 AND " + STORY_NOTIFIED + " < 1" + + STORY_QUERY_BASE_2; + public static final String JOIN_STORIES_ON_SOCIALFEED_MAP = " INNER JOIN " + STORY_TABLE + " ON " + STORY_TABLE + "." + STORY_ID + " = " + SOCIALFEED_STORY_MAP_TABLE + "." + SOCIALFEED_STORY_STORYID; diff --git a/clients/android/NewsBlur/src/com/newsblur/domain/Feed.java b/clients/android/NewsBlur/src/com/newsblur/domain/Feed.java index 4b8025140..f81c2cf3f 100644 --- a/clients/android/NewsBlur/src/com/newsblur/domain/Feed.java +++ b/clients/android/NewsBlur/src/com/newsblur/domain/Feed.java @@ -135,4 +135,13 @@ public class Feed implements Comparable, Serializable { return title.compareToIgnoreCase(f.title); } + public boolean isNotify() { + // the API vends more info on notifications than we need. distill it to a boolean + if (notificationTypes == null) return false; + for (String type : notificationTypes) { + if (type.equals("android")) return true; + } + return false; + } + } diff --git a/clients/android/NewsBlur/src/com/newsblur/domain/Story.java b/clients/android/NewsBlur/src/com/newsblur/domain/Story.java index 600920551..caa378d52 100644 --- a/clients/android/NewsBlur/src/com/newsblur/domain/Story.java +++ b/clients/android/NewsBlur/src/com/newsblur/domain/Story.java @@ -96,6 +96,10 @@ public class Story implements Serializable { // non-API, though it probably could/should be. populated on first story ingest if thumbnails are turned on public String thumbnailUrl; + + // non-API + public boolean notify; + public boolean notified; public ContentValues getValues() { final ContentValues values = new ContentValues(); @@ -126,6 +130,8 @@ public class Story implements Serializable { values.put(DatabaseConstants.STORY_LAST_READ_DATE, lastReadTimestamp); values.put(DatabaseConstants.STORY_SEARCH_HIT, searchHit); values.put(DatabaseConstants.STORY_THUMBNAIL_URL, thumbnailUrl); + values.put(DatabaseConstants.STORY_NOTIFY, notify); + values.put(DatabaseConstants.STORY_NOTIFIED, notified); return values; } @@ -157,6 +163,8 @@ public class Story implements Serializable { story.storyHash = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_HASH)); story.lastReadTimestamp = cursor.getLong(cursor.getColumnIndex(DatabaseConstants.STORY_LAST_READ_DATE)); story.thumbnailUrl = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_THUMBNAIL_URL)); + story.notify = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_NOTIFY)) > 0; + story.notified = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_NOTIFIED)) > 0; return story; } diff --git a/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java b/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java index 261d63839..25ad35bf5 100644 --- a/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java +++ b/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java @@ -124,6 +124,8 @@ public class NBSyncService extends Service { Set orphanFeedIds; Set disabledFeedIds; + Set notifyFeedIds; + private ExecutorService primaryExecutor; CleanupService cleanupService; OriginalTextService originalTextService; @@ -415,6 +417,7 @@ public class NBSyncService extends Service { Set debugFeedIdsFromFeeds = new HashSet(); orphanFeedIds = new HashSet(); disabledFeedIds = new HashSet(); + notifyFeedIds = new HashSet(); try { FeedFolderResponse feedResponse = apiManager.getFolderFeedMapping(true); @@ -470,6 +473,9 @@ public class NBSyncService extends Service { disabledFeedIds.add(feed.feedId); } feedValues.add(feed.getValues()); + if (feed.isNotify()) { + notifyFeedIds.add(feed.feedId); + } } // also add the implied zero-id feed feedValues.add(Feed.getZeroFeed().getValues()); diff --git a/clients/android/NewsBlur/src/com/newsblur/service/UnreadsService.java b/clients/android/NewsBlur/src/com/newsblur/service/UnreadsService.java index 8c0e7c4de..a91e10e70 100644 --- a/clients/android/NewsBlur/src/com/newsblur/service/UnreadsService.java +++ b/clients/android/NewsBlur/src/com/newsblur/service/UnreadsService.java @@ -1,12 +1,15 @@ package com.newsblur.service; +import android.database.Cursor; import android.util.Log; +import static com.newsblur.database.BlurDatabaseHelper.closeQuietly; import com.newsblur.domain.Story; import com.newsblur.network.domain.StoriesResponse; import com.newsblur.network.domain.UnreadStoryHashesResponse; import com.newsblur.util.AppConstants; import com.newsblur.util.DefaultFeedView; +import com.newsblur.util.NotificationUtils; import com.newsblur.util.PrefsUtils; import com.newsblur.util.StoryOrder; @@ -39,9 +42,13 @@ public class UnreadsService extends SubService { doMetadata = false; } - if (StoryHashQueue.size() < 1) return; + if (StoryHashQueue.size() > 0) { + getNewUnreadStories(); + } - getNewUnreadStories(); + if (StoryHashQueue.size() < 1) { + notifyStories(); + } } private void syncUnreadList() { @@ -133,6 +140,16 @@ public class UnreadsService extends SubService { Log.e(this.getClass().getName(), "error fetching unreads batch, abandoning sync."); break unreadsyncloop; } + + for (Story story : response.stories) { + if (parent.notifyFeedIds.contains(story.feedId)) { + // for now, only notify stories with 1+ intel + if (story.intelligence.calcTotalIntel() > 0) { + story.notify = true; + } + } + } + parent.insertStories(response); for (String hash : hashBatch) { StoryHashQueue.remove(hash); @@ -169,6 +186,15 @@ public class UnreadsService extends SubService { return true; } + private void notifyStories() { + Cursor c = parent.dbHelper.getNotifyStoriesCursor(); + if (c.getCount() > 0 ) { + NotificationUtils.notifyStories(c, parent); + parent.dbHelper.markNotifications(); + } + closeQuietly(c); + } + public static void clear() { StoryHashQueue.clear(); } diff --git a/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java b/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java index 0c0c11c35..46bf1ed6e 100644 --- a/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java +++ b/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java @@ -29,7 +29,7 @@ public class AppConstants { public static final String LAST_SYNC_TIME = "LAST_SYNC_TIME"; // how long to wait before auto-syncing the feed/folder list - public static final long AUTO_SYNC_TIME_MILLIS = 20L * 60L * 1000L; + public static final long AUTO_SYNC_TIME_MILLIS = 15L * 60L * 1000L; // how often to rebuild the DB public static final long VACUUM_TIME_MILLIS = 12L * 60L * 60L * 1000L; diff --git a/clients/android/NewsBlur/src/com/newsblur/util/NotificationUtils.java b/clients/android/NewsBlur/src/com/newsblur/util/NotificationUtils.java new file mode 100644 index 000000000..07d1d0ed3 --- /dev/null +++ b/clients/android/NewsBlur/src/com/newsblur/util/NotificationUtils.java @@ -0,0 +1,56 @@ +package com.newsblur.util; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import android.app.Activity; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; + +import com.newsblur.R; +import com.newsblur.activity.Main; +import com.newsblur.database.DatabaseConstants; +import com.newsblur.domain.Feed; +import com.newsblur.domain.Story; + +public class NotificationUtils { + + private NotificationUtils() {} // util class - no instances + + public static void notifyStories(Cursor stories, Context context) { + String title = stories.getCount() + " New Stories"; + if (stories.getCount() == 1) title = "1 New Story"; + + StringBuilder content = new StringBuilder(); + while (stories.moveToNext()) { + String feedTitle = stories.getString(stories.getColumnIndex(DatabaseConstants.FEED_TITLE)); + content.append(feedTitle); + if (! stories.isLast()) { + content.append(", "); + } + } + + Intent appIntent = new Intent(context, Main.class); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, appIntent, 0); + + Notification n = new Notification.Builder(context) + .setContentTitle(title) + .setContentText(content.toString()) + .setSmallIcon(R.drawable.logo_monochrome) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setNumber(stories.getCount()) + .build(); + + NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(1, n); + + } + +}