Merge pull request #581 from dosiecki/master

Android: Beta 3 (again)
This commit is contained in:
Samuel Clay 2014-09-23 13:04:23 -07:00
commit aae9793216
10 changed files with 237 additions and 97 deletions

View file

@ -152,13 +152,16 @@ public abstract class ItemsList extends NbActivity implements StateChangedListen
public void handleUpdate() {
updateStatusIndicators();
if (itemListFragment != null) {
itemListFragment.syncDone();
itemListFragment.hasUpdated();
}
}
private void updateStatusIndicators() {
setProgressBarIndeterminateVisibility(NBSyncService.isFeedSetSyncing(this.fs));
boolean isLoading = NBSyncService.isFeedSetSyncing(this.fs);
setProgressBarIndeterminateVisibility(isLoading);
if (itemListFragment != null) {
itemListFragment.setLoading(isLoading);
}
if (overlayStatusText != null) {
String syncStatus = NBSyncService.getSyncStatusMessage();

View file

@ -535,6 +535,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
private void markStoryUnread(Story story) {
synchronized (STORIES_MUTEX) {
FeedUtils.markStoryUnread(story, this);
Toast.makeText(Reading.this, R.string.toast_story_unread, Toast.LENGTH_SHORT).show();
updateCursor();
}
enableOverlays();
@ -607,6 +608,12 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
}
}
// we didn't find a story, so now we need to get more stories. First, though,
// double check that there are even any left
if (getUnreadCount() <= 0) {
break unreadSearch;
}
// if we didn't find a story trigger a check to see if there are any more to search before proceeding
this.unreadSearchLatch = new CountDownLatch(1);
this.checkStoryCount(candidate+1);

View file

@ -21,6 +21,7 @@ import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadingAction;
import com.newsblur.util.ReadFilter;
import com.newsblur.util.StoryOrder;
@ -359,36 +360,15 @@ public class BlurDatabaseHelper {
dbRW.update(DatabaseConstants.SOCIALFEED_TABLE, values, whereClause, whereArgs);
}
public void enqueueActionStoryRead(String hash, boolean read) {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.ACTION_TIME, System.currentTimeMillis());
values.put(DatabaseConstants.ACTION_STORY_HASH, hash);
values.put(read ? DatabaseConstants.ACTION_MARK_READ : DatabaseConstants.ACTION_MARK_UNREAD, 1);
dbRW.insertOrThrow(DatabaseConstants.ACTION_TABLE, null, values);
}
public void enqueueActionFeedRead(FeedSet fs, Long olderThan, Long newerThan) {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.ACTION_TIME, System.currentTimeMillis());
values.put(DatabaseConstants.ACTION_MARK_READ, 1);
values.put(DatabaseConstants.ACTION_FEED_ID, TextUtils.join(",", fs.getFeedIds()));
if (olderThan != null) values.put(DatabaseConstants.ACTION_INCLUDE_OLDER, olderThan);
if (newerThan != null) values.put(DatabaseConstants.ACTION_INCLUDE_NEWER, newerThan);
dbRW.insertOrThrow(DatabaseConstants.ACTION_TABLE, null, values);
public void enqueueAction(ReadingAction ra) {
dbRW.insertOrThrow(DatabaseConstants.ACTION_TABLE, null, ra.toContentValues());
}
public Cursor getActions(boolean includeDone) {
String q = "SELECT * FROM " + DatabaseConstants.ACTION_TABLE +
" WHERE " + DatabaseConstants.ACTION_DONE_REMOTE + " = " + (includeDone ? 1 : 0);
String q = "SELECT * FROM " + DatabaseConstants.ACTION_TABLE;
return dbRO.rawQuery(q, null);
}
public void setActionDone(String actionId) {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.ACTION_DONE_REMOTE, 1);
dbRW.update(DatabaseConstants.ACTION_TABLE, values, DatabaseConstants.ACTION_ID + " = ?", new String[]{actionId});
}
public void clearAction(String actionId) {
dbRW.delete(DatabaseConstants.ACTION_TABLE, DatabaseConstants.ACTION_ID + " = ?", new String[]{actionId});
}

View file

