Merge branch 'master' into ios8

* master:
  use notify preference
  seperate notification preference
  fix dismissal cleanup
  more detailed crash logs in log capture
  make notification autocancel quicker
  notification actions
  make notification dismissals stick across launches
  re-tune some logging
  fix notification taps sometimes showing wrong story
  fix story list reusing dead cursors on search query so we can re-enable prompt cursor cleanup
  clarify feedback options
  add logging about action queue yields
  don't reset pagination on slow Reading activity load
  correctly show no-stories explainer for exhausted feeds with all phantom stories
  don't reset API pagination on context switch. don't waste cycles on exhausted feeds.
  tone down some doubled up debug
  fix stuck Loaders on some platforms. improve docs for our custom loader.
  fix story list activity not corresponding to filter setting
  zero-feeds explainer for when in Saved mode
  require explicit android java version, bump android gradle version
This commit is contained in:
Samuel Clay 2017-04-04 12:23:10 -07:00
commit fbacb05c49
28 changed files with 446 additions and 103 deletions

View file

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.newsblur"
android:versionCode="135"
android:versionName="5.1.0" >
android:versionName="5.2.0_b2" >
<uses-sdk
android:minSdkVersion="16"
@ -140,6 +140,10 @@
</receiver>
<receiver android:name=".service.ServiceScheduleReceiver" />
<receiver android:name=".util.NotifyDismissReceiver" android:exported="false" />
<receiver android:name=".util.NotifySaveReceiver" android:exported="false" />
<receiver android:name=".util.NotifyMarkreadReceiver" android:exported="false" />
</application>

View file

