Merge pull request #662 from dosiecki/master

Android: Bugfixes and Speed
This commit is contained in:
Samuel Clay 2015-02-17 12:35:32 -08:00
commit 9b51f50bb4
32 changed files with 457 additions and 178 deletions

View file

@ -2,11 +2,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.newsblur" package="com.newsblur"
android:versionCode="85" android:versionCode="85"
android:versionName="4.2.0b1" > android:versionName="4.2.0b3" >
<uses-sdk <uses-sdk
android:minSdkVersion="11" android:minSdkVersion="11"
android:targetSdkVersion="19" /> android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -55,8 +55,9 @@
<activity <activity
android:name=".activity.Main" android:name=".activity.Main"
android:label="@string/newsblur" /> android:label="@string/newsblur"
android:launchMode="singleTask"
android:alwaysRetainTaskState="true" />
<activity <activity
android:name=".activity.Profile" android:name=".activity.Profile"
@ -126,6 +127,12 @@
<receiver android:name=".service.ServiceScheduleReceiver" /> <receiver android:name=".service.ServiceScheduleReceiver" />
<receiver android:name=".service.NetStateReceiver">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

View file

@ -11,4 +11,4 @@
proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target. # Project target.
target=android-19 target=android-21

View file

@ -2,7 +2,7 @@
<set xmlns:android="http://schemas.android.com/apk/res/android" <set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false" > android:shareInterpolator="false" >
<translate <translate
android:duration="200" android:duration="300"
android:fromXDelta="-100%" android:fromXDelta="-100%"
android:toXDelta="0%" /> android:toXDelta="0%" />
</set> </set>

View file

@ -2,7 +2,7 @@
<set xmlns:android="http://schemas.android.com/apk/res/android" <set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false" > android:shareInterpolator="false" >
<translate <translate
android:duration="200" android:duration="300"
android:fromXDelta="0%" android:fromXDelta="0%"
android:toXDelta="100%" /> android:toXDelta="100%" />
</set> </set>

View file

@ -15,7 +15,7 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:padding="3dp" android:padding="2dp"
android:textSize="14sp" android:textSize="14sp"
android:gravity="center" android:gravity="center"
android:textColor="@color/status_overlay_text" android:textColor="@color/status_overlay_text"

View file

@ -39,7 +39,7 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_above="@+id/fragment_feedintelligenceselector" android:layout_above="@+id/fragment_feedintelligenceselector"
android:padding="3dp" android:padding="2dp"
android:textSize="14sp" android:textSize="14sp"
android:gravity="center" android:gravity="center"
android:textColor="@color/status_overlay_text" android:textColor="@color/status_overlay_text"

View file

@ -104,7 +104,7 @@
<color name="progress_circle_complete">#8F918B</color> <color name="progress_circle_complete">#8F918B</color>
<color name="progress_circle_remaining">#D5D7CF</color> <color name="progress_circle_remaining">#D5D7CF</color>
<color name="status_overlay_text">#DD111111</color> <color name="status_overlay_text">#DDFFFFFF</color>
<color name="status_overlay_background">#AA777777</color> <color name="status_overlay_background">#AA777777</color>
<color name="story_buttons_dark">#90928b</color> <color name="story_buttons_dark">#90928b</color>

View file