@ -118,8 +118,6 @@ public class DatabaseConstants {
public static final String ACTION_TABLE = "story_actions";
public static final String ACTION_ID = BaseColumns._ID;
public static final String ACTION_TIME = "time";
public static final String ACTION_DONE_REMOTE = "done_remote";
public static final String ACTION_DONE_LOCAL = "done_local";
public static final String ACTION_MARK_READ = "mark_read";
public static final String ACTION_MARK_UNREAD = "mark_unread";
public static final String ACTION_SAVE = "save";
@ -251,7 +249,6 @@ public class DatabaseConstants {
static final String ACTION_SQL = "CREATE TABLE " + ACTION_TABLE + " (" +
ACTION_ID + INTEGER + " PRIMARY KEY AUTOINCREMENT, " +
ACTION_TIME + INTEGER + " NOT NULL, " +
ACTION_DONE_REMOTE + INTEGER + " DEFAULT 0, " +
ACTION_MARK_READ + INTEGER + " DEFAULT 0, " +
ACTION_MARK_UNREAD + INTEGER + " DEFAULT 0, " +
ACTION_SAVE + INTEGER + " DEFAULT 0, " +

View file

@ -4,6 +4,7 @@ import android.content.AsyncTaskLoader;
import android.content.Context;
import android.database.Cursor;
import android.os.CancellationSignal;
import android.os.Build;
import android.os.OperationCanceledException;
/**
@ -25,10 +26,12 @@ public abstract class QueryCursorLoader extends AsyncTaskLoader<Cursor> {
@Override
public Cursor loadInBackground() {
synchronized (this) {
if (isLoadInBackgroundCanceled()) {
throw new OperationCanceledException();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
if (isLoadInBackgroundCanceled()) {
throw new OperationCanceledException();
}
cancellationSignal = new CancellationSignal();
}
cancellationSignal = new CancellationSignal();
}
try {
Cursor c = createCursor();

View file

@ -45,8 +45,8 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
protected StoryItemsAdapter adapter;
protected DefaultFeedView defaultFeedView;
protected int currentState;
private boolean firstSyncDone = false;
private int lastRequestedStoryCount = 0;
private boolean isLoading = true;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -57,16 +57,15 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
* Indicate that the DB was cleared.
*/
public void resetEmptyState() {
firstSyncDone = false;
setEmptyListView(R.string.empty_list_view_loading);
setLoading(true);
lastRequestedStoryCount = 0;
}
public void syncDone() {
this.firstSyncDone = true;
public void setLoading(boolean loading) {
isLoading = loading;
}
private void setEmptyListView(int rid) {
private void updateLoadingIndicator() {
View v = this.getView();
if (v == null) return; // we might have beat construction?
@ -75,9 +74,13 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
Log.w(this.getClass().getName(), "ItemListFragment does not have the expected ListView.");
return;
}
TextView emptyView = (TextView) itemList.getEmptyView();
emptyView.setText(rid);
if (isLoading) {
emptyView.setText(R.string.empty_list_view_loading);
} else {
emptyView.setText(R.string.empty_list_view_no_stories);
}
}
public void scrollToTop() {
@ -141,12 +144,8 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
triggerRefresh(1);
}
adapter.swapCursor(cursor);
// iff a sync has finished and a cursor load has finished, it is safe to remove the loading message
if (this.firstSyncDone) {
setEmptyListView(R.string.empty_list_view_no_stories);
}
}
updateLoadingIndicator();
}
@Override

View file

@ -27,6 +27,7 @@ import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedSet;
import com.newsblur.util.ImageCache;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadingAction;
import com.newsblur.util.ReadFilter;
import com.newsblur.util.StoryOrder;
@ -78,17 +79,27 @@ public class NBSyncService extends Service {
/** Feed sets that we need to sync and how many stories the UI wants for them. */
private static Map<FeedSet,Integer> PendingFeeds;
static { PendingFeeds = new HashMap<FeedSet,Integer>(); }
/** Feed sets that the API has said to have no more pages left. */
private static Set<FeedSet> ExhaustedFeeds;
static { ExhaustedFeeds = new HashSet<FeedSet>(); }
/** The number of pages we have collected for the given feed set. */
private static Map<FeedSet,Integer> FeedPagesSeen;
static { FeedPagesSeen = new HashMap<FeedSet,Integer>(); }
/** The number of stories we have collected for the given feed set. */
private static Map<FeedSet,Integer> FeedStoriesSeen;
static { FeedStoriesSeen = new HashMap<FeedSet,Integer>(); }
/** Unread story hashes the API listed that we do not appear to have locally yet. */
private static Set<String> StoryHashQueue;
static { StoryHashQueue = new HashSet<String>(); }
/** URLs of images contained in recently fetched stories that are candidates for prefetch. */
private static Set<String> ImageQueue;
static { ImageQueue = new HashSet<String>(); }
/** Actions that may need to be double-checked locally due to overlapping API calls. */
private static List<ReadingAction> FollowupActions;
static { FollowupActions = new ArrayList<ReadingAction>(); }
private PowerManager.WakeLock wl = null;
private ExecutorService executor;
private APIManager apiManager;
@ -203,31 +214,8 @@ public class NBSyncService extends Service {
actionsloop : while (c.moveToNext()) {
String id = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_ID));
NewsBlurResponse response = null;
if (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_MARK_READ)) == 1) {
String hash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
String feedIds = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
Long includeOlder = DatabaseConstants.nullIfZero(c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_INCLUDE_OLDER)));
Long includeNewer = DatabaseConstants.nullIfZero(c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_INCLUDE_NEWER)));
if (hash != null) {
response = apiManager.markStoryAsRead(hash);
} else if (feedIds != null) {
Set<String> feeds = new HashSet<String>();
for (String feedId : TextUtils.split(feedIds, ",")) feeds.add(feedId);
if (feeds.size() == 0) {
response = apiManager.markAllAsRead();
} else {
response = apiManager.markFeedsAsRead(feeds, includeOlder, includeNewer);
}
} else {
Log.w(this.getClass().getName(), "discarding mark-read action of unknown type: " + id);
}
} else if (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_MARK_UNREAD)) == 1) {
String hash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
response = apiManager.markStoryHashUnread(hash);
} else {
Log.w(this.getClass().getName(), "discarding action of unknown type: " + id);
}
ReadingAction ra = ReadingAction.fromCursor(c);
NewsBlurResponse response = ra.doRemote(apiManager);
// if we attempted a call and it failed, do not mark the action as done
if (response != null) {
@ -236,7 +224,8 @@ public class NBSyncService extends Service {
}
}
dbHelper.setActionDone(id);
dbHelper.clearAction(id);
FollowupActions.add(ra);
}
} finally {
closeQuietly(c);
@ -252,33 +241,17 @@ public class NBSyncService extends Service {
private void finishActions() {
if (HaltNow) return;
Cursor c = null;
try {
c = dbHelper.getActions(true);
if (c.getCount() < 1) return;
if (FollowupActions.size() < 1) return;
Log.d(this.getClass().getName(), "found done actions: " + c.getCount());
Log.d(this.getClass().getName(), "found old actions: " + FollowupActions.size());
ActionsRunning = true;
NbActivity.updateAllActivities();
while (c.moveToNext()) {
String id = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_ID));
if (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_MARK_READ)) == 1) {
String hash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
if (hash != null ) {
dbHelper.setStoryReadState(hash, true);
}
// TODO: double-check stories from feed-marks
} else if (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_MARK_UNREAD)) == 1) {
String hash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
dbHelper.setStoryReadState(hash, false);
} else {
Log.w(this.getClass().getName(), "discarding action of unknown type: " + id);
}
dbHelper.clearAction(id);
for (ReadingAction ra : FollowupActions) {
ra.doLocalSecondary(dbHelper);
}
FollowupActions.clear();
} finally {
closeQuietly(c);
ActionsRunning = false;
NbActivity.updateAllActivities();
}

