diff --git a/clients/android/NewsBlur/AndroidManifest.xml b/clients/android/NewsBlur/AndroidManifest.xml index 5c7931fc4..6e9bc30ae 100644 --- a/clients/android/NewsBlur/AndroidManifest.xml +++ b/clients/android/NewsBlur/AndroidManifest.xml @@ -2,11 +2,11 @@ + android:versionName="4.2.0b3" > + android:targetSdkVersion="21" /> @@ -55,8 +55,9 @@ - + android:label="@string/newsblur" + android:launchMode="singleTask" + android:alwaysRetainTaskState="true" /> + + + + + + diff --git a/clients/android/NewsBlur/project.properties b/clients/android/NewsBlur/project.properties index 738e84e9f..b84c9a2a3 100644 --- a/clients/android/NewsBlur/project.properties +++ b/clients/android/NewsBlur/project.properties @@ -11,4 +11,4 @@ proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. -target=android-19 +target=android-21 diff --git a/clients/android/NewsBlur/res/anim/slide_in_from_left.xml b/clients/android/NewsBlur/res/anim/slide_in_from_left.xml index 4f3b09f17..9816e732c 100644 --- a/clients/android/NewsBlur/res/anim/slide_in_from_left.xml +++ b/clients/android/NewsBlur/res/anim/slide_in_from_left.xml @@ -2,7 +2,7 @@ diff --git a/clients/android/NewsBlur/res/anim/slide_out_to_right.xml b/clients/android/NewsBlur/res/anim/slide_out_to_right.xml index 0c8afaebf..319d7d408 100644 --- a/clients/android/NewsBlur/res/anim/slide_out_to_right.xml +++ b/clients/android/NewsBlur/res/anim/slide_out_to_right.xml @@ -2,7 +2,7 @@ diff --git a/clients/android/NewsBlur/res/layout/activity_itemslist.xml b/clients/android/NewsBlur/res/layout/activity_itemslist.xml index 9cc437dc4..107fa9709 100644 --- a/clients/android/NewsBlur/res/layout/activity_itemslist.xml +++ b/clients/android/NewsBlur/res/layout/activity_itemslist.xml @@ -15,7 +15,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" - android:padding="3dp" + android:padding="2dp" android:textSize="14sp" android:gravity="center" android:textColor="@color/status_overlay_text" diff --git a/clients/android/NewsBlur/res/layout/activity_main.xml b/clients/android/NewsBlur/res/layout/activity_main.xml index 74a004b78..6a3829661 100644 --- a/clients/android/NewsBlur/res/layout/activity_main.xml +++ b/clients/android/NewsBlur/res/layout/activity_main.xml @@ -39,7 +39,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_above="@+id/fragment_feedintelligenceselector" - android:padding="3dp" + android:padding="2dp" android:textSize="14sp" android:gravity="center" android:textColor="@color/status_overlay_text" diff --git a/clients/android/NewsBlur/res/values/colors.xml b/clients/android/NewsBlur/res/values/colors.xml index 7b21fafc9..edc05a41d 100644 --- a/clients/android/NewsBlur/res/values/colors.xml +++ b/clients/android/NewsBlur/res/values/colors.xml @@ -104,7 +104,7 @@ #8F918B #D5D7CF - #DD111111 + #DDFFFFFF #AA777777 #90928b diff --git a/clients/android/NewsBlur/res/values/strings.xml b/clients/android/NewsBlur/res/values/strings.xml index cf366a8ee..915ee76d3 100644 --- a/clients/android/NewsBlur/res/values/strings.xml +++ b/clients/android/NewsBlur/res/values/strings.xml @@ -110,26 +110,14 @@ Mark as read Mark as unread Full screen - - Error loading stories Stories marked as read - - Error marking feed as read. Check your internet connection. - - Story saved - Error marking story as saved. - Story unsaved - Error marking story as unsaved. Story marked as unread - Error marking story as unread - Error marking story as unread Could not load next unread story Feed deleted - There was an error deleting the feed. Are you sure you want to log out? @@ -148,7 +136,6 @@ No stories to read Register - Add some sites Let\'s get started Add your friends Connect with your friends to easily follow the stories that matter to them @@ -159,11 +146,6 @@ Add Facebook friends I need to log in! I need to register - Wonderful things are happening at NewsBlur. Add our blog for the latest news. - Follow the NewsBlur blog - Follow Popular Stories - All Done! - Share with comments Share this story Comment favorited Error favoriting comment @@ -260,6 +242,7 @@ Storing%sunread stories... Storing text for %s stories... Storing %s images... + Offline Volume Key Navigation Off diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/AllStoriesItemsList.java b/clients/android/NewsBlur/src/com/newsblur/activity/AllStoriesItemsList.java index 92979fd3a..b38444af7 100644 --- a/clients/android/NewsBlur/src/com/newsblur/activity/AllStoriesItemsList.java +++ b/clients/android/NewsBlur/src/com/newsblur/activity/AllStoriesItemsList.java @@ -1,21 +1,12 @@ package com.newsblur.activity; -import java.util.LinkedHashSet; -import java.util.List; - -import android.content.ContentValues; -import android.content.Intent; -import android.database.Cursor; -import android.os.AsyncTask; import android.os.Bundle; import android.app.FragmentTransaction; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; -import android.widget.Toast; import com.newsblur.R; -import com.newsblur.database.DatabaseConstants; import com.newsblur.fragment.AllStoriesItemListFragment; import com.newsblur.fragment.FeedItemListFragment; import com.newsblur.fragment.MarkAllReadDialogFragment; diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/FeedSearch.java b/clients/android/NewsBlur/src/com/newsblur/activity/FeedSearch.java deleted file mode 100644 index 292eb3cb5..000000000 --- a/clients/android/NewsBlur/src/com/newsblur/activity/FeedSearch.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.newsblur.activity; - - -public class FeedSearch extends NbActivity { - -} diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/FolderItemsList.java b/clients/android/NewsBlur/src/com/newsblur/activity/FolderItemsList.java index 5760bd37c..f426ecaf0 100644 --- a/clients/android/NewsBlur/src/com/newsblur/activity/FolderItemsList.java +++ b/clients/android/NewsBlur/src/com/newsblur/activity/FolderItemsList.java @@ -15,7 +15,6 @@ import android.widget.Toast; import android.util.Log; import com.newsblur.R; -import com.newsblur.database.DatabaseConstants; import com.newsblur.fragment.FolderItemListFragment; import com.newsblur.fragment.MarkAllReadDialogFragment; import com.newsblur.fragment.MarkAllReadDialogFragment.MarkAllReadDialogListener; diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/GlobalSharedStoriesItemsList.java b/clients/android/NewsBlur/src/com/newsblur/activity/GlobalSharedStoriesItemsList.java index 00488d9c9..7dc50c24b 100644 --- a/clients/android/NewsBlur/src/com/newsblur/activity/GlobalSharedStoriesItemsList.java +++ b/clients/android/NewsBlur/src/com/newsblur/activity/GlobalSharedStoriesItemsList.java @@ -1,7 +1,6 @@ package com.newsblur.activity; import android.app.FragmentTransaction; -import android.content.ContentResolver; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; @@ -17,16 +16,12 @@ import com.newsblur.util.StoryOrder; public class GlobalSharedStoriesItemsList extends ItemsList { - private ContentResolver resolver; - @Override protected void onCreate(Bundle bundle) { super.onCreate(bundle); setTitle(getResources().getString(R.string.global_shared_stories)); - resolver = getContentResolver(); - itemListFragment = (GlobalSharedStoriesItemListFragment) fragmentManager.findFragmentByTag(GlobalSharedStoriesItemListFragment.class.getName()); if (itemListFragment == null) { itemListFragment = GlobalSharedStoriesItemListFragment.newInstance(getDefaultFeedView(), currentState); diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/ItemsList.java b/clients/android/NewsBlur/src/com/newsblur/activity/ItemsList.java index 3042f25b1..f0ea56d47 100644 --- a/clients/android/NewsBlur/src/com/newsblur/activity/ItemsList.java +++ b/clients/android/NewsBlur/src/com/newsblur/activity/ItemsList.java @@ -81,6 +81,12 @@ public abstract class ItemsList extends NbActivity implements StateChangedListen itemListFragment.hasUpdated(); } + @Override + protected void onPause() { + super.onPause(); + NBSyncService.addRecountCandidates(fs); + } + public void markItemListAsRead() { FeedUtils.markFeedsRead(fs, null, null, this); Toast.makeText(this, R.string.toast_marked_stories_as_read, Toast.LENGTH_SHORT).show(); diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/Main.java b/clients/android/NewsBlur/src/com/newsblur/activity/Main.java index c46cf4ba7..e8ccfa76b 100644 --- a/clients/android/NewsBlur/src/com/newsblur/activity/Main.java +++ b/clients/android/NewsBlur/src/com/newsblur/activity/Main.java @@ -74,6 +74,7 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre super.onResume(); NBSyncService.clearPendingStoryRequest(); + NBSyncService.flushRecounts(); NBSyncService.setActivationMode(NBSyncService.ActivationMode.ALL); FeedUtils.activateAllStories(); FeedUtils.clearReadingSession(); @@ -155,12 +156,6 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre folderFeedList.changeState(state); } - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode == RESULT_OK) { - folderFeedList.hasUpdated(); - } - } - @Override public void handleUpdate(boolean freshData) { updateStatusIndicators(); diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/Reading.java b/clients/android/NewsBlur/src/com/newsblur/activity/Reading.java index d4f838688..c7d35bebf 100644 --- a/clients/android/NewsBlur/src/com/newsblur/activity/Reading.java +++ b/clients/android/NewsBlur/src/com/newsblur/activity/Reading.java @@ -99,6 +99,8 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener private float overlayRangeTopPx; private float overlayRangeBotPx; + private int lastVScrollPos = 0; + private List pageHistory; protected DefaultFeedView defaultFeedView; @@ -377,9 +379,10 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener @Override public void scrollChanged(int hPos, int vPos, int currentWidth, int currentHeight) { - // only update overlay alpha about half the time. modern screens are so dense that it + // only update overlay alpha every few pixels. modern screens are so dense that it // is way overkill to do it on every pixel - if (vPos % 2 == 1) return; + if (Math.abs(lastVScrollPos-vPos) < 2) return; + lastVScrollPos = vPos; int scrollMax = currentHeight - contentView.getMeasuredHeight(); int posFromBot = (scrollMax - vPos); @@ -710,6 +713,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener } public void overlaySend(View v) { + if ((readingAdapter == null) || (pager == null)) return; Story story = readingAdapter.getStory(pager.getCurrentItem()); FeedUtils.shareStory(story, this); } diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/SavedStoriesItemsList.java b/clients/android/NewsBlur/src/com/newsblur/activity/SavedStoriesItemsList.java index 89fe4e18f..a3ec3d9e3 100644 --- a/clients/android/NewsBlur/src/com/newsblur/activity/SavedStoriesItemsList.java +++ b/clients/android/NewsBlur/src/com/newsblur/activity/SavedStoriesItemsList.java @@ -1,12 +1,5 @@ package com.newsblur.activity; -import java.util.ArrayList; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Intent; -import android.database.Cursor; -import android.os.AsyncTask; import android.os.Bundle; import android.app.FragmentTransaction; import android.view.Menu; @@ -14,7 +7,6 @@ import android.view.MenuInflater; import android.widget.Toast; import com.newsblur.R; -import com.newsblur.database.DatabaseConstants; import com.newsblur.fragment.SavedStoriesItemListFragment; import com.newsblur.fragment.FeedItemListFragment; import com.newsblur.util.DefaultFeedView; @@ -27,16 +19,12 @@ import com.newsblur.util.StoryOrder; public class SavedStoriesItemsList extends ItemsList { - private ContentResolver resolver; - @Override protected void onCreate(Bundle bundle) { super.onCreate(bundle); setTitle(getResources().getString(R.string.saved_stories_title)); - resolver = getContentResolver(); - itemListFragment = (SavedStoriesItemListFragment) fragmentManager.findFragmentByTag(SavedStoriesItemListFragment.class.getName()); if (itemListFragment == null) { itemListFragment = SavedStoriesItemListFragment.newInstance(getDefaultFeedView()); diff --git a/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java b/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java index 91c6951c4..4f445093d 100644 --- a/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java +++ b/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java @@ -355,33 +355,25 @@ public class BlurDatabaseHelper { } } + /** + * Marks a story (un)read but does not adjust counts. + */ public void setStoryReadState(String hash, boolean read) { - Cursor c = getStory(hash); - if (c.getCount() < 1) { - Log.w(this.getClass().getName(), "story removed before finishing mark-read"); - return; - } - Story story = Story.fromCursor(c); - if (story == null) { - Log.w(this.getClass().getName(), "story removed before finishing mark-read"); - return; - } - setStoryReadState(story, read); - + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_READ, read); + values.put(DatabaseConstants.STORY_READ_THIS_SESSION, read); + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});} } /** * Marks a story (un)read and also adjusts unread counts for it. + * + * @return the set of feed IDs that potentially have counts impacted by the mark. */ - public void setStoryReadState(Story story, boolean read) { - // read flag - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.STORY_READ, read); - values.put(DatabaseConstants.STORY_READ_THIS_SESSION, read); - synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{story.storyHash});} - // non-social feed count - refreshFeedCounts(FeedSet.singleFeed(story.feedId)); - // social feed counts + public Set setStoryReadState(Story story, boolean read) { + // calculate the impact surface so the caller can re-check counts if needed + Set impactedFeeds = new HashSet(); + impactedFeeds.add(FeedSet.singleFeed(story.feedId)); Set socialIds = new HashSet(); if (!TextUtils.isEmpty(story.socialUserId)) { socialIds.add(story.socialUserId); @@ -392,10 +384,73 @@ public class BlurDatabaseHelper { } } if (socialIds.size() > 0) { - refreshFeedCounts(FeedSet.multipleSocialFeeds(socialIds)); + impactedFeeds.add(FeedSet.multipleSocialFeeds(socialIds)); } + // check the story's starting state and the desired state and adjust it as an atom so we + // know if it truly changed or not + synchronized (RW_MUTEX) { + dbRW.beginTransaction(); + try { + // get a fresh copy of the story from the DB so we know if it changed + Cursor c = dbRW.query(DatabaseConstants.STORY_TABLE, + new String[]{DatabaseConstants.STORY_READ}, + DatabaseConstants.STORY_HASH + " = ?", + new String[]{story.storyHash}, + null, null, null); + if (c.getCount() < 1) { + Log.w(this.getClass().getName(), "story removed before finishing mark-read"); + return impactedFeeds; + } + c.moveToFirst(); + boolean origState = (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.STORY_READ)) > 0); + c.close(); + // if there is nothing to be done, halt + if (origState == read) { + dbRW.setTransactionSuccessful(); + return impactedFeeds; + } + // update the story's read state + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.STORY_READ, read); + values.put(DatabaseConstants.STORY_READ_THIS_SESSION, read); + dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{story.storyHash}); + // which column to inc/dec depends on story intel + String impactedCol; + String impactedSocialCol; + if (story.intelTotal < 0) { + // negative stories don't affect counts + dbRW.setTransactionSuccessful(); + return impactedFeeds; + } else if (story.intelTotal == 0 ) { + impactedCol = DatabaseConstants.FEED_NEUTRAL_COUNT; + impactedSocialCol = DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT; + } else { + impactedCol = DatabaseConstants.FEED_POSITIVE_COUNT; + impactedSocialCol = DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT; + } + String operator = (read ? " - 1" : " + 1"); + StringBuilder q = new StringBuilder("UPDATE " + DatabaseConstants.FEED_TABLE); + q.append(" SET ").append(impactedCol).append(" = ").append(impactedCol).append(operator); + q.append(" WHERE " + DatabaseConstants.FEED_ID + " = ").append(story.feedId); + dbRW.execSQL(q.toString()); + for (String socialId : socialIds) { + q = new StringBuilder("UPDATE " + DatabaseConstants.SOCIALFEED_TABLE); + q.append(" SET ").append(impactedSocialCol).append(" = ").append(impactedSocialCol).append(operator); + q.append(" WHERE " + DatabaseConstants.SOCIAL_FEED_ID + " = ").append(socialId); + dbRW.execSQL(q.toString()); + } + dbRW.setTransactionSuccessful(); + } finally { + dbRW.endTransaction(); + } + } + return impactedFeeds; } + /** + * Marks a range of stories in a subset of feeds as read. Does not update unread counts; + * the caller must use updateLocalFeedCounts() or the /reader/feed_unread_count API. + */ public void markStoriesRead(FeedSet fs, Long olderThan, Long newerThan) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_READ, true); @@ -419,14 +474,73 @@ public class BlurDatabaseHelper { throw new IllegalStateException("Asked to mark stories for FeedSet of unknown type."); } synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, conjoinSelections(feedSelection, rangeSelection), null);} + } - refreshFeedCounts(fs); + /** + * Get the unread count for the given feedset based on the totals in the feeds table. + */ + public int getUnreadCount(FeedSet fs, StateFilter stateFilter) { + if (fs.isAllNormal()) { + return getFeedsUnreadCount(stateFilter, null, null); + } else if (fs.isAllSocial()) { + //return getSocialFeedsUnreadCount(stateFilter, null, null); + // even though we can count up and total the unreads in social feeds, the API doesn't vend + // unread status for stories viewed when reading All Shared Stories, so force this to 0. + return 0; + } else if (fs.getMultipleFeeds() != null) { + StringBuilder selection = new StringBuilder(DatabaseConstants.FEED_ID + " IN ( "); + selection.append(TextUtils.join(",", fs.getMultipleFeeds())).append(")"); + return getFeedsUnreadCount(stateFilter, selection.toString(), null); + } else if (fs.getMultipleSocialFeeds() != null) { + StringBuilder selection = new StringBuilder(DatabaseConstants.SOCIAL_FEED_ID + " IN ( "); + selection.append(TextUtils.join(",", fs.getMultipleFeeds())).append(")"); + return getSocialFeedsUnreadCount(stateFilter, selection.toString(), null); + } else if (fs.getSingleFeed() != null) { + return getFeedsUnreadCount(stateFilter, DatabaseConstants.FEED_ID + " = ?", new String[]{fs.getSingleFeed()}); + } else if (fs.getSingleSocialFeed() != null) { + return getSocialFeedsUnreadCount(stateFilter, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{fs.getSingleSocialFeed().getKey()}); + } else { + // all other types of view don't track unreads correctly + return 0; + } + } + + private int getFeedsUnreadCount(StateFilter stateFilter, String selection, String[] selArgs) { + int result = 0; + Cursor c = dbRO.query(DatabaseConstants.FEED_TABLE, null, selection, selArgs, null, null, null); + while (c.moveToNext()) { + Feed f = Feed.fromCursor(c); + result += f.positiveCount; + if ((stateFilter == StateFilter.SOME) || (stateFilter == StateFilter.ALL)) result += f.neutralCount; + if (stateFilter == StateFilter.ALL) result += f.negativeCount; + } + return result; + } + + private int getSocialFeedsUnreadCount(StateFilter stateFilter, String selection, String[] selArgs) { + int result = 0; + Cursor c = dbRO.query(DatabaseConstants.SOCIALFEED_TABLE, null, selection, selArgs, null, null, null); + while (c.moveToNext()) { + SocialFeed f = SocialFeed.fromCursor(c); + result += f.positiveCount; + if ((stateFilter == StateFilter.SOME) || (stateFilter == StateFilter.ALL)) result += f.neutralCount; + if (stateFilter == StateFilter.ALL) result += f.negativeCount; + } + return result; + } + + public void updateFeedCounts(String feedId, ContentValues values) { + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} + } + + public void updateSocialFeedCounts(String feedId, ContentValues values) { + synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{feedId});} } /** * Refreshes the counts in the feeds/socialfeeds tables by counting stories in the story table. */ - public void refreshFeedCounts(FeedSet fs) { + public void updateLocalFeedCounts(FeedSet fs) { // decompose the FeedSet into a list of single feeds that need to be recounted List feedIds = new ArrayList(); List socialFeedIds = new ArrayList(); @@ -450,23 +564,26 @@ public class BlurDatabaseHelper { for (String feedId : feedIds) { FeedSet singleFs = FeedSet.singleFeed(feedId); ContentValues values = new ContentValues(); - values.put(DatabaseConstants.FEED_NEGATIVE_COUNT, getUnreadCount(singleFs, StateFilter.NEG)); - values.put(DatabaseConstants.FEED_NEUTRAL_COUNT, getUnreadCount(singleFs, StateFilter.NEUT)); - values.put(DatabaseConstants.FEED_POSITIVE_COUNT, getUnreadCount(singleFs, StateFilter.BEST)); + values.put(DatabaseConstants.FEED_NEGATIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEG)); + values.put(DatabaseConstants.FEED_NEUTRAL_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEUT)); + values.put(DatabaseConstants.FEED_POSITIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.BEST)); synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});} } for (String socialId : socialFeedIds) { FeedSet singleFs = FeedSet.singleSocialFeed(socialId, ""); ContentValues values = new ContentValues(); - values.put(DatabaseConstants.SOCIAL_FEED_NEGATIVE_COUNT, getUnreadCount(singleFs, StateFilter.NEG)); - values.put(DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT, getUnreadCount(singleFs, StateFilter.NEUT)); - values.put(DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT, getUnreadCount(singleFs, StateFilter.BEST)); + values.put(DatabaseConstants.SOCIAL_FEED_NEGATIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEG)); + values.put(DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEUT)); + values.put(DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.BEST)); synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{socialId});} } } - public int getUnreadCount(FeedSet fs, StateFilter stateFilter) { + /** + * Get the unread count for the given feedset based on local story state. + */ + public int getLocalUnreadCount(FeedSet fs, StateFilter stateFilter) { Cursor c = getStoriesCursor(fs, stateFilter, ReadFilter.PURE_UNREAD, null, null); int count = c.getCount(); c.close(); @@ -486,12 +603,6 @@ public class BlurDatabaseHelper { synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.ACTION_TABLE, DatabaseConstants.ACTION_ID + " = ?", new String[]{actionId});} } - public Cursor getStory(String hash) { - String q = "SELECT * FROM " + DatabaseConstants.STORY_TABLE + - " WHERE " + DatabaseConstants.STORY_HASH + " = ?"; - return dbRO.rawQuery(q, new String[]{hash}); - } - public void setStoryStarred(String hash, boolean starred) { ContentValues values = new ContentValues(); values.put(DatabaseConstants.STORY_STARRED, starred); diff --git a/clients/android/NewsBlur/src/com/newsblur/domain/Story.java b/clients/android/NewsBlur/src/com/newsblur/domain/Story.java index 40cc63c1a..9125fdff4 100644 --- a/clients/android/NewsBlur/src/com/newsblur/domain/Story.java +++ b/clients/android/NewsBlur/src/com/newsblur/domain/Story.java @@ -81,6 +81,8 @@ public class Story implements Serializable { @SerializedName("intelligence") public Intelligence intelligence = new Intelligence(); + public int intelTotal; + @SerializedName("short_parsed_date") public String shortDate; @@ -149,6 +151,7 @@ public class Story implements Serializable { story.intelligence.intelligenceFeed = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_INTELLIGENCE_FEED)); story.intelligence.intelligenceTags = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_INTELLIGENCE_TAGS)); story.intelligence.intelligenceTitle = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_INTELLIGENCE_TITLE)); + story.intelTotal = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.SUM_STORY_TOTAL)); story.read = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_READ)) > 0; story.starred = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_STARRED)) > 0; story.starredTimestamp = cursor.getLong(cursor.getColumnIndex(DatabaseConstants.STORY_STARRED_DATE)); diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/LogoutDialogFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/LogoutDialogFragment.java index 5a7ad10b8..9b68be2f5 100644 --- a/clients/android/NewsBlur/src/com/newsblur/fragment/LogoutDialogFragment.java +++ b/clients/android/NewsBlur/src/com/newsblur/fragment/LogoutDialogFragment.java @@ -19,6 +19,9 @@ public class LogoutDialogFragment extends DialogFragment { @Override public void onClick(DialogInterface dialogInterface, int i) { PrefsUtils.logout(getActivity()); + // make sure the instance of Main that called us is killed now, or else the system + // might try to recycle it with a stale login ID, which will cause it to self-destruct + getActivity().finish(); } }); builder.setNegativeButton(R.string.alert_dialog_cancel, new DialogInterface.OnClickListener() { diff --git a/clients/android/NewsBlur/src/com/newsblur/network/APIConstants.java b/clients/android/NewsBlur/src/com/newsblur/network/APIConstants.java index 88bde7b3c..c47035d13 100644 --- a/clients/android/NewsBlur/src/com/newsblur/network/APIConstants.java +++ b/clients/android/NewsBlur/src/com/newsblur/network/APIConstants.java @@ -25,6 +25,7 @@ public class APIConstants { public static final String URL_SHARED_RIVER_STORIES = NEWSBLUR_URL + "/social/river_stories"; public static final String URL_FEED_STORIES = NEWSBLUR_URL + "/reader/feed"; + public static final String URL_FEED_UNREAD_COUNT = NEWSBLUR_URL + "/reader/feed_unread_count"; public static final String URL_SOCIALFEED_STORIES = NEWSBLUR_URL + "/social/stories"; public static final String URL_SIGNUP = NEWSBLUR_URL + "/api/signup"; public static final String URL_MARK_FEED_AS_READ = NEWSBLUR_URL + "/reader/mark_feed_as_read/"; diff --git a/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java b/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java index e0e5502ac..7e66c78f8 100644 --- a/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java +++ b/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java @@ -11,8 +11,8 @@ import java.util.Date; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; -import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; @@ -38,6 +38,7 @@ import com.newsblur.network.domain.ProfileResponse; import com.newsblur.network.domain.RegisterResponse; import com.newsblur.network.domain.StoriesResponse; import com.newsblur.network.domain.StoryTextResponse; +import com.newsblur.network.domain.UnreadCountResponse; import com.newsblur.network.domain.UnreadStoryHashesResponse; import com.newsblur.serialization.BooleanTypeAdapter; import com.newsblur.serialization.ClassifierMapTypeAdapter; @@ -58,12 +59,10 @@ public class APIManager { private Context context; private Gson gson; - private ContentResolver contentResolver; private String customUserAgent; public APIManager(final Context context) { this.context = context; - this.contentResolver = context.getContentResolver(); this.gson = new GsonBuilder() .registerTypeAdapter(Date.class, new DateStringTypeAdapter()) @@ -246,6 +245,15 @@ public class APIManager { } } + public UnreadCountResponse getFeedUnreadCounts(Set apiIds) { + ValueMultimap values = new ValueMultimap(); + for (String id : apiIds) { + values.put(APIConstants.PARAMETER_FEEDID, id); + } + APIResponse response = get(APIConstants.URL_FEED_UNREAD_COUNT, values); + return (UnreadCountResponse) response.getResponse(gson, UnreadCountResponse.class); + } + public UnreadStoryHashesResponse getUnreadStoryHashes() { ValueMultimap values = new ValueMultimap(); values.put(APIConstants.PARAMETER_INCLUDE_TIMESTAMPS, "1"); @@ -370,7 +378,9 @@ public class APIManager { } // note: this response is complex enough, we have to do a custom parse in the FFR - return new FeedFolderResponse(response.getResponseBody(), gson); + FeedFolderResponse result = new FeedFolderResponse(response.getResponseBody(), gson); + result.readTime = response.readTime; + return result; } public NewsBlurResponse trainClassifier(String feedId, String key, int type, int action) { diff --git a/clients/android/NewsBlur/src/com/newsblur/network/APIResponse.java b/clients/android/NewsBlur/src/com/newsblur/network/APIResponse.java index 43c6b4a02..3e3835945 100644 --- a/clients/android/NewsBlur/src/com/newsblur/network/APIResponse.java +++ b/clients/android/NewsBlur/src/com/newsblur/network/APIResponse.java @@ -1,9 +1,10 @@ package com.newsblur.network; +import java.io.InputStreamReader; import java.io.IOException; +import java.io.Reader; import java.net.HttpURLConnection; import java.net.URL; -import java.util.Scanner; import android.content.Context; import android.text.TextUtils; @@ -28,6 +29,7 @@ public class APIResponse { private String errorMessage; private String cookie; private String responseBody; + public long readTime; /** * Construct an online response. Will test the response for errors and extract all the @@ -71,8 +73,14 @@ public class APIResponse { try { StringBuilder builder = new StringBuilder(); - Scanner scanner = new Scanner(connection.getInputStream(), "UTF-8"); - while (scanner.hasNextLine()) { builder.append(scanner.nextLine()); } + Reader reader = new InputStreamReader(connection.getInputStream()); + char[] chunk = new char[1024]; + int len; + long startTime = System.currentTimeMillis(); + while ( (len = reader.read(chunk)) > 0) { + builder.append(chunk, 0, len); + } + readTime = System.currentTimeMillis() - startTime; this.responseBody = builder.toString(); } catch (Exception e) { Log.e(this.getClass().getName(), e.getClass().getName() + " (" + e.getMessage() + ") reading " + originalUrl, e); @@ -81,16 +89,23 @@ public class APIResponse { return; } - if (AppConstants.VERBOSE_LOG_NET) { - Log.d(this.getClass().getName(), "received API response: \n" + this.responseBody); - } - try { connection.disconnect(); } catch (Exception e) { Log.e(this.getClass().getName(), e.getClass().getName() + " caught closing connection: " + e.getMessage(), e); } - + + if (AppConstants.VERBOSE_LOG_NET) { + // the default kernel truncates log lines. split by something we probably have, like a json delim + if (responseBody.length() < 2048) { + Log.d(this.getClass().getName(), "API response: \n" + this.responseBody); + } else { + Log.d(this.getClass().getName(), "API response: "); + for (String s : TextUtils.split(responseBody, "\\}")) { + Log.d(this.getClass().getName(), s + "}"); + } + } + } } /** @@ -131,7 +146,9 @@ public class APIResponse { } else { // otherwise, parse the response as the expected class and defer error detection // to the NewsBlurResponse parent class - return gson.fromJson(this.responseBody, classOfT); + T response = gson.fromJson(this.responseBody, classOfT); + response.readTime = readTime; + return response; } } diff --git a/clients/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java b/clients/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java index a731b7168..798084358 100644 --- a/clients/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java +++ b/clients/android/NewsBlur/src/com/newsblur/network/domain/FeedFolderResponse.java @@ -21,6 +21,8 @@ import com.newsblur.domain.SocialFeed; import com.newsblur.util.AppConstants; public class FeedFolderResponse { + + public long readTime; @SerializedName("starred_count") public int starredCount; diff --git a/clients/android/NewsBlur/src/com/newsblur/network/domain/FeedRefreshResponse.java b/clients/android/NewsBlur/src/com/newsblur/network/domain/FeedRefreshResponse.java deleted file mode 100644 index 22c984026..000000000 --- a/clients/android/NewsBlur/src/com/newsblur/network/domain/FeedRefreshResponse.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.newsblur.network.domain; - -import java.util.Map; - -import android.content.ContentValues; - -import com.google.gson.annotations.SerializedName; -import com.newsblur.database.DatabaseConstants; - -public class FeedRefreshResponse extends NewsBlurResponse { - - - @SerializedName("feeds") - public Map feedCounts; - - @SerializedName("social_feeds") - public Map socialfeedCounts; - - public class Count { - - @SerializedName("ps") - int positive; - - @SerializedName("ng") - int negative; - - @SerializedName("nt") - int neutral; - - public ContentValues getValues() { - ContentValues values = new ContentValues(); - values.put(DatabaseConstants.FEED_NEGATIVE_COUNT, negative); - values.put(DatabaseConstants.FEED_NEUTRAL_COUNT, neutral); - values.put(DatabaseConstants.FEED_POSITIVE_COUNT, positive); - return values; - } - - } - -} diff --git a/clients/android/NewsBlur/src/com/newsblur/network/domain/NewsBlurResponse.java b/clients/android/NewsBlur/src/com/newsblur/network/domain/NewsBlurResponse.java index 3225cd615..5bc485787 100644 --- a/clients/android/NewsBlur/src/com/newsblur/network/domain/NewsBlurResponse.java +++ b/clients/android/NewsBlur/src/com/newsblur/network/domain/NewsBlurResponse.java @@ -2,6 +2,9 @@ package com.newsblur.network.domain; import android.util.Log; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * A generic response to an API call that only encapsuates success versus failure. */ @@ -11,6 +14,9 @@ public class NewsBlurResponse { public int code; public String message; public ResponseErrors errors; + public long readTime; + + public static final Pattern KnownUserErrors = Pattern.compile("cannot mark as unread"); public boolean isError() { if ((message != null) && (!message.equals(""))) { @@ -24,6 +30,20 @@ public class NewsBlurResponse { return false; } + // TODO: can we add a canonical flag of some sort to 100% of API responses that differentiates + // between 400-type and 2/3/500-type errors? Until then, we have to sniff known bad ones. + public boolean isUserError() { + if (message != null) { + Matcher m = KnownUserErrors.matcher(message); + if (m.find()) return true; + } + if ((errors != null) && (errors.message.length > 0) && (errors.message[0] != null)) { + Matcher m = KnownUserErrors.matcher(errors.message[0]); + if (m.find()) return true; + } + return false; + } + /** * Gets the error message returned by the API, or defaultMessage if none was found. */ diff --git a/clients/android/NewsBlur/src/com/newsblur/network/domain/UnreadCountResponse.java b/clients/android/NewsBlur/src/com/newsblur/network/domain/UnreadCountResponse.java new file mode 100644 index 000000000..c65708c30 --- /dev/null +++ b/clients/android/NewsBlur/src/com/newsblur/network/domain/UnreadCountResponse.java @@ -0,0 +1,34 @@ +package com.newsblur.network.domain; + +import android.content.ContentValues; + +import java.util.Map; + +import com.google.gson.annotations.SerializedName; +import com.newsblur.database.DatabaseConstants; + +public class UnreadCountResponse extends NewsBlurResponse { + + @SerializedName("feeds") + public Map feeds; + + @SerializedName("social_feeds") + public Map socialFeeds; + + public class UnreadMD { + + public int ps; + public int nt; + public int ng; + + public ContentValues getValues() { + ContentValues values = new ContentValues(); + values.put(DatabaseConstants.FEED_POSITIVE_COUNT, ps); + values.put(DatabaseConstants.FEED_NEUTRAL_COUNT, nt); + values.put(DatabaseConstants.FEED_NEGATIVE_COUNT, ng); + return values; + } + + } + +} diff --git a/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java b/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java index 241fc3db3..050a9fd57 100644 --- a/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java +++ b/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java @@ -20,10 +20,12 @@ import static com.newsblur.database.BlurDatabaseHelper.closeQuietly; import com.newsblur.database.DatabaseConstants; import com.newsblur.domain.SocialFeed; import com.newsblur.domain.Story; +import com.newsblur.network.APIConstants; import com.newsblur.network.APIManager; import com.newsblur.network.domain.FeedFolderResponse; import com.newsblur.network.domain.NewsBlurResponse; import com.newsblur.network.domain.StoriesResponse; +import com.newsblur.network.domain.UnreadCountResponse; import com.newsblur.network.domain.UnreadStoryHashesResponse; import com.newsblur.util.AppConstants; import com.newsblur.util.FeedSet; @@ -32,6 +34,7 @@ import com.newsblur.util.NetworkUtils; import com.newsblur.util.PrefsUtils; import com.newsblur.util.ReadingAction; import com.newsblur.util.ReadFilter; +import com.newsblur.util.StateFilter; import com.newsblur.util.StoryOrder; import java.util.ArrayList; @@ -76,6 +79,7 @@ public class NBSyncService extends Service { private volatile static boolean FFSyncRunning = false; private volatile static boolean StorySyncRunning = false; private volatile static boolean HousekeepingRunning = false; + private volatile static boolean RecountsRunning = false; private volatile static boolean DoFeedsFolders = false; private volatile static boolean DoUnreads = false; @@ -83,11 +87,15 @@ public class NBSyncService extends Service { private volatile static ActivationMode ActMode = ActivationMode.ALL; private volatile static long ModeCutoff = 0L; + /** Informational flag only, as to whether we were offline last time we cycled. */ + public volatile static boolean OfflineNow = false; + public volatile static Boolean isPremium = null; public volatile static Boolean isStaff = null; private volatile static boolean isMemoryLow = false; private static long lastFeedCount = 0L; + private static long lastFFReadMillis = 0L; private static long lastFFWriteMillis = 0L; /** Feed set that we need to sync immediately for the UI. */ @@ -108,6 +116,11 @@ public class NBSyncService extends Service { private static List FollowupActions; static { FollowupActions = new ArrayList(); } + /** Feed IDs (API stype) that have been acted upon and need a double-check for counts. */ + private static Set RecountCandidates; + static { RecountCandidates = new HashSet(); } + private volatile static boolean FlushRecounts = false; + Set orphanFeedIds; private ExecutorService primaryExecutor; @@ -192,6 +205,11 @@ public class NBSyncService extends Service { Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE); } + if (OfflineNow) { + OfflineNow = false; + NbActivity.updateAllActivities(false); + } + // do this even if background syncs aren't enabled, because it absolutely must happen // on all devices housekeeping(); @@ -212,6 +230,8 @@ public class NBSyncService extends Service { syncMetadata(startId); + checkRecounts(); + unreadsService.start(startId); imagePrefetchService.start(startId); @@ -296,7 +316,11 @@ public class NBSyncService extends Service { // if we attempted a call and it failed, do not mark the action as done if (response != null) { if (response.isError()) { - continue actionsloop; + if (response.isUserError()) { + Log.d(this.getClass().getName(), "Discarding reading action with user error."); + } else { + continue actionsloop; + } } } @@ -368,6 +392,7 @@ public class NBSyncService extends Service { FeedPagesSeen.clear(); FeedStoriesSeen.clear(); UnreadsService.clearHashes(); + RecountCandidates.clear(); FeedFolderResponse feedResponse = apiManager.getFolderFeedMapping(true); @@ -382,6 +407,7 @@ public class NBSyncService extends Service { return; } + lastFFReadMillis = feedResponse.readTime; long startTime = System.currentTimeMillis(); isPremium = feedResponse.isPremium; @@ -454,6 +480,74 @@ public class NBSyncService extends Service { } + /** + * See if any feeds have been touched in a way that require us to double-check unread counts; + */ + private void checkRecounts() { + if (!FlushRecounts) return; + + try { + if (RecountCandidates.size() < 1) return; + + RecountsRunning = true; + NbActivity.updateAllActivities(false); + + // of all candidate feeds that were touched, now check to see if + // any of them have mismatched local and remote counts we need to reconcile + Set dirtySets = new HashSet(); + for (FeedSet fs : RecountCandidates) { + if (dbHelper.getUnreadCount(fs, StateFilter.SOME) != dbHelper.getLocalUnreadCount(fs, StateFilter.SOME)) { + dirtySets.add(fs); + } + } + if (dirtySets.size() < 1) { + RecountCandidates.clear(); + return; + } + + // if we are offline, the best we can do is perform a local unread recount and + // save the true one for when we go back online. + if (!NetworkUtils.isOnline(this)) { + for (FeedSet fs : RecountCandidates) { + dbHelper.updateLocalFeedCounts(fs); + } + } else { + if (stopSync()) return; + Set apiIds = new HashSet(); + for (FeedSet fs : RecountCandidates) { + apiIds.addAll(fs.getFlatFeedIds()); + } + + // if any reading activities are pending, it makes no sense to recount yet + if (dbHelper.getActions(false).getCount() > 0) return; + + UnreadCountResponse apiResponse = apiManager.getFeedUnreadCounts(apiIds); + if ((apiResponse == null) || (apiResponse.isError())) { + Log.w(this.getClass().getName(), "Bad response to feed_unread_count"); + return; + } + if (apiResponse.feeds != null ) { + for (Map.Entry entry : apiResponse.feeds.entrySet()) { + dbHelper.updateFeedCounts(entry.getKey(), entry.getValue().getValues()); + } + } + if (apiResponse.socialFeeds != null ) { + for (Map.Entry entry : apiResponse.socialFeeds.entrySet()) { + String feedId = entry.getKey().replaceAll(APIConstants.VALUE_PREFIX_SOCIAL, ""); + dbHelper.updateSocialFeedCounts(feedId, entry.getValue().getValues()); + } + } + RecountCandidates.clear(); + } + } finally { + if (RecountsRunning) { + RecountsRunning = false; + NbActivity.updateAllActivities(true); + } + FlushRecounts = false; + } + } + /** * Fetch stories needed because the user is actively viewing a feed or folder. */ @@ -572,7 +666,10 @@ public class NBSyncService extends Service { return true; } if (context == null) return false; - if (!NetworkUtils.isOnline(context)) return true; + if (!NetworkUtils.isOnline(context)) { + OfflineNow = true; + return true; + } return false; } @@ -596,7 +693,7 @@ public class NBSyncService extends Service { * Is the main feed/folder list sync running? */ public static boolean isFeedFolderSyncRunning() { - return (HousekeepingRunning || ActionsRunning || FFSyncRunning || CleanupRunning || UnreadsService.running() || StorySyncRunning || OriginalTextService.running() || ImagePrefetchService.running()); + return (HousekeepingRunning || ActionsRunning || RecountsRunning || FFSyncRunning || CleanupRunning || UnreadsService.running() || StorySyncRunning || OriginalTextService.running() || ImagePrefetchService.running()); } /** @@ -608,13 +705,14 @@ public class NBSyncService extends Service { public static String getSyncStatusMessage(Context context) { if (HousekeepingRunning) return context.getResources().getString(R.string.sync_status_housekeeping); - if (ActionsRunning) return context.getResources().getString(R.string.sync_status_actions); + if (ActionsRunning||RecountsRunning) return context.getResources().getString(R.string.sync_status_actions); if (FFSyncRunning) return context.getResources().getString(R.string.sync_status_ffsync); if (CleanupRunning) return context.getResources().getString(R.string.sync_status_cleanup); if (StorySyncRunning) return context.getResources().getString(R.string.sync_status_stories); if (UnreadsService.running()) return String.format(context.getResources().getString(R.string.sync_status_unreads), UnreadsService.getPendingCount()); if (OriginalTextService.running()) return String.format(context.getResources().getString(R.string.sync_status_text), OriginalTextService.getPendingCount()); if (ImagePrefetchService.running()) return String.format(context.getResources().getString(R.string.sync_status_images), ImagePrefetchService.getPendingCount()); + if (OfflineNow) return context.getResources().getString(R.string.sync_status_offline); return null; } @@ -626,6 +724,10 @@ public class NBSyncService extends Service { DoFeedsFolders = true; } + public static void flushRecounts() { + FlushRecounts = true; + } + /** * Tell the service which stories can be activated if received. See ActivationMode. */ @@ -695,6 +797,16 @@ public class NBSyncService extends Service { OriginalTextService.addHash(hash); } + public static void addRecountCandidates(FeedSet fs) { + if (fs != null) { + RecountCandidates.add(fs); + } + } + + public static void addRecountCandidates(Set fs) { + RecountCandidates.addAll(fs); + } + public static void softInterrupt() { if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "soft stop"); HaltNow = true; @@ -709,17 +821,19 @@ public class NBSyncService extends Service { try { if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - stopping execution"); HaltNow = true; - unreadsService.shutdown(); - originalTextService.shutdown(); - imagePrefetchService.shutdown(); - primaryExecutor.shutdown(); - try { - primaryExecutor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS); - } catch (InterruptedException e) { - primaryExecutor.shutdownNow(); - Thread.currentThread().interrupt(); + if (unreadsService != null) unreadsService.shutdown(); + if (originalTextService != null) originalTextService.shutdown(); + if (imagePrefetchService != null) imagePrefetchService.shutdown(); + if (primaryExecutor != null) { + primaryExecutor.shutdown(); + try { + primaryExecutor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + primaryExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } } - dbHelper.close(); + if (dbHelper != null) dbHelper.close(); if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - execution halted"); super.onDestroy(); } catch (Exception ex) { @@ -738,7 +852,7 @@ public class NBSyncService extends Service { public static String getSpeedInfo() { StringBuilder s = new StringBuilder(); - s.append(lastFeedCount).append(" in ").append(lastFFWriteMillis); + s.append(lastFeedCount).append(" in ").append(lastFFReadMillis).append(" and ").append(lastFFWriteMillis); return s.toString(); } diff --git a/clients/android/NewsBlur/src/com/newsblur/service/NetStateReceiver.java b/clients/android/NewsBlur/src/com/newsblur/service/NetStateReceiver.java new file mode 100644 index 000000000..2619d0953 --- /dev/null +++ b/clients/android/NewsBlur/src/com/newsblur/service/NetStateReceiver.java @@ -0,0 +1,17 @@ +package com.newsblur.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class NetStateReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // poke the sync service when network state changes, in case we were offline + if (!NBSyncService.OfflineNow) return; + Intent i = new Intent(context, NBSyncService.class); + context.startService(i); + } + +} diff --git a/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java b/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java index a345bd448..74b6aa51f 100644 --- a/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java +++ b/clients/android/NewsBlur/src/com/newsblur/util/AppConstants.java @@ -5,7 +5,7 @@ public class AppConstants { // Enables high-volume logging that may be useful for debugging. This should // never be enabled for releases, as it not only slows down the app considerably, // it will log sensitive info such as passwords! - public static final boolean VERBOSE_LOG = false; + public static final boolean VERBOSE_LOG = true; public static final boolean VERBOSE_LOG_DB = false; public static final boolean VERBOSE_LOG_NET = false; diff --git a/clients/android/NewsBlur/src/com/newsblur/util/FeedSet.java b/clients/android/NewsBlur/src/com/newsblur/util/FeedSet.java index ed53c29ae..68f59e70d 100644 --- a/clients/android/NewsBlur/src/com/newsblur/util/FeedSet.java +++ b/clients/android/NewsBlur/src/com/newsblur/util/FeedSet.java @@ -10,6 +10,8 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; +import com.newsblur.network.APIConstants; + /** * A subset of one, several, or all NewsBlur feeds or social feeds. Used to encapsulate the * complexity of the fact that social feeds are special and requesting a river of feeds is not @@ -195,6 +197,26 @@ public class FeedSet implements Serializable { return this.folderName; } + /** + * Gets a flat set of feed IDs that can be passed to API calls that take raw numeric IDs or + * social IDs prefixed with "social:". Returns an empty set for feed sets that don't track + * unread counts or that are essentially "everything". + */ + public Set getFlatFeedIds() { + Set result = new HashSet(); + if (feeds != null) { + for (String id : feeds) { + result.add(id); + } + } + if (socialFeeds != null) { + for (Map.Entry e : socialFeeds.entrySet()) { + result.add(APIConstants.VALUE_PREFIX_SOCIAL + e.getKey()); + } + } + return result; + } + private static final String COM_SER_NUL = "NUL"; public String toCompactSerial() { diff --git a/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java b/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java index 57472879c..d9f960343 100644 --- a/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java +++ b/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java @@ -133,16 +133,20 @@ public class FeedUtils { story.read = read; // update unread state and unread counts in the local DB - dbHelper.setStoryReadState(story, read); + Set impactedFeeds = dbHelper.setStoryReadState(story, read); NbActivity.updateAllActivities(); // tell the sync service we need to mark read ReadingAction ra = (read ? ReadingAction.markStoryRead(story.storyHash) : ReadingAction.markStoryUnread(story.storyHash)); dbHelper.enqueueAction(ra); triggerSync(context); + NBSyncService.addRecountCandidates(impactedFeeds); } public static void markFeedsRead(final FeedSet fs, final Long olderThan, final Long newerThan, final Context context) { + dbHelper.markStoriesRead(fs, olderThan, newerThan); + dbHelper.updateLocalFeedCounts(fs); + NbActivity.updateAllActivities(); new AsyncTask() { @Override protected Void doInBackground(Void... arg) { @@ -153,8 +157,6 @@ public class FeedUtils { FeedSet newFeedSet = FeedSet.folder("all", dbHelper.getAllFeeds()); ra = ReadingAction.markFeedRead(newFeedSet, olderThan, newerThan); } - dbHelper.markStoriesRead(fs, olderThan, newerThan); - NbActivity.updateAllActivities(); dbHelper.enqueueAction(ra); triggerSync(context); return null; diff --git a/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java b/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java index a5a1dab3e..868a7386f 100644 --- a/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java +++ b/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java @@ -91,6 +91,7 @@ public class PrefsUtils { } else { s.append("unknown"); } + s.append("%0Aprefetch: ").append(isOfflineEnabled(context) ? "yes" : "no"); return s.toString(); }