From a85a6099db58edb2ca9c1a200c013250ae36005b Mon Sep 17 00:00:00 2001 From: dosiecki Date: Thu, 1 Dec 2016 05:56:48 -0800 Subject: [PATCH] basic support for notifications (#966) --- .../NewsBlur/res/drawable/logo_monochrome.png | Bin 0 -> 2758 bytes .../newsblur/database/BlurDatabaseHelper.java | 14 ++++- .../newsblur/database/DatabaseConstants.java | 26 ++++++-- .../src/com/newsblur/domain/Feed.java | 9 +++ .../src/com/newsblur/domain/Story.java | 8 +++ .../com/newsblur/service/NBSyncService.java | 6 ++ .../com/newsblur/service/UnreadsService.java | 30 +++++++++- .../src/com/newsblur/util/AppConstants.java | 2 +- .../com/newsblur/util/NotificationUtils.java | 56 ++++++++++++++++++ 9 files changed, 142 insertions(+), 9 deletions(-) create mode 100755 clients/android/NewsBlur/res/drawable/logo_monochrome.png create mode 100644 clients/android/NewsBlur/src/com/newsblur/util/NotificationUtils.java 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 0000000000000000000000000000000000000000..24562cc29a7851f713081bee51d6069eed69843f GIT binary patch literal 2758 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 M07*qoM6N<$g2Denwg3PC literal 0 HcmV?d00001 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); + + } + +}