notifications alpha 2

This commit is contained in:
dosiecki 2017-02-08 00:41:08 -08:00
parent 7b8795af15
commit 21228501ee
11 changed files with 157 additions and 49 deletions

View file

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.newsblur"
android:versionCode="133"
android:versionName="5.0.0" >
android:versionName="5.1.0b1" >
<uses-sdk
android:minSdkVersion="16"

View file

@ -977,16 +977,12 @@ public class BlurDatabaseHelper {
return c;
}
public Cursor getNotifyStoriesCursor() {
return rawQuery(DatabaseConstants.NOTIFY_STORY_QUERY_BASE, null, null);
public Cursor getNotifyFocusStoriesCursor() {
return rawQuery(DatabaseConstants.NOTIFY_FOCUS_STORY_QUERY, 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 Cursor getNotifyUnreadStoriesCursor() {
return rawQuery(DatabaseConstants.NOTIFY_UNREAD_STORY_QUERY, null, null);
}
public Loader<Cursor> getActiveStoriesLoader(final FeedSet fs) {

View file

@ -8,6 +8,7 @@ import android.provider.BaseColumns;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.newsblur.domain.Feed;
import com.newsblur.util.ReadFilter;
import com.newsblur.util.StateFilter;
import com.newsblur.util.StoryOrder;
@ -42,6 +43,7 @@ public class DatabaseConstants {
public static final String FEED_NEUTRAL_COUNT = "nt";
public static final String FEED_NEGATIVE_COUNT = "ng";
public static final String FEED_NOTIFICATION_TYPES = "notification_types";
public static final String FEED_NOTIFICATION_FILTER = "notification_filter";
public static final String SOCIALFEED_TABLE = "social_feeds";
public static final String SOCIAL_FEED_ID = BaseColumns._ID;
@ -97,8 +99,6 @@ 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";
@ -170,7 +170,8 @@ public class DatabaseConstants {
FEED_SUBSCRIBERS + TEXT + ", " +
FEED_TITLE + TEXT + ", " +
FEED_UPDATED_SECONDS + INTEGER + ", " +
FEED_NOTIFICATION_TYPES + TEXT +
FEED_NOTIFICATION_TYPES + TEXT + ", " +
FEED_NOTIFICATION_FILTER + TEXT +
")";
static final String USER_SQL = "CREATE TABLE " + USER_TABLE + " (" +
@ -235,8 +236,6 @@ 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 + ", " +
@ -295,7 +294,7 @@ 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_NOTIFY, STORY_NOTIFIED,
STORY_LAST_READ_DATE, STORY_THUMBNAIL_URL,
};
private static final String STORY_COLUMNS =
@ -320,10 +319,19 @@ public class DatabaseConstants {
")" +
STORY_QUERY_BASE_2;
public static final String NOTIFY_STORY_QUERY_BASE =
public static String NOTIFY_FOCUS_STORY_QUERY =
STORY_QUERY_BASE_1 +
STORY_NOTIFY + " > 0 AND " + STORY_NOTIFIED + " < 1" +
STORY_QUERY_BASE_2;
STORY_FEED_ID + " IN (SELECT " + FEED_ID + " FROM " + FEED_TABLE + " WHERE " + FEED_NOTIFICATION_FILTER + " = '" + Feed.NOTIFY_FILTER_FOCUS + "')" +
" AND " + STORY_INTELLIGENCE_TOTAL + " > 0 " +
STORY_QUERY_BASE_2 +
" ORDER BY " + STORY_TIMESTAMP + " DESC";
public static String NOTIFY_UNREAD_STORY_QUERY =
STORY_QUERY_BASE_1 +
STORY_FEED_ID + " IN (SELECT " + FEED_ID + " FROM " + FEED_TABLE + " WHERE " + FEED_NOTIFICATION_FILTER + " = '" + Feed.NOTIFY_FILTER_UNREAD + "')" +
" AND " + STORY_INTELLIGENCE_TOTAL + " >= 0 " +
STORY_QUERY_BASE_2 +
" ORDER BY " + STORY_TIMESTAMP + " DESC";
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;

View file

@ -59,9 +59,14 @@ public class Feed implements Comparable<Feed>, Serializable {
@SerializedName("updated_seconds_ago")
public int lastUpdated;
// NB: deserialized but not stored
@SerializedName("notification_types")
public List<String> notificationTypes;
// NB: only stored if notificationTypes was set to include android
@SerializedName("notification_filter")
public String notificationFilter;
public ContentValues getValues() {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.FEED_ID, feedId);
@ -79,7 +84,9 @@ public class Feed implements Comparable<Feed>, Serializable {
values.put(DatabaseConstants.FEED_SUBSCRIBERS, subscribers);
values.put(DatabaseConstants.FEED_TITLE, title);
values.put(DatabaseConstants.FEED_UPDATED_SECONDS, lastUpdated);
values.put(DatabaseConstants.FEED_NOTIFICATION_TYPES, DatabaseConstants.flattenStringList(notificationTypes));
if (isNotifyAndroid()) {
values.put(DatabaseConstants.FEED_NOTIFICATION_FILTER, notificationFilter);
}
return values;
}
@ -103,7 +110,7 @@ public class Feed implements Comparable<Feed>, Serializable {
feed.subscribers = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_SUBSCRIBERS));
feed.title = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_TITLE));
feed.lastUpdated = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_UPDATED_SECONDS));
feed.notificationTypes = DatabaseConstants.unflattenStringList(cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_NOTIFICATION_TYPES)));
feed.notificationFilter = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_NOTIFICATION_FILTER));
return feed;
}
@ -135,13 +142,16 @@ public class Feed implements Comparable<Feed>, Serializable {
return title.compareToIgnoreCase(f.title);
}
public boolean isNotify() {
// the API vends more info on notifications than we need. distill it to a boolean
private boolean isNotifyAndroid() {
if (notificationTypes == null) return false;
for (String type : notificationTypes) {
if (type.equals("android")) return true;
if (type.equals(NOTIFY_TYPE_ANDROID)) return true;
}
return false;
}
private static final String NOTIFY_TYPE_ANDROID = "android";
public static final String NOTIFY_FILTER_UNREAD = "unread";
public static final String NOTIFY_FILTER_FOCUS = "focus";
}

