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

View file

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

View file

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

View file

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

View file

@ -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"

View file

@ -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"

View file

@ -104,7 +104,7 @@
<color name="progress_circle_complete">#8F918B</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="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_unread">Mark as unread</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_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_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_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>
@ -148,7 +136,6 @@
<string name="empty_list_view_no_stories">No stories to read</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="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>
@ -159,11 +146,6 @@
<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_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="comment_favourited">Comment favorited</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_text">Storing text for %s stories...</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="off">Off</string>

View file

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

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 com.newsblur.R;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.fragment.FolderItemListFragment;
import com.newsblur.fragment.MarkAllReadDialogFragment;
import com.newsblur.fragment.MarkAllReadDialogFragment.MarkAllReadDialogListener;

View file

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

View file

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

View file

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

View file

@ -99,6 +99,8 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
private float overlayRangeTopPx;
private float overlayRangeBotPx;
private int lastVScrollPos = 0;
private List<Story> 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);
}

View file

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

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) {
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<FeedSet> setStoryReadState(Story story, boolean read) {
// calculate the impact surface so the caller can re-check counts if needed
Set<FeedSet> impactedFeeds = new HashSet<FeedSet>();
impactedFeeds.add(FeedSet.singleFeed(story.feedId));
Set<String> socialIds = new HashSet<String>();
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<String> feedIds = new ArrayList<String>();
List<String> socialFeedIds = new ArrayList<String>();
@ -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);

View file

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

View file

@ -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() {

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_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/";

View file

@ -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<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() {
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) {

View file

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

View file

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

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 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.
*/

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.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<ReadingAction> FollowupActions;
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;
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<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.
*/
@ -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<FeedSet> 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();
}

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
// 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;

View file

@ -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<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";
public String toCompactSerial() {

View file

@ -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<FeedSet> 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<Void, Void, Void>() {
@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;

View file

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