@ -6,6 +6,8 @@ It is the goal of this repository to stay agnostic to build environments or tool
## How to Build from the Command Line with Ant
**TODO: this section may be out of date, as GOOG no longer support ant builds**
*an abridged version of the official guide found [here](https://developer.android.com/tools/building/building-cmdline.html)*
*this type of build will use the vendored dependencies in `clients/android/NewsBlur/libs`*
@ -21,11 +23,14 @@ It is the goal of this repository to stay agnostic to build environments or tool
*this type of build will pull dependencies as prescribed in the gradle configuration*
1. install gradle v2.8 or better
1. install gradle v3.3 or better
2. ensure that all Android license agreements have been accepted via `$ANDROID_HOME/tools/bin/sdkmanager tools` or similar
2. build a test APK with `gradle build` (.apk will be in `/build/outputs/apk/` under the working directory)
## How to Build from Android Studio
**TODO: this section may be out of date, as GOOG regularly update the Android Studio UI**
*this type of build will pull dependencies as prescribed in the gradle configuration*
1. install and fully update [Android Studio](http://developer.android.com/tools/studio/index.html)

View file

@ -24,6 +24,9 @@ dependencies {
android {
compileSdkVersion 23
buildToolsVersion '25.0.0'
compileOptions.with {
sourceCompatibility = JavaVersion.VERSION_1_7
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'

View file

@ -149,7 +149,7 @@
<string name="menu_mark_all_as_read">Mark all as read</string>
<string name="menu_logout">Log out</string>
<string name="menu_feedback">Send app feedback</string>
<string name="menu_feedback_post">Create a support post</string>
<string name="menu_feedback_post">Create a feedback post</string>
<string name="menu_feedback_email">Email a bug report</string>
<string name="menu_loginas">Login as...</string>
@ -162,6 +162,7 @@
<string name="empty_list_view_no_unread_stories">You have no unread stories.</string>
<string name="empty_list_view_no_focus_stories">You have no unread stories in Focus mode.\n\nSwitch to All or Unread.</string>
<string name="empty_list_view_no_saved_stories">You have no saved stories.\n\nSwitch to All or Unread.</string>
<string name="login_registration_register">Register</string>
<string name="register_message_error">There was problem registering for NewsBlur.</string>
@ -261,6 +262,8 @@
<string name="settings_immersive_enter_single_tap">Immersive Mode Via Single Tap</string>
<string name="settings_show_content_preview">Show Content Preview Text</string>
<string name="settings_show_thumbnails">Show Image Preview Thumbnails</string>
<string name="settings_notifications">Notifications</string>
<string name="settings_enable_notifications">Enable Notifications</string>
<string name="story">Story</string>
<string name="text">Text</string>

View file

@ -125,4 +125,13 @@
android:defaultValue="@string/rtl_gesture_action_value" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/settings_notifications">
<CheckBoxPreference
android:defaultValue="false"
android:key="enable_notifications"
android:title="@string/settings_enable_notifications" >
</CheckBoxPreference>
</PreferenceCategory>
</PreferenceScreen>

View file

@ -224,11 +224,11 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
}
fs.setSearchQuery(q);
if (!TextUtils.equals(q, oldQuery)) {
NBSyncService.resetReadingSession();
NBSyncService.resetFetchState(fs);
itemListFragment.resetEmptyState();
itemListFragment.hasUpdated();
itemListFragment.scrollToTop();
NBSyncService.resetReadingSession();
NBSyncService.resetFetchState(fs);
}
}

View file

@ -148,7 +148,15 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
unreadCountNeutText.setText(Integer.toString(neutCount));
unreadCountPosiText.setText(Integer.toString(posiCount));
if ((neutCount+posiCount) <= 0) {
}
/**
* A callback for the feed list fragment so it can tell us how many feeds (not folders)
* are being displayed based on mode, etc. This lets us adjust our wrapper UI without
* having to expensively recalculate those totals from the DB.
*/
public void updateFeedCount(int feedCount) {
if (feedCount < 1 ) {
if (NBSyncService.isFeedCountSyncRunning() || (!folderFeedList.firstCursorSeenYet)) {
emptyViewImage.setVisibility(View.INVISIBLE);
emptyViewText.setVisibility(View.INVISIBLE);
@ -156,6 +164,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
emptyViewImage.setVisibility(View.VISIBLE);
if (folderFeedList.currentState == StateFilter.BEST) {
emptyViewText.setText(R.string.empty_list_view_no_focus_stories);
} else if (folderFeedList.currentState == StateFilter.SAVED) {
emptyViewText.setText(R.string.empty_list_view_no_saved_stories);
} else {
emptyViewText.setText(R.string.empty_list_view_no_unread_stories);
}

View file

@ -251,9 +251,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
setupPager();
}
// see if we are just starting and need to jump to a target story
skipPagerToStoryHash();
try {
readingAdapter.notifyDataSetChanged();
} catch (IllegalStateException ise) {
@ -261,6 +258,10 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
finish();
}
// see if we are just starting and need to jump to a target story
skipPagerToStoryHash();
if (unreadSearchActive) {
// if we left this flag high, we were looking for an unread, but didn't find one;
// now that we have more stories, look again.
@ -279,8 +280,9 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
while (stories.moveToNext()) {
if (stopLoading) return;
Story story = Story.fromCursor(stories);
if ( ((storyHash.equals(FIND_FIRST_UNREAD)) && (!story.read)) ||
(story.storyHash.equals(storyHash)) ) {
if ( (story.storyHash.equals(storyHash)) ||
((storyHash.equals(FIND_FIRST_UNREAD)) && (!story.read))
) {
// now that the pager is getting the right story, make it visible
pager.setVisibility(View.VISIBLE);
emptyViewText.setVisibility(View.INVISIBLE);
@ -635,7 +637,8 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
private void triggerRefresh(int desiredStoryCount) {
if (!stopLoading) {
int currentCount = (stories == null) ? 0 : stories.getCount();
Integer currentCount = null;
if (stories != null) currentCount = stories.getCount();
boolean gotSome = NBSyncService.requestMoreForFeed(fs, desiredStoryCount, currentCount);
if (gotSome) triggerSync();
}

View file

@ -28,6 +28,7 @@ public class BlurDatabase extends SQLiteOpenHelper {
db.execSQL(DatabaseConstants.SOCIALFEED_STORIES_SQL);
db.execSQL(DatabaseConstants.STARREDCOUNTS_SQL);
db.execSQL(DatabaseConstants.ACTION_SQL);
db.execSQL(DatabaseConstants.NOTIFY_DISMISS_SQL);
}
void dropAndRecreateTables() {
@ -46,6 +47,7 @@ public class BlurDatabase extends SQLiteOpenHelper {
db.execSQL(drop + DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE);
db.execSQL(drop + DatabaseConstants.STARREDCOUNTS_TABLE);
db.execSQL(drop + DatabaseConstants.ACTION_TABLE);
db.execSQL(drop + DatabaseConstants.NOTIFY_DISMISS_TABLE);
onCreate(db);
}

View file

@ -987,6 +987,22 @@ public class BlurDatabaseHelper {
return rawQuery(DatabaseConstants.NOTIFY_UNREAD_STORY_QUERY, null, null);
}
public Set<String> getNotifyFeeds() {
String q = "SELECT " + DatabaseConstants.FEED_ID + " FROM " + DatabaseConstants.FEED_TABLE +
" WHERE " + DatabaseConstants.FEED_NOTIFICATION_FILTER + " = '" + Feed.NOTIFY_FILTER_FOCUS + "'" +
" OR " + DatabaseConstants.FEED_NOTIFICATION_FILTER + " = '" + Feed.NOTIFY_FILTER_UNREAD + "'";
Cursor c = dbRO.rawQuery(q, null);
Set<String> feedIds = new HashSet<String>(c.getCount());
while (c.moveToNext()) {
String id = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.FEED_ID));
if (id != null) {
feedIds.add(id);
}
}
c.close();
return feedIds;
}
public Loader<Cursor> getActiveStoriesLoader(final FeedSet fs) {
final StoryOrder order = PrefsUtils.getStoryOrder(context, fs);
return new QueryCursorLoader(context) {
@ -1254,6 +1270,33 @@ public class BlurDatabaseHelper {
synchronized (RW_MUTEX) {dbRW.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, reply.getValues(), SQLiteDatabase.CONFLICT_REPLACE);}
}
public void putStoryDismissed(String storyHash) {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.NOTIFY_DISMISS_STORY_HASH, storyHash);
values.put(DatabaseConstants.NOTIFY_DISMISS_TIME, Calendar.getInstance().getTime().getTime());
synchronized (RW_MUTEX) {dbRW.insertOrThrow(DatabaseConstants.NOTIFY_DISMISS_TABLE, null, values);}
}
public boolean isStoryDismissed(String storyHash) {
String[] selArgs = new String[] {storyHash};
String selection = DatabaseConstants.NOTIFY_DISMISS_STORY_HASH + " = ?";
Cursor c = dbRO.query(DatabaseConstants.NOTIFY_DISMISS_TABLE, null, selection, selArgs, null, null, null);
boolean result = (c.getCount() > 0);
closeQuietly(c);
return result;
}
public void cleanupDismissals() {
Calendar cutoffDate = Calendar.getInstance();
cutoffDate.add(Calendar.MONTH, -1);
synchronized (RW_MUTEX) {
int count = dbRW.delete(DatabaseConstants.NOTIFY_DISMISS_TABLE,
DatabaseConstants.NOTIFY_DISMISS_TIME + " < ?",
new String[]{Long.toString(cutoffDate.getTime().getTime())});
com.newsblur.util.Log.d(this.getClass().getName(), "cleaned up dismissals: " + count);
}
}
public static void closeQuietly(Cursor c) {
if (c == null) return;
try {c.close();} catch (Exception e) {;}

View file

@ -147,6 +147,11 @@ public class DatabaseConstants {
public static final String STARREDCOUNTS_TAG = "tag";
public static final String STARREDCOUNTS_FEEDID = "feed_id";
public static final String NOTIFY_DISMISS_TABLE = "notify_dimiss";
public static final String NOTIFY_DISMISS_STORY_HASH = "story_hash";
public static final String NOTIFY_DISMISS_TIME = "time";
static final String FOLDER_SQL = "CREATE TABLE " + FOLDER_TABLE + " (" +
FOLDER_NAME + TEXT + " PRIMARY KEY, " +
FOLDER_PARENT_NAMES + TEXT + ", " +
@ -288,6 +293,11 @@ public class DatabaseConstants {
STARREDCOUNTS_FEEDID + TEXT +
")";
static final String NOTIFY_DISMISS_SQL = "CREATE TABLE " + NOTIFY_DISMISS_TABLE + " (" +
NOTIFY_DISMISS_STORY_HASH + TEXT + ", " +
NOTIFY_DISMISS_TIME + INTEGER + " NOT NULL " +
")";
private static final String[] BASE_STORY_COLUMNS = {
STORY_AUTHORS, STORY_SHORT_CONTENT, STORY_TIMESTAMP, STORY_SHARED_DATE,
STORY_TABLE + "." + STORY_FEED_ID, STORY_TABLE + "." + STORY_ID,

View file

@ -89,6 +89,9 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
private int savedStoriesTotalCount;
/** A simple count of how many feeds/children are actually being displayed. */
public int lastFeedCount = 0;
/** Flat names of folders explicity closed by the user. */
private Set<String> closedFolders = new HashSet<String>();
@ -439,6 +442,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
totalSocialNeutCount += checkNegativeUnreads(f.neutralCount);
totalSocialPosiCount += checkNegativeUnreads(f.positiveCount);
}
recountChildren();
notifyDataSetChanged();
}
@ -539,6 +543,17 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
folderPosCounts.add(getFolderPositiveCountRecursive(folder, null));
}
}
recountChildren();
}
private void recountChildren() {
int newFeedCount = 0;
newFeedCount += socialFeedsOrdered.size();
newFeedCount += starredCountsByTag.size();
for (List<Feed> folder : activeFolderChildren) {
newFeedCount += folder.size();
}
lastFeedCount = newFeedCount;
}
/**

View file

@ -12,22 +12,30 @@ import com.newsblur.util.AppConstants;
/**
* A partial copy of android.content.CursorLoader with the bits related to ContentProviders
* gutted out so plain old SQLiteDatabase queries can be used where a ContentProvider is
* contraindicated. (Why this isn't in core Android I will never understand)
* contraindicated. (Why this isn't in core Android I will never understand) Also fixes
* several bugs with how LoaderManagers interact with AsyncTaskLoaders on several platforms.
*/
public abstract class QueryCursorLoader extends AsyncTaskLoader<Cursor> {
// we hold onto a copy of any cursor vended so we can auto-close it, per the contract of a Loader
private Cursor cursor;
// we create and manage a cancellation hook since SQLite support it and it lets us quickly catch up when behind
protected CancellationSignal cancellationSignal;
public QueryCursorLoader(Context context) {
super(context);
}
/**
* Subclasses (generally anonymous) must actually provide the code to load the cursor, we just
* handly lifecyle management.
*/
protected abstract Cursor createCursor();
// this is the method that AsyncTaskLoader actually calls to the the data object
@Override
public Cursor loadInBackground() {
synchronized (this) {
synchronized (this) {
if (isLoadInBackgroundCanceled()) {
throw new OperationCanceledException();
}
@ -42,9 +50,9 @@ public abstract class QueryCursorLoader extends AsyncTaskLoader<Cursor> {
// being called back. if the instrumentation is ever removed, do not remove this call.
count = c.getCount();
}
if (AppConstants.VERBOSE_LOG_DB) {
if (AppConstants.VERBOSE_LOG) {
long time = System.nanoTime() - startTime;
Log.d(this.getClass().getName(), "cursor load: " + (time/1000000L) + "ms to load " + count + " rows");
com.newsblur.util.Log.d(this.getClass().getName(), "cursor load: " + (time/1000000L) + "ms to load " + count + " rows");
}
return c;
} finally {
@ -54,60 +62,72 @@ public abstract class QueryCursorLoader extends AsyncTaskLoader<Cursor> {
}
}
// this is a hook to try and actively cancel an in-flight load. cancellation flagging is handled elsewhere
@Override
public void cancelLoadInBackground() {
super.cancelLoadInBackground();
synchronized (this) {
if (cancellationSignal != null) {
cancellationSignal.cancel();
cancellationSignal = null;
}
}
}
// a hook for when data are delivered that lets us snag a copy so we can manage it
@Override
public void deliverResult(Cursor cursor) {
if (isReset()) {
if (cursor != null) {
cursor.close();
}
clearCursor();
return;
}
Cursor oldCursor = cursor;
cursor = cursor;
if (this.cursor != cursor) {
clearCursor();
}
this.cursor = cursor;
if (isStarted()) {
super.deliverResult(cursor);
}
if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
oldCursor.close();
}
}
@Override
protected void onStartLoading() {
if (cursor != null) {
// if we already have a cursor and haven't been reset, use it!
deliverResult(cursor);
}
if (takeContentChanged() || cursor == null) {
} else {
takeContentChanged();
forceLoad();
}
}
@Override
protected void onStopLoading() {
// not that we do *not* clear data in this hook. the framework may tell us to stop loading
// but still request our data later. this isn't a reset.
cancelLoad();
}
@Override
public void onCanceled(Cursor cursor) {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
// many some other loaders mysteriously deliver results when they are cancelled and
// *very importantly*, some LoaderManager implementations rely upon this fact. do not
// remove this seemingly incorrect side-effect without rigorously testing many combinations
// of LoaderManager call patterns on all supported platforms.
// not that this may also require double-checking that adapters close cursors to prevent
// cursor leakage
if (cursor != null) {
deliverResult(cursor);
}
}
@Override
protected void onReset() {
super.onReset();
onStopLoading();
clearCursor();
}
private void clearCursor() {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}

View file

@ -228,6 +228,7 @@ public class Story implements Serializable {
*/
@Override
public int hashCode() {
if (storyHash != null) return storyHash.hashCode();
int result = 17;
if (this.id == null) { result = 37*result; } else { result = 37*result + this.id.hashCode();}
if (this.feedId == null) { result = 37*result; } else { result = 37*result + this.feedId.hashCode();}

View file

@ -332,13 +332,20 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
*/
public void pushUnreadCounts() {
((Main) getActivity()).updateUnreadCounts((adapter.totalNeutCount+adapter.totalSocialNeutCount), (adapter.totalPosCount+adapter.totalSocialPosiCount));
((Main) getActivity()).updateFeedCount(adapter.lastFeedCount);
}
@Override
public boolean onGroupClick(ExpandableListView list, View group, int groupPosition, long id) {
Intent i = null;
if (adapter.isFolderRoot(groupPosition)) {
i = new Intent(getActivity(), AllStoriesItemsList.class);
if (currentState == StateFilter.SAVED) {
// the existence of this row in saved mode is something of a framework artifact and may
// confuse users. redirect them to the activity corresponding to what they will actually see
i = new Intent(getActivity(), SavedStoriesItemsList.class);
} else {
i = new Intent(getActivity(), AllStoriesItemsList.class);
}
} else if (groupPosition == FolderListAdapter.GLOBAL_SHARED_STORIES_GROUP_POSITION) {
i = new Intent(getActivity(), GlobalSharedStoriesItemsList.class);
} else if (groupPosition == FolderListAdapter.ALL_SHARED_STORIES_GROUP_POSITION) {

View file

@ -130,17 +130,26 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
}
@Override
public synchronized void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
public void onStart() {
super.onStart();
stopLoading = false;
if (getLoaderManager().getLoader(ITEMLIST_LOADER) == null) {
getLoaderManager().initLoader(ITEMLIST_LOADER, null, this);
}
getLoaderManager().initLoader(ITEMLIST_LOADER, null, this);
}
private void triggerRefresh(int desiredStoryCount, int totalSeen) {
@Override
public void onPause() {
// a pause/resume cycle will depopulate and repopulate the list and trigger bad scroll
// readings and cause zero-index refreshes, wasting massive cycles. hold the refresh logic
// until the loaders reset
cursorSeenYet = false;
super.onPause();
}
private void triggerRefresh(int desiredStoryCount, Integer totalSeen) {
// ask the sync service for as many stories as we want
boolean gotSome = NBSyncService.requestMoreForFeed(getFeedSet(), desiredStoryCount, totalSeen);
if (gotSome) triggerSync();
// if the service thinks it can get more, or if we haven't even seen a cursor yet, start the service
if (gotSome || (totalSeen == null)) triggerSync();
}
/**
@ -154,6 +163,11 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
* Indicate that the DB was cleared.
*/
public void resetEmptyState() {
// this is going to cause us to lose access to any previous cursor and the next one might be
// stale, so wipe the listview. the adapter will be recreated in onLoadFinished as usual
if (adapter != null) adapter.notifyDataSetInvalidated();
if (itemList != null) itemList.setAdapter(null);
adapter = null;
cursorSeenYet = false;
FeedUtils.dbHelper.clearStorySession();
}
@ -280,7 +294,7 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
if (NBSyncService.ResetSession) {
// the DB hasn't caught up yet from the last story list; don't display stale stories.
com.newsblur.util.Log.i(this.getClass().getName(), "discarding stale load");
triggerRefresh(1, 0);
triggerRefresh(1, null);
return;
}
cursorSeenYet = true;

View file

@ -600,10 +600,6 @@ public class APIManager {
return new APIResponse(context);
}
if (AppConstants.VERBOSE_LOG) {
Log.d(this.getClass().getName(), "API GET " + urlString);
}
Request.Builder requestBuilder = new Request.Builder().url(urlString);
addCookieHeader(requestBuilder);
requestBuilder.header("User-Agent", this.customUserAgent);

View file

@ -29,6 +29,9 @@ public class CleanupService extends SubService {
com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up old story texts");
parent.dbHelper.cleanupStoryText();
com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up notification dismissals");
parent.dbHelper.cleanupDismissals();
com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up story image cache");
FileCache imageCache = FileCache.asStoryImageCache(parent);
imageCache.cleanupUnusedOrOld(parent.dbHelper.getAllStoryImages());

View file

@ -180,7 +180,7 @@ public class NBSyncService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, final int startId) {
// only perform a sync if the app is actually running or background syncs are enabled
if (PrefsUtils.isOfflineEnabled(this) || (NbActivity.getActiveActivityCount() > 0)) {
if ((NbActivity.getActiveActivityCount() > 0) || PrefsUtils.isBackgroundNeeded(this)) {
// Services actually get invoked on the main system thread, and are not
// allowed to do tangible work. We spawn a thread to do so.
Runnable r = new Runnable() {
@ -231,7 +231,9 @@ public class NBSyncService extends Service {
housekeeping();
// check to see if we are on an allowable network only after ensuring we have CPU
if (!(PrefsUtils.isBackgroundNetworkAllowed(this) || (NbActivity.getActiveActivityCount() > 0))) {
if (!( (NbActivity.getActiveActivityCount() > 0) ||
PrefsUtils.isEnableNotifications(this) ||
PrefsUtils.isBackgroundNetworkAllowed(this) )) {
Log.d(this.getClass().getName(), "Abandoning sync: app not active and network type not appropriate for background sync.");
return;
}
@ -405,7 +407,11 @@ public class NBSyncService extends Service {
private void syncMetadata(int startId) {
if (stopSync()) return;
if (backoffBackgroundCalls()) return;
if (dbHelper.getUntriedActionCount() > 0) return;
int untriedActions = dbHelper.getUntriedActionCount();
if (untriedActions > 0) {
com.newsblur.util.Log.i(this.getClass().getName(), untriedActions + " outstanding actions, yielding metadata sync");
return;
}
if (DoFeedsFolders || PrefsUtils.isTimeToAutoSync(this)) {
PrefsUtils.updateLastSyncTime(this);
@ -625,20 +631,44 @@ public class NBSyncService extends Service {
* Fetch stories needed because the user is actively viewing a feed or folder.
*/
private void syncPendingFeedStories() {
// before anything else, see if we need to quickly reset fetch state for a feed
if (ResetFeed != null) {
ExhaustedFeeds.remove(ResetFeed);
FeedStoriesSeen.remove(ResetFeed);
FeedPagesSeen.remove(ResetFeed);
ResetFeed = null;
}
// track whether we actually tried to handle the feedset and found we had nothing
// more to do, in which case we will clear it
boolean finished = false;
FeedSet fs = PendingFeed;
boolean finished = false;
if (fs == null) {
return;
}
try {
if (fs == null) {
return;
}
// before anything else, see if we need to quickly reset fetch state for a feed. we
// do this as part of the loop to prevent-mid loop state corruption
if (ResetFeed != null) {
ExhaustedFeeds.remove(ResetFeed);
FeedStoriesSeen.remove(ResetFeed);
FeedPagesSeen.remove(ResetFeed);
ResetFeed = null;
}
// now see if we need to reset the reading session table because the FeedSet was
// switched out
boolean doReset = false;
synchronized (PENDING_FEED_MUTEX) {
if (ResetSession) {
doReset = true;
ResetSession = false;
}
}
if (doReset) {
// the next fetch will be the start of a new reading session; clear it so it
// will be re-primed
dbHelper.clearStorySession();
// don't just rely on the auto-prepare code when fetching stories, it might be called
// after we insert our first page and not trigger
dbHelper.prepareReadingSession(fs);
}
if (ExhaustedFeeds.contains(fs)) {
com.newsblur.util.Log.i(this.getClass().getName(), "No more stories for feed set: " + fs);
finished = true;
@ -656,22 +686,9 @@ public class NBSyncService extends Service {
StoryOrder order = PrefsUtils.getStoryOrder(this, fs);
ReadFilter filter = PrefsUtils.getReadFilter(this, fs);
boolean doReset = false;
synchronized (PENDING_FEED_MUTEX) {
if (ResetSession) {
doReset = true;
ResetSession = false;
}
}
if (doReset) {
// the next fetch will be the start of a new reading session; clear it so it
// will be re-primed
dbHelper.clearStorySession();
// don't just rely on the auto-prepare code when fetching stories, it might be called
// after we insert our first page and not trigger
dbHelper.prepareReadingSession(fs);
}
StorySyncRunning = true;
NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS);
while (totalStoriesSeen < PendingFeedTarget) {
if (stopSync()) return;
// this is a good heuristic for double-checking if we have left the story list
@ -682,9 +699,6 @@ public class NBSyncService extends Service {
return;
}
StorySyncRunning = true;
NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS);
pageNumber++;
StoriesResponse apiResponse = apiManager.getStories(fs, pageNumber, order, filter);
@ -697,7 +711,7 @@ public class NBSyncService extends Service {
insertStories(apiResponse, fs);
// re-do any very recent actions that were incorrectly overwritten by this page
finishActions();
NbActivity.updateAllActivities(NbActivity.UPDATE_STORY);
NbActivity.updateAllActivities(NbActivity.UPDATE_STORY | NbActivity.UPDATE_STATUS);
FeedPagesSeen.put(fs, pageNumber);
totalStoriesSeen += apiResponse.stories.length;
@ -714,10 +728,10 @@ public class NBSyncService extends Service {
finished = true;
} finally {
if (StorySyncRunning) {
StorySyncRunning = false;
NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS);
}
StorySyncRunning = false;
// even if we didn't do much of a sync, a fragment might be waiting to even see if we
// tried, so still signal a status change and that story data (empty or not) are ready
NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS | NbActivity.UPDATE_STORY);
synchronized (PENDING_FEED_MUTEX) {
if (finished && fs.equals(PendingFeed)) PendingFeed = null;
}
@ -789,6 +803,8 @@ public class NBSyncService extends Service {
}
void pushNotifications() {
if (! PrefsUtils.isEnableNotifications(this)) return;
// don't notify stories until the queue is flushed so they don't churn
if (unreadsService.StoryHashQueue.size() > 0) return;
// don't slow down active story loading
@ -935,18 +951,17 @@ public class NBSyncService extends Service {
* @param totalSeen the number of stories the caller thinks they have seen for the FeedSet
* or a negative number if the caller trusts us to track for them
*/
public static boolean requestMoreForFeed(FeedSet fs, int desiredStoryCount, int callerSeen) {
if (ExhaustedFeeds.contains(fs)) {
com.newsblur.util.Log.d(NBSyncService.class.getName(), "rejecting request for feedset that is exhaused");
return false;
}
public static boolean requestMoreForFeed(FeedSet fs, int desiredStoryCount, Integer callerSeen) {
synchronized (PENDING_FEED_MUTEX) {
if (ExhaustedFeeds.contains(fs) && (!ResetSession)) {
com.newsblur.util.Log.d(NBSyncService.class.getName(), "rejecting request for feedset that is exhaused");
return false;
}
Integer alreadyPending = 0;
if (fs.equals(PendingFeed)) alreadyPending = PendingFeedTarget;
Integer alreadySeen = FeedStoriesSeen.get(fs);
if (alreadySeen == null) alreadySeen = 0;
if (callerSeen < alreadySeen) {
if ((callerSeen != null) && (callerSeen < alreadySeen)) {
// the caller is probably filtering and thinks they have fewer than we do, so
// update our count to agree with them, and force-allow another requet
alreadySeen = callerSeen;
@ -954,6 +969,9 @@ public class NBSyncService extends Service {
alreadyPending = 0;
}
PendingFeed = fs;
PendingFeedTarget = desiredStoryCount;
if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "callerhas: " + callerSeen + " have:" + alreadySeen + " want:" + desiredStoryCount + " pending:" + alreadyPending);
if (desiredStoryCount <= alreadySeen) {
return false;
@ -962,8 +980,6 @@ public class NBSyncService extends Service {
return false;
}
PendingFeed = fs;
PendingFeedTarget = desiredStoryCount;
}
return true;
}

View file

@ -10,6 +10,7 @@ import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.network.domain.UnreadStoryHashesResponse;
import com.newsblur.util.AppConstants;
import com.newsblur.util.DefaultFeedView;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.StoryOrder;
@ -93,6 +94,15 @@ public class UnreadsService extends SubService {
com.newsblur.util.Log.i(this.getClass().getName(), "new unreads found: " + sortationList.size());
com.newsblur.util.Log.i(this.getClass().getName(), "unreads to retire: " + oldUnreadHashes.size());
if (parent.stopSync()) return;
// any stories that we previously thought to be unread but were not found in the
// list, mark them read now
parent.dbHelper.markStoryHashesRead(oldUnreadHashes);
if (parent.stopSync()) return;
// now sort the unreads we need to fetch so they are fetched roughly in the order
// the user is likely to read them. if the user reads newest first, those come first.
final boolean sortNewest = (PrefsUtils.getDefaultStoryOrder(parent) == StoryOrder.NEWEST);
@ -120,24 +130,29 @@ public class UnreadsService extends SubService {
StoryHashQueue.add(tuple[0]);
}
if (parent.stopSync()) return;
// any stories that we previously thought to be unread but were not found in the
// list, mark them read now
parent.dbHelper.markStoryHashesRead(oldUnreadHashes);
}
private void getNewUnreadStories() {
int totalCount = StoryHashQueue.size();
Set<String> notifyFeeds = parent.dbHelper.getNotifyFeeds();
unreadsyncloop: while (StoryHashQueue.size() > 0) {
if (parent.stopSync()) return;
if(!PrefsUtils.isOfflineEnabled(parent)) return;
boolean isOfflineEnabled = PrefsUtils.isOfflineEnabled(parent);
boolean isEnableNotifications = PrefsUtils.isEnableNotifications(parent);
if (! (isOfflineEnabled || isEnableNotifications)) return;
gotWork();
startExpensiveCycle();
List<String> hashBatch = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
List<String> hashSkips = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
batchloop: for (String hash : StoryHashQueue) {
hashBatch.add(hash);
if( isOfflineEnabled ||
(isEnableNotifications && notifyFeeds.contains(FeedUtils.inferFeedId(hash))) ) {
hashBatch.add(hash);
} else {
hashSkips.add(hash);
}
if (hashBatch.size() >= AppConstants.UNREAD_FETCH_BATCH_SIZE) break batchloop;
}
StoriesResponse response = parent.apiManager.getStoriesByHash(hashBatch);
@ -150,6 +165,9 @@ public class UnreadsService extends SubService {
for (String hash : hashBatch) {
StoryHashQueue.remove(hash);
}
for (String hash : hashSkips) {
StoryHashQueue.remove(hash);
}
for (Story story : response.stories) {
if (story.imageUrls != null) {

View file

@ -78,10 +78,14 @@ public class FeedUtils {
}
public static void setStorySaved(final Story story, final boolean saved, final Context context) {
setStorySaved(story.storyHash, saved, context);
}
public static void setStorySaved(final String storyHash, final boolean saved, final Context context) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... arg) {
ReadingAction ra = (saved ? ReadingAction.saveStory(story.storyHash) : ReadingAction.unsaveStory(story.storyHash));
ReadingAction ra = (saved ? ReadingAction.saveStory(storyHash) : ReadingAction.unsaveStory(storyHash));
ra.doLocal(dbHelper);
NbActivity.updateAllActivities(NbActivity.UPDATE_STORY);
dbHelper.enqueueAction(ra);
@ -164,6 +168,23 @@ public class FeedUtils {
NBSyncService.addRecountCandidates(impactedFeeds);
}
/**
* Mark a story (un)read when only the hash is known. This can and will cause a brief mismatch in
* unread counts, or a longer mismatch if offline. This method should only be used from outside
* the app, such as from a notifiation handler. You must use setStoryReadState(Story, Context, boolean)
* when calling from within the UI.
*/
public static void setStoryReadStateExternal(String storyHash, Context context, boolean read) {
ReadingAction ra = (read ? ReadingAction.markStoryRead(storyHash) : ReadingAction.markStoryUnread(storyHash));
dbHelper.enqueueAction(ra);
String feedId = inferFeedId(storyHash);
FeedSet impactedFeed = FeedSet.singleFeed(feedId);
NBSyncService.addRecountCandidates(impactedFeed);
triggerSync(context);
}
/**
* Marks some or all of the stories in a FeedSet as read for an activity, handling confirmation dialogues as necessary.
*/

View file

@ -5,9 +5,13 @@ import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Queue;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
@ -30,7 +34,7 @@ public class Log {
private static final String LOG_NAME_INTERNAL = "logbuffer.txt";
private static final int MAX_LINE_SIZE = 4 * 1024;
private static final int TRIM_LINES = 256; // trim the log down to 256 lines
private static final int TRIM_LINES = 384; // trim the log down to 384 lines
private static final long MAX_SIZE = 512L * MAX_LINE_SIZE; // when it is at least 512 lines long
private static Queue<String> q;
@ -40,6 +44,11 @@ public class Log {
q = new ConcurrentLinkedQueue<String>();
executor = Executors.newFixedThreadPool(1);
}
private static DateFormat dateFormat = null;
static {
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private Log() {} // util class - no instances
@ -72,16 +81,18 @@ public class Log {
if (q.size() > TRIM_LINES) return;
if (m != null && m.length() > MAX_LINE_SIZE) m = m.substring(0, MAX_LINE_SIZE);
StringBuilder s = new StringBuilder();
s.append(Long.toString(System.currentTimeMillis()))
.append(" ")
synchronized (dateFormat) {s.append(dateFormat.format(new Date()));}
s.append(" ")
.append(lvl)
.append(tag)
.append(" ");
s.append(m);
if (t != null) {
s.append(" ");
s.append(t.getMessage());
s.append(" ");
s.append(android.util.Log.getStackTraceString(t));
}
s.append(m);
q.offer(s.toString());
Runnable r = new Runnable() {
public void run() {

View file

@ -31,6 +31,10 @@ public class NotificationUtils {
private NotificationUtils() {} // util class - no instances
/**
* @param storiesFocus a cursor of unread, focus stories to notify, ordered newest to oldest
* @param storiesUnread a cursor of unread, neutral stories to notify, ordered newest to oldest
*/
public static synchronized void notifyStories(Cursor storiesFocus, Cursor storiesUnread, Context context, FileCache iconCache) {
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
@ -41,11 +45,16 @@ public class NotificationUtils {
nm.cancel(story.hashCode());
continue;
}
if (FeedUtils.dbHelper.isStoryDismissed(story.storyHash)) {
nm.cancel(story.hashCode());
continue;
}
if (count < MAX_CONCUR_NOTIFY) {
Notification n = buildStoryNotification(story, storiesFocus, context, iconCache);
nm.notify(story.hashCode(), n);
} else {
nm.cancel(story.hashCode());
FeedUtils.dbHelper.putStoryDismissed(story.storyHash);
}
count++;
}
@ -55,16 +64,23 @@ public class NotificationUtils {
nm.cancel(story.hashCode());
continue;
}
if (FeedUtils.dbHelper.isStoryDismissed(story.storyHash)) {
nm.cancel(story.hashCode());
continue;
}
if (count < MAX_CONCUR_NOTIFY) {
Notification n = buildStoryNotification(story, storiesUnread, context, iconCache);
nm.notify(story.hashCode(), n);
} else {
nm.cancel(story.hashCode());
FeedUtils.dbHelper.putStoryDismissed(story.storyHash);
}
count++;
}
}
// addAction deprecated in 23 but replacement not avail until 21
@SuppressWarnings("deprecation")
private static Notification buildStoryNotification(Story story, Cursor cursor, Context context, FileCache iconCache) {
Intent i = new Intent(context, FeedReading.class);
// the action is unused, but bugs in some platform versions ignore extras if it is unset
@ -79,6 +95,18 @@ public class NotificationUtils {
// set the requestCode to the story hashcode to prevent the PI re-using the wrong Intent
PendingIntent pendingIntent = PendingIntent.getActivity(context, story.hashCode(), i, 0);
Intent dismissIntent = new Intent(context, NotifyDismissReceiver.class);
dismissIntent.putExtra(Reading.EXTRA_STORY_HASH, story.storyHash);
PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), story.hashCode(), dismissIntent, 0);
Intent saveIntent = new Intent(context, NotifySaveReceiver.class);
saveIntent.putExtra(Reading.EXTRA_STORY_HASH, story.storyHash);
PendingIntent savePendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), story.hashCode(), saveIntent, 0);
Intent markreadIntent = new Intent(context, NotifyMarkreadReceiver.class);
markreadIntent.putExtra(Reading.EXTRA_STORY_HASH, story.storyHash);
PendingIntent markreadPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), story.hashCode(), markreadIntent, 0);
String feedTitle = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_TITLE));
StringBuilder title = new StringBuilder();
title.append(feedTitle).append(": ").append(story.title);
@ -91,8 +119,11 @@ public class NotificationUtils {
.setContentText(story.shortContent)
.setSmallIcon(R.drawable.logo_monochrome)
.setContentIntent(pendingIntent)
.setDeleteIntent(dismissPendingIntent)
.setAutoCancel(true)
.setWhen(story.timestamp);
.setWhen(story.timestamp)
.addAction(0, "Save", savePendingIntent)
.addAction(0, "Mark Read", markreadPendingIntent);
if (feedIcon != null) {
nb.setLargeIcon(feedIcon);
}
@ -108,4 +139,10 @@ public class NotificationUtils {
nm.cancelAll();
}
public static void cancel(Context context, int nid) {
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(nid);
}
}

View file

@ -0,0 +1,25 @@
package com.newsblur.util;
import com.newsblur.activity.Reading;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
public class NotifyDismissReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context c, final Intent i) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... arg) {
String storyHash = i.getStringExtra(Reading.EXTRA_STORY_HASH);
FeedUtils.offerInitContext(c);
FeedUtils.dbHelper.putStoryDismissed(storyHash);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}

View file

@ -0,0 +1,27 @@
package com.newsblur.util;
import com.newsblur.activity.Reading;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
public class NotifyMarkreadReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context c, final Intent i) {
final String storyHash = i.getStringExtra(Reading.EXTRA_STORY_HASH);
NotificationUtils.cancel(c, storyHash.hashCode());
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... arg) {
FeedUtils.offerInitContext(c);
FeedUtils.dbHelper.putStoryDismissed(storyHash);
FeedUtils.setStoryReadStateExternal(storyHash, c, true);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}

View file

@ -0,0 +1,27 @@
package com.newsblur.util;
import com.newsblur.activity.Reading;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
public class NotifySaveReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context c, final Intent i) {
final String storyHash = i.getStringExtra(Reading.EXTRA_STORY_HASH);
NotificationUtils.cancel(c, storyHash.hashCode());
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... arg) {
FeedUtils.offerInitContext(c);
FeedUtils.dbHelper.putStoryDismissed(storyHash);
FeedUtils.setStorySaved(storyHash, true, c);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}

View file

@ -78,4 +78,6 @@ public class PrefConstants {
public static final String LTR_GESTURE_ACTION = "ltr_gesture_action";
public static final String RTL_GESTURE_ACTION = "rtl_gesture_action";
public static final String ENABLE_NOTIFICATIONS = "enable_notifications";
}

View file

@ -140,6 +140,8 @@ public class PrefsUtils {
s.append("\n");
s.append("prefetch: ").append(isOfflineEnabled(context) ? "yes" : "no");
s.append("\n");
s.append("notifications: ").append(isEnableNotifications(context) ? "yes" : "no");
s.append("\n");
s.append("keepread: ").append(isKeepOldStories(context) ? "yes" : "no");
s.append("\n");
s.append("thumbs: ").append(isShowThumbnails(context) ? "yes" : "no");
@ -678,4 +680,13 @@ public class PrefsUtils {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return GestureAction.valueOf(prefs.getString(PrefConstants.RTL_GESTURE_ACTION, GestureAction.GEST_ACTION_MARKUNREAD.toString()));
}
public static boolean isEnableNotifications(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return prefs.getBoolean(PrefConstants.ENABLE_NOTIFICATIONS, false);
}
public static boolean isBackgroundNeeded(Context context) {
return (isEnableNotifications(context) || isOfflineEnabled(context));
}
}