View file

@ -97,10 +97,6 @@ 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();
values.put(DatabaseConstants.STORY_ID, id);
@ -130,8 +126,6 @@ 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;
}
@ -163,8 +157,6 @@ 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;
}

View file

@ -31,6 +31,7 @@ import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FileCache;
import com.newsblur.util.NetworkUtils;
import com.newsblur.util.NotificationUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadingAction;
import com.newsblur.util.ReadFilter;
@ -134,6 +135,7 @@ public class NBSyncService extends Service {
PowerManager.WakeLock wl = null;
APIManager apiManager;
BlurDatabaseHelper dbHelper;
FileCache iconCache;
private int lastStartIdCompleted = -1;
/** The time of the last hard API failure we encountered. Used to implement back-off so that the sync
@ -162,6 +164,7 @@ public class NBSyncService extends Service {
if (apiManager == null) {
apiManager = new APIManager(this);
dbHelper = new BlurDatabaseHelper(this);
iconCache = FileCache.asIconCache(this);
cleanupService = new CleanupService(this);
originalTextService = new OriginalTextService(this);
unreadsService = new UnreadsService(this);
@ -253,6 +256,8 @@ public class NBSyncService extends Service {
checkRecounts();
pushNotifications();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "finishing primary sync");
} catch (Exception e) {
@ -774,6 +779,19 @@ public class NBSyncService extends Service {
dbHelper.insertStories(apiResponse, false);
}
void pushNotifications() {
// don't notify stories until the queue is flushed so they don't churn
if (unreadsService.StoryHashQueue.size() > 0) return;
// don't slow down active story loading
if (PendingFeed != null) return;
Cursor cFocus = dbHelper.getNotifyFocusStoriesCursor();
Cursor cUnread = dbHelper.getNotifyUnreadStoriesCursor();
NotificationUtils.notifyStories(cFocus, cUnread, this, iconCache);
closeQuietly(cFocus);
closeQuietly(cUnread);
}
void incrementRunningChild() {
synchronized (WAKELOCK_MUTEX) {
wl.acquire();

View file

@ -33,7 +33,6 @@ public abstract class SubService {
}
public void start(final int startId) {
if (Boolean.TRUE != parent.isAuth) return;
if (parent.stopSync()) return;
parent.incrementRunningChild();
this.startId = startId;

View file

@ -4,12 +4,12 @@ import android.database.Cursor;
import android.util.Log;
import static com.newsblur.database.BlurDatabaseHelper.closeQuietly;
import com.newsblur.domain.Feed;
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;
@ -17,6 +17,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@ -27,7 +28,7 @@ public class UnreadsService extends SubService {
private static volatile boolean doMetadata = false;
/** Unread story hashes the API listed that we do not appear to have locally yet. */
private static List<String> StoryHashQueue;
static List<String> StoryHashQueue;
static { StoryHashQueue = new ArrayList<String>(); }
public UnreadsService(NBSyncService parent) {
@ -44,11 +45,9 @@ public class UnreadsService extends SubService {
if (StoryHashQueue.size() > 0) {
getNewUnreadStories();
parent.pushNotifications();
}
if (StoryHashQueue.size() < 1) {
notifyStories();
}
}
private void syncUnreadList() {
@ -177,15 +176,6 @@ 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();
}

View file

@ -149,7 +149,7 @@ public class ImageLoader {
}
}
private Bitmap decodeBitmap(File f) {
private static Bitmap decodeBitmap(File f) {
// is is perfectly normal for files not to exist on cache misses or low
// device memory. this class will handle nulls with a queued action or
// placeholder image.
@ -161,4 +161,17 @@ public class ImageLoader {
}
}
/**
* Directly access a previously cached image's bitmap. This method is *not* for use
* in foreground UI methods; it was designed for low-priority background use for
* creating notifications.
*/
public static Bitmap getCachedImageSynchro(FileCache fileCache, String url) {
if (url.startsWith("/")) {
url = APIConstants.buildUrl(url);
}
File f = fileCache.getCachedFile(url);
return decodeBitmap(f);
}
}

View file

@ -12,9 +12,13 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.os.Build;
import com.newsblur.R;
import com.newsblur.activity.FeedReading;
import com.newsblur.activity.Main;
import com.newsblur.activity.Reading;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.domain.Feed;
import com.newsblur.domain.Story;
@ -22,10 +26,86 @@ import com.newsblur.util.FileCache;
public class NotificationUtils {
private static final int NOTIFY_COLOUR = 0xFFDA8A35;
private static final int MAX_CONCUR_NOTIFY = 5;
private NotificationUtils() {} // util class - no instances
public static void notifyStories(Cursor stories, Context context) {
;
public static synchronized void notifyStories(Cursor storiesFocus, Cursor storiesUnread, Context context, FileCache iconCache) {
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
int count = 0;
while (storiesFocus.moveToNext()) {
Story story = Story.fromCursor(storiesFocus);
if (story.read) {
nm.cancel(story.hashCode());
continue;
}
if (count < MAX_CONCUR_NOTIFY) {
Notification n = buildStoryNotification(story, storiesFocus, context, iconCache);
nm.notify(story.hashCode(), n);
} else {
nm.cancel(story.hashCode());
}
count++;
}
while (storiesUnread.moveToNext()) {
Story story = Story.fromCursor(storiesUnread);
if (story.read) {
nm.cancel(story.hashCode());
continue;
}
if (count < MAX_CONCUR_NOTIFY) {
Notification n = buildStoryNotification(story, storiesUnread, context, iconCache);
nm.notify(story.hashCode(), n);
} else {
nm.cancel(story.hashCode());
}
count++;
}
}
private static Notification buildStoryNotification(Story story, Cursor cursor, Context context, FileCache iconCache) {
Intent i = new Intent(context, FeedReading.class);
// the action is unused, but bugs in some platform versions ignore extras if it is unset
i.setAction(story.storyHash);
// these extras actually dictate activity behaviour
i.putExtra(Reading.EXTRA_FEEDSET, FeedSet.singleFeed(story.feedId));
i.putExtra(Reading.EXTRA_STORY_HASH, story.storyHash);
// force a new Reading activity, since if multiple notifications are tapped, any re-use or
// stacking of the activity would almost certainly out-race the sync loop and cause stale
// UI on some devices.
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
// set the requestCode to the story hashcode to prevent the PI re-using the wrong Intent
PendingIntent pendingIntent = PendingIntent.getActivity(context, story.hashCode(), i, 0);
String feedTitle = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_TITLE));
StringBuilder title = new StringBuilder();
title.append(feedTitle).append(": ").append(story.title);
String faviconUrl = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_FAVICON_URL));
Bitmap feedIcon = ImageLoader.getCachedImageSynchro(iconCache, faviconUrl);
Notification.Builder nb = new Notification.Builder(context)
.setContentTitle(title.toString())
.setContentText(story.shortContent)
.setSmallIcon(R.drawable.logo_monochrome)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setWhen(story.timestamp);
if (feedIcon != null) {
nb.setLargeIcon(feedIcon);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
nb.setColor(NOTIFY_COLOUR);
}
return nb.build();
}
public static void clear(Context context) {
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancelAll();
}
}

View file

@ -116,6 +116,8 @@ public class PrefsUtils {
NBSyncService.softInterrupt();
NBSyncService.clearState();
NotificationUtils.clear(context);
// wipe the prefs store
context.getSharedPreferences(PrefConstants.PREFERENCES, 0).edit().clear().commit();