@ -110,26 +110,14 @@
<string name="menu_mark_story_as_read">Mark as read</string> <string name="menu_mark_story_as_read">Mark as read</string>
<string name="menu_mark_unread">Mark as unread</string> <string name="menu_mark_unread">Mark as unread</string>
<string name="menu_fullscreen">Full screen</string> <string name="menu_fullscreen">Full screen</string>
<string name="toast_error_loading_stories">Error loading stories</string>
<string name="toast_marked_stories_as_read">Stories marked as read</string> <string name="toast_marked_stories_as_read">Stories marked as read</string>
<string name="toast_error_marking_feed_as_read">Error marking feed as read. Check your internet connection.</string>
<string name="toast_story_saved">Story saved</string>
<string name="toast_story_save_error">Error marking story as saved.</string>
<string name="toast_story_unsaved">Story unsaved</string>
<string name="toast_story_unsave_error">Error marking story as unsaved.</string>
<string name="toast_story_unread">Story marked as unread</string> <string name="toast_story_unread">Story marked as unread</string>
<string name="toast_story_unread_error">Error marking story as unread</string>
<string name="toast_story_read_error">Error marking story as unread</string>
<string name="toast_unread_search_error">Could not load next unread story</string> <string name="toast_unread_search_error">Could not load next unread story</string>
<string name="toast_feed_deleted">Feed deleted</string> <string name="toast_feed_deleted">Feed deleted</string>
<string name="toast_feed_delete_error">There was an error deleting the feed.</string>
<string name="logout_warning">Are you sure you want to log out?</string> <string name="logout_warning">Are you sure you want to log out?</string>
@ -148,7 +136,6 @@
<string name="empty_list_view_no_stories">No stories to read</string> <string name="empty_list_view_no_stories">No stories to read</string>
<string name="login_registration_register">Register</string> <string name="login_registration_register">Register</string>
<string name="add_sites">Add some sites</string>
<string name="get_started">Let\'s get started</string> <string name="get_started">Let\'s get started</string>
<string name="add_friends">Add your friends</string> <string name="add_friends">Add your friends</string>
<string name="connect_with_your_friends">Connect with your friends to easily follow the stories that matter to them</string> <string name="connect_with_your_friends">Connect with your friends to easily follow the stories that matter to them</string>
@ -159,11 +146,6 @@
<string name="add_facebook">Add Facebook friends</string> <string name="add_facebook">Add Facebook friends</string>
<string name="need_to_login"><u>I need to log in!</u></string> <string name="need_to_login"><u>I need to log in!</u></string>
<string name="need_to_register"><u>I need to register</u></string> <string name="need_to_register"><u>I need to register</u></string>
<string name="wonderful_things">Wonderful things are happening at NewsBlur. Add our blog for the latest news.</string>
<string name="addfollow_add_newsblur">Follow the NewsBlur blog</string>
<string name="addfollow_add_popular">Follow Popular Stories</string>
<string name="add_follow">All Done!</string>
<string name="share_with_comments">Share with comments</string>
<string name="share_this_story">Share this story</string> <string name="share_this_story">Share this story</string>
<string name="comment_favourited">Comment favorited</string> <string name="comment_favourited">Comment favorited</string>
<string name="error_liking_comment">Error favoriting comment</string> <string name="error_liking_comment">Error favoriting comment</string>
@ -260,6 +242,7 @@
<string name="sync_status_unreads">Storing%sunread stories...</string> <string name="sync_status_unreads">Storing%sunread stories...</string>
<string name="sync_status_text">Storing text for %s stories...</string> <string name="sync_status_text">Storing text for %s stories...</string>
<string name="sync_status_images">Storing %s images...</string> <string name="sync_status_images">Storing %s images...</string>
<string name="sync_status_offline">Offline</string>
<string name="volume_key_navigation">Volume Key Navigation</string> <string name="volume_key_navigation">Volume Key Navigation</string>
<string name="off">Off</string> <string name="off">Off</string>

View file

@ -1,21 +1,12 @@
package com.newsblur.activity; 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.os.Bundle;
import android.app.FragmentTransaction; import android.app.FragmentTransaction;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.widget.Toast;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.fragment.AllStoriesItemListFragment; import com.newsblur.fragment.AllStoriesItemListFragment;
import com.newsblur.fragment.FeedItemListFragment; import com.newsblur.fragment.FeedItemListFragment;
import com.newsblur.fragment.MarkAllReadDialogFragment; import com.newsblur.fragment.MarkAllReadDialogFragment;

View file

@ -1,6 +0,0 @@
package com.newsblur.activity;
public class FeedSearch extends NbActivity {
}

View file

@ -15,7 +15,6 @@ import android.widget.Toast;
import android.util.Log; import android.util.Log;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.fragment.FolderItemListFragment; import com.newsblur.fragment.FolderItemListFragment;
import com.newsblur.fragment.MarkAllReadDialogFragment; import com.newsblur.fragment.MarkAllReadDialogFragment;
import com.newsblur.fragment.MarkAllReadDialogFragment.MarkAllReadDialogListener; import com.newsblur.fragment.MarkAllReadDialogFragment.MarkAllReadDialogListener;

View file

