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