View file

@ -159,12 +159,13 @@ public class FeedUtils {
// update the local object to show as read before DB is touched
story.read = read;
// update unread state and unread counts in the local DB
dbHelper.setStoryReadState(story, read);
// tell the sync service we need to mark read
dbHelper.enqueueActionStoryRead(story.storyHash, read);
ReadingAction ra = (read ? ReadingAction.markStoryRead(story.storyHash) : ReadingAction.markStoryUnread(story.storyHash));
dbHelper.enqueueAction(ra);
triggerSync(context);
}
@ -172,7 +173,8 @@ public class FeedUtils {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... arg) {
dbHelper.enqueueActionFeedRead(fs, olderThan, newerThan);
ReadingAction ra = ReadingAction.markFeedRead(fs, olderThan, newerThan);
dbHelper.enqueueAction(ra);
dbHelper.markFeedsRead(fs, olderThan, newerThan);
triggerSync(context);
return null;

View file

@ -96,8 +96,6 @@ public class PrefsUtils {
NBSyncService.softInterrupt();
// TODO: wait for any BG processes
// wipe the prefs store
context.getSharedPreferences(PrefConstants.PREFERENCES, 0).edit().clear().commit();

View file

@ -0,0 +1,178 @@
package com.newsblur.util;
import android.content.ContentValues;
import android.database.Cursor;
import android.text.TextUtils;
import com.newsblur.database.BlurDatabaseHelper;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.network.domain.NewsBlurResponse;
import com.newsblur.network.APIManager;
import java.util.HashSet;
import java.util.Set;
public class ReadingAction {
private enum ActionType {
MARK_READ,
MARK_UNREAD,
SAVE,
UNSAVE
};
private ActionType type;
private String storyHash;
private FeedSet feedSet;
private Long olderThan;
private Long newerThan;
private ReadingAction() {
; // must use helpers
}
public static ReadingAction markStoryRead(String hash) {
ReadingAction ra = new ReadingAction();
ra.type = ActionType.MARK_READ;
ra.storyHash = hash;
return ra;
}
public static ReadingAction markStoryUnread(String hash) {
ReadingAction ra = new ReadingAction();
ra.type = ActionType.MARK_UNREAD;
ra.storyHash = hash;
return ra;
}
public static ReadingAction markFeedRead(FeedSet fs, Long olderThan, Long newerThan) {
ReadingAction ra = new ReadingAction();
ra.type = ActionType.MARK_READ;
ra.feedSet = fs;
ra.olderThan = olderThan;
ra.newerThan = newerThan;
return ra;
}
public ContentValues toContentValues() {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.ACTION_TIME, System.currentTimeMillis());
switch (type) {
case MARK_READ:
values.put(DatabaseConstants.ACTION_MARK_READ, 1);
if (storyHash != null) {
values.put(DatabaseConstants.ACTION_STORY_HASH, storyHash);
} else if (feedSet != null) {
values.put(DatabaseConstants.ACTION_FEED_ID, TextUtils.join(",", feedSet.getFeedIds()));
if (olderThan != null) values.put(DatabaseConstants.ACTION_INCLUDE_OLDER, olderThan);
if (newerThan != null) values.put(DatabaseConstants.ACTION_INCLUDE_NEWER, newerThan);
}
break;
case MARK_UNREAD:
values.put(DatabaseConstants.ACTION_MARK_UNREAD, 1);
if (storyHash != null) {
values.put(DatabaseConstants.ACTION_STORY_HASH, storyHash);
}
break;
default:
throw new IllegalStateException("cannot serialise uknown type of action.");
}
return values;
}
public static ReadingAction fromCursor(Cursor c) {
ReadingAction ra = new ReadingAction();
if (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_MARK_READ)) == 1) {
ra.type = ActionType.MARK_READ;
String hash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
String feedIds = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
Long includeOlder = DatabaseConstants.nullIfZero(c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_INCLUDE_OLDER)));
Long includeNewer = DatabaseConstants.nullIfZero(c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_INCLUDE_NEWER)));
if (hash != null) {
ra.storyHash = hash;
} else if (feedIds != null) {
Set<String> feeds = new HashSet<String>();
for (String feedId : TextUtils.split(feedIds, ",")) feeds.add(feedId);
if (feeds.size() == 0) {
ra.feedSet = FeedSet.allFeeds();
} else {
ra.feedSet = FeedSet.folder(null, feeds);
}
ra.olderThan = includeOlder;
ra.newerThan = includeNewer;
} else {
throw new IllegalStateException("cannot deserialise uknown type of action.");
}
} else if (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_MARK_UNREAD)) == 1) {
ra.type = ActionType.MARK_UNREAD;
String hash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
ra.storyHash = hash;
} else {
throw new IllegalStateException("cannot deserialise uknown type of action.");
}
return ra;
}
/**
* Execute this action remotely via the API.
*/
public NewsBlurResponse doRemote(APIManager apiManager) {
switch (type) {
case MARK_READ:
if (storyHash != null) {
return apiManager.markStoryAsRead(storyHash);
} else if (feedSet != null) {
if (feedSet.isAllNormal()) {
return apiManager.markAllAsRead();
} else if (feedSet.getFeedIds() != null) {
return apiManager.markFeedsAsRead(feedSet.getFeedIds(), olderThan, newerThan);
}
}
break;
case MARK_UNREAD:
if (storyHash != null) {
return apiManager.markStoryHashUnread(storyHash);
}
break;
default:
}
throw new IllegalStateException("cannot execute uknown type of action.");
}
/**
* Excecute this action on the local DB performing only idempotent sub-actions.
* Basically, double-check any local effects of this action that can be done safely.
*/
public void doLocalSecondary(BlurDatabaseHelper dbHelper) {
switch (type) {
case MARK_READ:
if (storyHash != null) {
dbHelper.setStoryReadState(storyHash, true);
} else if (feedSet != null) {
dbHelper.markFeedsRead_storyCounts(feedSet, olderThan, newerThan);
}
break;
case MARK_UNREAD:
if (storyHash != null) {
dbHelper.setStoryReadState(storyHash, false);
}
break;
default:
// not all actions have these, which is fine
}
}
}