@ -1,7 +1,6 @@
package com.newsblur.activity; package com.newsblur.activity;
import android.app.FragmentTransaction; import android.app.FragmentTransaction;
import android.content.ContentResolver;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -17,16 +16,12 @@ import com.newsblur.util.StoryOrder;
public class GlobalSharedStoriesItemsList extends ItemsList { public class GlobalSharedStoriesItemsList extends ItemsList {
private ContentResolver resolver;
@Override @Override
protected void onCreate(Bundle bundle) { protected void onCreate(Bundle bundle) {
super.onCreate(bundle); super.onCreate(bundle);
setTitle(getResources().getString(R.string.global_shared_stories)); setTitle(getResources().getString(R.string.global_shared_stories));
resolver = getContentResolver();
itemListFragment = (GlobalSharedStoriesItemListFragment) fragmentManager.findFragmentByTag(GlobalSharedStoriesItemListFragment.class.getName()); itemListFragment = (GlobalSharedStoriesItemListFragment) fragmentManager.findFragmentByTag(GlobalSharedStoriesItemListFragment.class.getName());
if (itemListFragment == null) { if (itemListFragment == null) {
itemListFragment = GlobalSharedStoriesItemListFragment.newInstance(getDefaultFeedView(), currentState); itemListFragment = GlobalSharedStoriesItemListFragment.newInstance(getDefaultFeedView(), currentState);

View file

@ -81,6 +81,12 @@ public abstract class ItemsList extends NbActivity implements StateChangedListen
itemListFragment.hasUpdated(); itemListFragment.hasUpdated();
} }
@Override
protected void onPause() {
super.onPause();
NBSyncService.addRecountCandidates(fs);
}
public void markItemListAsRead() { public void markItemListAsRead() {
FeedUtils.markFeedsRead(fs, null, null, this); FeedUtils.markFeedsRead(fs, null, null, this);
Toast.makeText(this, R.string.toast_marked_stories_as_read, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.toast_marked_stories_as_read, Toast.LENGTH_SHORT).show();

View file

@ -74,6 +74,7 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
super.onResume(); super.onResume();
NBSyncService.clearPendingStoryRequest(); NBSyncService.clearPendingStoryRequest();
NBSyncService.flushRecounts();
NBSyncService.setActivationMode(NBSyncService.ActivationMode.ALL); NBSyncService.setActivationMode(NBSyncService.ActivationMode.ALL);
FeedUtils.activateAllStories(); FeedUtils.activateAllStories();
FeedUtils.clearReadingSession(); FeedUtils.clearReadingSession();
@ -155,12 +156,6 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
folderFeedList.changeState(state); folderFeedList.changeState(state);
} }
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
folderFeedList.hasUpdated();
}
}
@Override @Override
public void handleUpdate(boolean freshData) { public void handleUpdate(boolean freshData) {
updateStatusIndicators(); updateStatusIndicators();

View file

@ -99,6 +99,8 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
private float overlayRangeTopPx; private float overlayRangeTopPx;
private float overlayRangeBotPx; private float overlayRangeBotPx;
private int lastVScrollPos = 0;
private List<Story> pageHistory; private List<Story> pageHistory;
protected DefaultFeedView defaultFeedView; protected DefaultFeedView defaultFeedView;
@ -377,9 +379,10 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
@Override @Override
public void scrollChanged(int hPos, int vPos, int currentWidth, int currentHeight) { 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 // 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 scrollMax = currentHeight - contentView.getMeasuredHeight();
int posFromBot = (scrollMax - vPos); int posFromBot = (scrollMax - vPos);
@ -710,6 +713,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
} }
public void overlaySend(View v) { public void overlaySend(View v) {
if ((readingAdapter == null) || (pager == null)) return;
Story story = readingAdapter.getStory(pager.getCurrentItem()); Story story = readingAdapter.getStory(pager.getCurrentItem());
FeedUtils.shareStory(story, this); FeedUtils.shareStory(story, this);
} }

View file

@ -1,12 +1,5 @@
package com.newsblur.activity; 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.os.Bundle;
import android.app.FragmentTransaction; import android.app.FragmentTransaction;
import android.view.Menu; import android.view.Menu;
@ -14,7 +7,6 @@ import android.view.MenuInflater;
import android.widget.Toast; import android.widget.Toast;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.fragment.SavedStoriesItemListFragment; import com.newsblur.fragment.SavedStoriesItemListFragment;
import com.newsblur.fragment.FeedItemListFragment; import com.newsblur.fragment.FeedItemListFragment;
import com.newsblur.util.DefaultFeedView; import com.newsblur.util.DefaultFeedView;
@ -27,16 +19,12 @@ import com.newsblur.util.StoryOrder;
public class SavedStoriesItemsList extends ItemsList { public class SavedStoriesItemsList extends ItemsList {
private ContentResolver resolver;
@Override @Override
protected void onCreate(Bundle bundle) { protected void onCreate(Bundle bundle) {
super.onCreate(bundle); super.onCreate(bundle);
setTitle(getResources().getString(R.string.saved_stories_title)); setTitle(getResources().getString(R.string.saved_stories_title));
resolver = getContentResolver();
itemListFragment = (SavedStoriesItemListFragment) fragmentManager.findFragmentByTag(SavedStoriesItemListFragment.class.getName()); itemListFragment = (SavedStoriesItemListFragment) fragmentManager.findFragmentByTag(SavedStoriesItemListFragment.class.getName());
if (itemListFragment == null) { if (itemListFragment == null) {
itemListFragment = SavedStoriesItemListFragment.newInstance(getDefaultFeedView()); itemListFragment = SavedStoriesItemListFragment.newInstance(getDefaultFeedView());

View file

@ -355,33 +355,25 @@ public class BlurDatabaseHelper {
} }
} }
/**
* Marks a story (un)read but does not adjust counts.
*/
public void setStoryReadState(String hash, boolean read) { public void setStoryReadState(String hash, boolean read) {
Cursor c = getStory(hash); ContentValues values = new ContentValues();
if (c.getCount() < 1) { values.put(DatabaseConstants.STORY_READ, read);
Log.w(this.getClass().getName(), "story removed before finishing mark-read"); values.put(DatabaseConstants.STORY_READ_THIS_SESSION, read);
return; synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});}
}
Story story = Story.fromCursor(c);
if (story == null) {
Log.w(this.getClass().getName(), "story removed before finishing mark-read");
return;
}
setStoryReadState(story, read);
} }
/** /**
* Marks a story (un)read and also adjusts unread counts for it. * 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) { public Set<FeedSet> setStoryReadState(Story story, boolean read) {
// read flag // calculate the impact surface so the caller can re-check counts if needed
ContentValues values = new ContentValues(); Set<FeedSet> impactedFeeds = new HashSet<FeedSet>();
values.put(DatabaseConstants.STORY_READ, read); impactedFeeds.add(FeedSet.singleFeed(story.feedId));
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
Set<String> socialIds = new HashSet<String>(); Set<String> socialIds = new HashSet<String>();
if (!TextUtils.isEmpty(story.socialUserId)) { if (!TextUtils.isEmpty(story.socialUserId)) {
socialIds.add(story.socialUserId); socialIds.add(story.socialUserId);
@ -392,10 +384,73 @@ public class BlurDatabaseHelper {
} }
} }
if (socialIds.size() > 0) { 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) { public void markStoriesRead(FeedSet fs, Long olderThan, Long newerThan) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.STORY_READ, true); 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."); 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);} 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. * 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 // decompose the FeedSet into a list of single feeds that need to be recounted
List<String> feedIds = new ArrayList<String>(); List<String> feedIds = new ArrayList<String>();
List<String> socialFeedIds = new ArrayList<String>(); List<String> socialFeedIds = new ArrayList<String>();
@ -450,23 +564,26 @@ public class BlurDatabaseHelper {
for (String feedId : feedIds) { for (String feedId : feedIds) {
FeedSet singleFs = FeedSet.singleFeed(feedId); FeedSet singleFs = FeedSet.singleFeed(feedId);
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.FEED_NEGATIVE_COUNT, getUnreadCount(singleFs, StateFilter.NEG)); values.put(DatabaseConstants.FEED_NEGATIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEG));
values.put(DatabaseConstants.FEED_NEUTRAL_COUNT, getUnreadCount(singleFs, StateFilter.NEUT)); values.put(DatabaseConstants.FEED_NEUTRAL_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEUT));
values.put(DatabaseConstants.FEED_POSITIVE_COUNT, getUnreadCount(singleFs, StateFilter.BEST)); 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});} synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});}
} }
for (String socialId : socialFeedIds) { for (String socialId : socialFeedIds) {
FeedSet singleFs = FeedSet.singleSocialFeed(socialId, ""); FeedSet singleFs = FeedSet.singleSocialFeed(socialId, "");
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.SOCIAL_FEED_NEGATIVE_COUNT, getUnreadCount(singleFs, StateFilter.NEG)); values.put(DatabaseConstants.SOCIAL_FEED_NEGATIVE_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEG));
values.put(DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT, getUnreadCount(singleFs, StateFilter.NEUT)); values.put(DatabaseConstants.SOCIAL_FEED_NEUTRAL_COUNT, getLocalUnreadCount(singleFs, StateFilter.NEUT));
values.put(DatabaseConstants.SOCIAL_FEED_POSITIVE_COUNT, getUnreadCount(singleFs, StateFilter.BEST)); 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});} 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); Cursor c = getStoriesCursor(fs, stateFilter, ReadFilter.PURE_UNREAD, null, null);
int count = c.getCount(); int count = c.getCount();
c.close(); c.close();
@ -486,12 +603,6 @@ public class BlurDatabaseHelper {
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.ACTION_TABLE, DatabaseConstants.ACTION_ID + " = ?", new String[]{actionId});} 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) { public void setStoryStarred(String hash, boolean starred) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.STORY_STARRED, starred); values.put(DatabaseConstants.STORY_STARRED, starred);

View file

@ -81,6 +81,8 @@ public class Story implements Serializable {
@SerializedName("intelligence") @SerializedName("intelligence")
public Intelligence intelligence = new Intelligence(); public Intelligence intelligence = new Intelligence();
public int intelTotal;
@SerializedName("short_parsed_date") @SerializedName("short_parsed_date")
public String shortDate; 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.intelligenceFeed = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_INTELLIGENCE_FEED));
story.intelligence.intelligenceTags = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_INTELLIGENCE_TAGS)); story.intelligence.intelligenceTags = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_INTELLIGENCE_TAGS));
story.intelligence.intelligenceTitle = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_INTELLIGENCE_TITLE)); 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.read = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_READ)) > 0;
story.starred = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_STARRED)) > 0; story.starred = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.STORY_STARRED)) > 0;
story.starredTimestamp = cursor.getLong(cursor.getColumnIndex(DatabaseConstants.STORY_STARRED_DATE)); story.starredTimestamp = cursor.getLong(cursor.getColumnIndex(DatabaseConstants.STORY_STARRED_DATE));

View file

@ -19,6 +19,9 @@ public class LogoutDialogFragment extends DialogFragment {
@Override @Override
public void onClick(DialogInterface dialogInterface, int i) { public void onClick(DialogInterface dialogInterface, int i) {
PrefsUtils.logout(getActivity()); 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() { builder.setNegativeButton(R.string.alert_dialog_cancel, new DialogInterface.OnClickListener() {

View file

@ -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_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_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_SOCIALFEED_STORIES = NEWSBLUR_URL + "/social/stories";
public static final String URL_SIGNUP = NEWSBLUR_URL + "/api/signup"; 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/"; public static final String URL_MARK_FEED_AS_READ = NEWSBLUR_URL + "/reader/mark_feed_as_read/";

View file

@ -11,8 +11,8 @@ import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import android.content.ContentResolver;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; 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.RegisterResponse;
import com.newsblur.network.domain.StoriesResponse; import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.network.domain.StoryTextResponse; import com.newsblur.network.domain.StoryTextResponse;
import com.newsblur.network.domain.UnreadCountResponse;
import com.newsblur.network.domain.UnreadStoryHashesResponse; import com.newsblur.network.domain.UnreadStoryHashesResponse;
import com.newsblur.serialization.BooleanTypeAdapter; import com.newsblur.serialization.BooleanTypeAdapter;
import com.newsblur.serialization.ClassifierMapTypeAdapter; import com.newsblur.serialization.ClassifierMapTypeAdapter;
@ -58,12 +59,10 @@ public class APIManager {
private Context context; private Context context;
private Gson gson; private Gson gson;
private ContentResolver contentResolver;
private String customUserAgent; private String customUserAgent;
public APIManager(final Context context) { public APIManager(final Context context) {
this.context = context; this.context = context;
this.contentResolver = context.getContentResolver();
this.gson = new GsonBuilder() this.gson = new GsonBuilder()
.registerTypeAdapter(Date.class, new DateStringTypeAdapter()) .registerTypeAdapter(Date.class, new DateStringTypeAdapter())
@ -246,6 +245,15 @@ public class APIManager {
} }
} }
public UnreadCountResponse getFeedUnreadCounts(Set<String> 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() { public UnreadStoryHashesResponse getUnreadStoryHashes() {
ValueMultimap values = new ValueMultimap(); ValueMultimap values = new ValueMultimap();
values.put(APIConstants.PARAMETER_INCLUDE_TIMESTAMPS, "1"); 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 // 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) { public NewsBlurResponse trainClassifier(String feedId, String key, int type, int action) {

View file

@ -1,9 +1,10 @@
package com.newsblur.network; package com.newsblur.network;
import java.io.InputStreamReader;
import java.io.IOException; import java.io.IOException;
import java.io.Reader;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.Scanner;
import android.content.Context; import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
@ -28,6 +29,7 @@ public class APIResponse {
private String errorMessage; private String errorMessage;
private String cookie; private String cookie;
private String responseBody; private String responseBody;
public long readTime;
/** /**
* Construct an online response. Will test the response for errors and extract all the * Construct an online response. Will test the response for errors and extract all the
@ -71,8 +73,14 @@ public class APIResponse {
try { try {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
Scanner scanner = new Scanner(connection.getInputStream(), "UTF-8"); Reader reader = new InputStreamReader(connection.getInputStream());
while (scanner.hasNextLine()) { builder.append(scanner.nextLine()); } 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(); this.responseBody = builder.toString();
} catch (Exception e) { } catch (Exception e) {
Log.e(this.getClass().getName(), e.getClass().getName() + " (" + e.getMessage() + ") reading " + originalUrl, e); Log.e(this.getClass().getName(), e.getClass().getName() + " (" + e.getMessage() + ") reading " + originalUrl, e);
@ -81,16 +89,23 @@ public class APIResponse {
return; return;
} }
if (AppConstants.VERBOSE_LOG_NET) {
Log.d(this.getClass().getName(), "received API response: \n" + this.responseBody);
}
try { try {
connection.disconnect(); connection.disconnect();
} catch (Exception e) { } catch (Exception e) {
Log.e(this.getClass().getName(), e.getClass().getName() + " caught closing connection: " + e.getMessage(), 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 { } else {
// otherwise, parse the response as the expected class and defer error detection // otherwise, parse the response as the expected class and defer error detection
// to the NewsBlurResponse parent class // to the NewsBlurResponse parent class
return gson.fromJson(this.responseBody, classOfT); T response = gson.fromJson(this.responseBody, classOfT);
response.readTime = readTime;
return response;
} }
} }

View file

@ -21,6 +21,8 @@ import com.newsblur.domain.SocialFeed;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
public class FeedFolderResponse { public class FeedFolderResponse {
public long readTime;
@SerializedName("starred_count") @SerializedName("starred_count")
public int starredCount; public int starredCount;

View file

@ -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<String, Count> feedCounts;
@SerializedName("social_feeds")
public Map<String, Count> 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;
}
}
}

View file

@ -2,6 +2,9 @@ package com.newsblur.network.domain;
import android.util.Log; 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. * A generic response to an API call that only encapsuates success versus failure.
*/ */
@ -11,6 +14,9 @@ public class NewsBlurResponse {
public int code; public int code;
public String message; public String message;
public ResponseErrors errors; public ResponseErrors errors;
public long readTime;
public static final Pattern KnownUserErrors = Pattern.compile("cannot mark as unread");
public boolean isError() { public boolean isError() {
if ((message != null) && (!message.equals(""))) { if ((message != null) && (!message.equals(""))) {
@ -24,6 +30,20 @@ public class NewsBlurResponse {
return false; 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. * Gets the error message returned by the API, or defaultMessage if none was found.
*/ */

View file

@ -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<String,UnreadMD> feeds;
@SerializedName("social_feeds")
public Map<String,UnreadMD> 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;
}
}
}

View file

@ -20,10 +20,12 @@ import static com.newsblur.database.BlurDatabaseHelper.closeQuietly;
import com.newsblur.database.DatabaseConstants; import com.newsblur.database.DatabaseConstants;
import com.newsblur.domain.SocialFeed; import com.newsblur.domain.SocialFeed;
import com.newsblur.domain.Story; import com.newsblur.domain.Story;
import com.newsblur.network.APIConstants;
import com.newsblur.network.APIManager; import com.newsblur.network.APIManager;
import com.newsblur.network.domain.FeedFolderResponse; import com.newsblur.network.domain.FeedFolderResponse;
import com.newsblur.network.domain.NewsBlurResponse; import com.newsblur.network.domain.NewsBlurResponse;
import com.newsblur.network.domain.StoriesResponse; import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.network.domain.UnreadCountResponse;
import com.newsblur.network.domain.UnreadStoryHashesResponse; import com.newsblur.network.domain.UnreadStoryHashesResponse;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedSet; import com.newsblur.util.FeedSet;
@ -32,6 +34,7 @@ import com.newsblur.util.NetworkUtils;
import com.newsblur.util.PrefsUtils; import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadingAction; import com.newsblur.util.ReadingAction;
import com.newsblur.util.ReadFilter; import com.newsblur.util.ReadFilter;
import com.newsblur.util.StateFilter;
import com.newsblur.util.StoryOrder; import com.newsblur.util.StoryOrder;
import java.util.ArrayList; import java.util.ArrayList;
@ -76,6 +79,7 @@ public class NBSyncService extends Service {
private volatile static boolean FFSyncRunning = false; private volatile static boolean FFSyncRunning = false;
private volatile static boolean StorySyncRunning = false; private volatile static boolean StorySyncRunning = false;
private volatile static boolean HousekeepingRunning = false; private volatile static boolean HousekeepingRunning = false;
private volatile static boolean RecountsRunning = false;
private volatile static boolean DoFeedsFolders = false; private volatile static boolean DoFeedsFolders = false;
private volatile static boolean DoUnreads = 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 ActivationMode ActMode = ActivationMode.ALL;
private volatile static long ModeCutoff = 0L; 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 isPremium = null;
public volatile static Boolean isStaff = null; public volatile static Boolean isStaff = null;
private volatile static boolean isMemoryLow = false; private volatile static boolean isMemoryLow = false;
private static long lastFeedCount = 0L; private static long lastFeedCount = 0L;
private static long lastFFReadMillis = 0L;
private static long lastFFWriteMillis = 0L; private static long lastFFWriteMillis = 0L;
/** Feed set that we need to sync immediately for the UI. */ /** Feed set that we need to sync immediately for the UI. */
@ -108,6 +116,11 @@ public class NBSyncService extends Service {
private static List<ReadingAction> FollowupActions; private static List<ReadingAction> FollowupActions;
static { FollowupActions = new ArrayList<ReadingAction>(); } static { FollowupActions = new ArrayList<ReadingAction>(); }
/** Feed IDs (API stype) that have been acted upon and need a double-check for counts. */
private static Set<FeedSet> RecountCandidates;
static { RecountCandidates = new HashSet<FeedSet>(); }
private volatile static boolean FlushRecounts = false;
Set<String> orphanFeedIds; Set<String> orphanFeedIds;
private ExecutorService primaryExecutor; private ExecutorService primaryExecutor;
@ -192,6 +205,11 @@ public class NBSyncService extends Service {
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE); 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 // do this even if background syncs aren't enabled, because it absolutely must happen
// on all devices // on all devices
housekeeping(); housekeeping();
@ -212,6 +230,8 @@ public class NBSyncService extends Service {
syncMetadata(startId); syncMetadata(startId);
checkRecounts();
unreadsService.start(startId); unreadsService.start(startId);
imagePrefetchService.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 we attempted a call and it failed, do not mark the action as done
if (response != null) { if (response != null) {
if (response.isError()) { 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(); FeedPagesSeen.clear();
FeedStoriesSeen.clear(); FeedStoriesSeen.clear();
UnreadsService.clearHashes(); UnreadsService.clearHashes();
RecountCandidates.clear();
FeedFolderResponse feedResponse = apiManager.getFolderFeedMapping(true); FeedFolderResponse feedResponse = apiManager.getFolderFeedMapping(true);
@ -382,6 +407,7 @@ public class NBSyncService extends Service {
return; return;
} }
lastFFReadMillis = feedResponse.readTime;
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
isPremium = feedResponse.isPremium; 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<FeedSet> dirtySets = new HashSet<FeedSet>();
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<String> apiIds = new HashSet<String>();
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<String,UnreadCountResponse.UnreadMD> entry : apiResponse.feeds.entrySet()) {
dbHelper.updateFeedCounts(entry.getKey(), entry.getValue().getValues());
}
}
if (apiResponse.socialFeeds != null ) {
for (Map.Entry<String,UnreadCountResponse.UnreadMD> 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. * Fetch stories needed because the user is actively viewing a feed or folder.
*/ */
@ -572,7 +666,10 @@ public class NBSyncService extends Service {
return true; return true;
} }
if (context == null) return false; if (context == null) return false;
if (!NetworkUtils.isOnline(context)) return true; if (!NetworkUtils.isOnline(context)) {
OfflineNow = true;
return true;
}
return false; return false;
} }
@ -596,7 +693,7 @@ public class NBSyncService extends Service {
* Is the main feed/folder list sync running? * Is the main feed/folder list sync running?
*/ */
public static boolean isFeedFolderSyncRunning() { 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) { public static String getSyncStatusMessage(Context context) {
if (HousekeepingRunning) return context.getResources().getString(R.string.sync_status_housekeeping); 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 (FFSyncRunning) return context.getResources().getString(R.string.sync_status_ffsync);
if (CleanupRunning) return context.getResources().getString(R.string.sync_status_cleanup); if (CleanupRunning) return context.getResources().getString(R.string.sync_status_cleanup);
if (StorySyncRunning) return context.getResources().getString(R.string.sync_status_stories); 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 (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 (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 (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; return null;
} }
@ -626,6 +724,10 @@ public class NBSyncService extends Service {
DoFeedsFolders = true; DoFeedsFolders = true;
} }
public static void flushRecounts() {
FlushRecounts = true;
}
/** /**
* Tell the service which stories can be activated if received. See ActivationMode. * Tell the service which stories can be activated if received. See ActivationMode.
*/ */
@ -695,6 +797,16 @@ public class NBSyncService extends Service {
OriginalTextService.addHash(hash); OriginalTextService.addHash(hash);
} }
public static void addRecountCandidates(FeedSet fs) {
if (fs != null) {
RecountCandidates.add(fs);
}
}
public static void addRecountCandidates(Set<FeedSet> fs) {
RecountCandidates.addAll(fs);
}
public static void softInterrupt() { public static void softInterrupt() {
if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "soft stop"); if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "soft stop");
HaltNow = true; HaltNow = true;
@ -709,17 +821,19 @@ public class NBSyncService extends Service {
try { try {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - stopping execution"); if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - stopping execution");
HaltNow = true; HaltNow = true;
unreadsService.shutdown(); if (unreadsService != null) unreadsService.shutdown();
originalTextService.shutdown(); if (originalTextService != null) originalTextService.shutdown();
imagePrefetchService.shutdown(); if (imagePrefetchService != null) imagePrefetchService.shutdown();
primaryExecutor.shutdown(); if (primaryExecutor != null) {
try { primaryExecutor.shutdown();
primaryExecutor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS); try {
} catch (InterruptedException e) { primaryExecutor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS);
primaryExecutor.shutdownNow(); } catch (InterruptedException e) {
Thread.currentThread().interrupt(); primaryExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
} }
dbHelper.close(); if (dbHelper != null) dbHelper.close();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - execution halted"); if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - execution halted");
super.onDestroy(); super.onDestroy();
} catch (Exception ex) { } catch (Exception ex) {
@ -738,7 +852,7 @@ public class NBSyncService extends Service {
public static String getSpeedInfo() { public static String getSpeedInfo() {
StringBuilder s = new StringBuilder(); 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(); return s.toString();
} }

View file

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

View file

@ -5,7 +5,7 @@ public class AppConstants {
// Enables high-volume logging that may be useful for debugging. This should // 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, // never be enabled for releases, as it not only slows down the app considerably,
// it will log sensitive info such as passwords! // 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_DB = false;
public static final boolean VERBOSE_LOG_NET = false; public static final boolean VERBOSE_LOG_NET = false;

View file

@ -10,6 +10,8 @@ import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set; 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 * 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 * 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; 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<String> getFlatFeedIds() {
Set<String> result = new HashSet<String>();
if (feeds != null) {
for (String id : feeds) {
result.add(id);
}
}
if (socialFeeds != null) {
for (Map.Entry<String,String> e : socialFeeds.entrySet()) {
result.add(APIConstants.VALUE_PREFIX_SOCIAL + e.getKey());
}
}
return result;
}
private static final String COM_SER_NUL = "NUL"; private static final String COM_SER_NUL = "NUL";
public String toCompactSerial() { public String toCompactSerial() {

View file

@ -133,16 +133,20 @@ public class FeedUtils {
story.read = read; story.read = read;
// update unread state and unread counts in the local DB // update unread state and unread counts in the local DB
dbHelper.setStoryReadState(story, read); Set<FeedSet> impactedFeeds = dbHelper.setStoryReadState(story, read);
NbActivity.updateAllActivities(); NbActivity.updateAllActivities();
// tell the sync service we need to mark read // tell the sync service we need to mark read
ReadingAction ra = (read ? ReadingAction.markStoryRead(story.storyHash) : ReadingAction.markStoryUnread(story.storyHash)); ReadingAction ra = (read ? ReadingAction.markStoryRead(story.storyHash) : ReadingAction.markStoryUnread(story.storyHash));
dbHelper.enqueueAction(ra); dbHelper.enqueueAction(ra);
triggerSync(context); triggerSync(context);
NBSyncService.addRecountCandidates(impactedFeeds);
} }
public static void markFeedsRead(final FeedSet fs, final Long olderThan, final Long newerThan, final Context context) { 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<Void, Void, Void>() { new AsyncTask<Void, Void, Void>() {
@Override @Override
protected Void doInBackground(Void... arg) { protected Void doInBackground(Void... arg) {
@ -153,8 +157,6 @@ public class FeedUtils {
FeedSet newFeedSet = FeedSet.folder("all", dbHelper.getAllFeeds()); FeedSet newFeedSet = FeedSet.folder("all", dbHelper.getAllFeeds());
ra = ReadingAction.markFeedRead(newFeedSet, olderThan, newerThan); ra = ReadingAction.markFeedRead(newFeedSet, olderThan, newerThan);
} }
dbHelper.markStoriesRead(fs, olderThan, newerThan);
NbActivity.updateAllActivities();
dbHelper.enqueueAction(ra); dbHelper.enqueueAction(ra);
triggerSync(context); triggerSync(context);
return null; return null;

View file

@ -91,6 +91,7 @@ public class PrefsUtils {
} else { } else {
s.append("unknown"); s.append("unknown");
} }
s.append("%0Aprefetch: ").append(isOfflineEnabled(context) ? "yes" : "no");
return s.toString(); return s.toString();
} }