New partial UI update system. Modularized sync services, starting with original text sync.

This commit is contained in:
dosiecki 2014-12-17 03:27:47 -08:00
parent bba15a8056
commit 6055895b72
9 changed files with 232 additions and 103 deletions

View file

@ -146,9 +146,9 @@ public abstract class ItemsList extends NbActivity implements StateChangedListen
protected abstract DefaultFeedView getDefaultFeedView();
@Override
public void handleUpdate() {
public void handleUpdate(boolean freshData) {
updateStatusIndicators();
if (itemListFragment != null) {
if (freshData && (itemListFragment != null)) {
itemListFragment.hasUpdated();
}
}

View file

@ -163,9 +163,9 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
}
@Override
public void handleUpdate() {
folderFeedList.hasUpdated();
public void handleUpdate(boolean freshData) {
updateStatusIndicators();
if (freshData) folderFeedList.hasUpdated();
}
private void updateStatusIndicators() {

View file

@ -108,14 +108,14 @@ public class NbActivity extends Activity {
* Called on each NB activity after the DB has been updated by the sync service. This method
* should return as quickly as possible.
*/
protected void handleUpdate() {
protected void handleUpdate(boolean freshData) {
Log.w(this.getClass().getName(), "activity doesn't implement handleUpdate");
}
private void _handleUpdate() {
private void _handleUpdate(final boolean freshData) {
runOnUiThread(new Runnable() {
public void run() {
handleUpdate();
handleUpdate(freshData);
}
});
}
@ -124,14 +124,19 @@ public class NbActivity extends Activity {
* Notify all activities in the app that the DB has been updated. Should only be called
* by the sync service, which owns updating the DB.
*/
public static void updateAllActivities() {
public static void updateAllActivities(boolean freshData) {
synchronized (AllActivities) {
for (NbActivity activity : AllActivities) {
activity._handleUpdate();
activity._handleUpdate(freshData);
}
}
}
public static void updateAllActivities() {
Log.w(NbActivity.class.getName(), "legacy handleUpdate used");
NbActivity.updateAllActivities(true);
}
/**
* Gets the number of active/foreground NB activities. Used by the sync service to
* determine if the app is active so we can honour user requests not to run in

View file

@ -326,9 +326,9 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
}
@Override
protected void handleUpdate() {
protected void handleUpdate(boolean freshData) {
enableMainProgress(NBSyncService.isFeedSetSyncing(this.fs));
updateCursor();
if (freshData) updateCursor();
}
private void updateCursor() {

View file

@ -58,7 +58,7 @@ public class DeleteFeedFragment extends DialogFragment {
// called from the feed view so finish
Activity activity = DeleteFeedFragment.this.getActivity();
if (activity instanceof Main) {
((Main)activity).handleUpdate();
((Main)activity).handleUpdate(true);
} else {
activity.finish();
}

View file

@ -23,12 +23,10 @@ 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.StoryTextResponse;
import com.newsblur.network.domain.UnreadStoryHashesResponse;
import com.newsblur.util.AppConstants;
import com.newsblur.util.DefaultFeedView;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.ImageCache;
import com.newsblur.util.NetworkUtils;
import com.newsblur.util.PrefsUtils;
@ -70,18 +68,12 @@ public class NBSyncService extends Service {
*/
public enum ActivationMode { ALL, OLDER, NEWER };
// this value is somewhat arbitrary. ideally we would wait the max network timeout, but
// the system like to force-kill terminating services that take too long, so it is often
// moot to tune.
private final static long SHUTDOWN_SLACK_SECONDS = 60L;
private volatile static boolean ActionsRunning = false;
private volatile static boolean CleanupRunning = false;
private volatile static boolean FFSyncRunning = false;
private volatile static boolean UnreadSyncRunning = false;
private volatile static boolean UnreadHashSyncRunning = false;
private volatile static boolean StorySyncRunning = false;
private volatile static boolean OriginalTextSyncRunning = false;
private volatile static boolean ImagePrefetchRunning = false;
private volatile static boolean DoFeedsFolders = false;
@ -118,21 +110,18 @@ public class NBSyncService extends Service {
private static Set<String> ImageQueue;
static { ImageQueue = new HashSet<String>(); }
/** Stories for which we want to fetch original text data. */
private static Set<String> OriginalTextQueue;
static { OriginalTextQueue = 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 Set<String> orphanFeedIds;
private PowerManager.WakeLock wl = null;
private ExecutorService primaryExecutor;
private ExecutorService secondaryExecutor;
private APIManager apiManager;
private BlurDatabaseHelper dbHelper;
private OriginalTextService originalTextService;
PowerManager.WakeLock wl = null;
APIManager apiManager;
BlurDatabaseHelper dbHelper;
private ImageCache imageCache;
private int lastStartIdCompleted = -1;
@ -144,12 +133,14 @@ public class NBSyncService extends Service {
PowerManager pm = (PowerManager) getApplicationContext().getSystemService(POWER_SERVICE);
wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getSimpleName());
wl.setReferenceCounted(true);
primaryExecutor = Executors.newFixedThreadPool(1);
secondaryExecutor = Executors.newFixedThreadPool(1);
apiManager = new APIManager(this);
PrefsUtils.checkForUpgrade(this);
dbHelper = new BlurDatabaseHelper(this);
imageCache = new ImageCache(this);
primaryExecutor = Executors.newFixedThreadPool(1);
originalTextService = new OriginalTextService(this);
}
/**
@ -185,7 +176,7 @@ public class NBSyncService extends Service {
try {
if (HaltNow) return;
wl.acquire();
incrementRunningChild();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "starting primary sync");
// check to see if we are on an allowable network only after ensuring we have CPU
@ -211,17 +202,15 @@ public class NBSyncService extends Service {
syncMetadata();
finishActions();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "finishing primary sync");
Runnable r = new Runnable() {
public void run() {
doSyncSecondary(startId);
}
};
secondaryExecutor.execute(r);
originalTextService.start(startId);
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "finishing primary sync");
} catch (Exception e) {
Log.e(this.getClass().getName(), "Sync error.", e);
} finally {
decrementRunningChild(startId);
}
}
@ -231,25 +220,13 @@ public class NBSyncService extends Service {
*/
private synchronized void doSyncSecondary(int startId) {
try {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "starting secondary sync");
// run one step below normal background priority
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_LESS_FAVORABLE);
syncUnreads();
syncOriginalTexts();
prefetchImages();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "finishing secondary sync");
} catch (Exception e) {
Log.e(this.getClass().getName(), "Sync error.", e);
} finally {
if (NbActivity.getActiveActivityCount() < 1) {
stopSelf(startId);
}
lastStartIdCompleted = startId;
if (wl != null) wl.release();
}
}
@ -308,7 +285,7 @@ public class NBSyncService extends Service {
if (FollowupActions.size() < 1) return;
for (ReadingAction ra : FollowupActions) {
ra.doLocal(dbHelper);
//ra.doLocal(dbHelper);
}
FollowupActions.clear();
}
@ -511,7 +488,7 @@ public class NBSyncService extends Service {
}
DefaultFeedView mode = PrefsUtils.getDefaultFeedViewForFeed(this, story.feedId);
if (mode == DefaultFeedView.TEXT) {
OriginalTextQueue.add(story.storyHash);
originalTextService.addHash(story.storyHash);
}
}
}
@ -523,43 +500,6 @@ public class NBSyncService extends Service {
}
}
private void syncOriginalTexts() {
try {
while (OriginalTextQueue.size() > 0) {
OriginalTextSyncRunning = true;
NbActivity.updateAllActivities();
Set<String> fetchedHashes = new HashSet<String>();
Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
batchloop: for (String hash : OriginalTextQueue) {
batch.add(hash);
if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
}
try {
fetchloop: for (String hash : batch) {
if (stopSync()) return;
String result = "";
StoryTextResponse response = apiManager.getStoryText(FeedUtils.inferFeedId(hash), hash);
if ((response != null) && (response.originalText != null)) {
result = response.originalText;
}
dbHelper.putStoryText(hash, result);
fetchedHashes.add(hash);
}
} finally {
OriginalTextQueue.removeAll(fetchedHashes);
}
}
} finally {
if (OriginalTextSyncRunning) {
OriginalTextSyncRunning = false;
NbActivity.updateAllActivities();
}
}
}
/**
* Fetch stories needed because the user is actively viewing a feed or folder.
*/
@ -665,7 +605,23 @@ public class NBSyncService extends Service {
return true;
}
private boolean stopSync() {
void incrementRunningChild() {
wl.acquire();
}
void decrementRunningChild(int startId) {
if (wl != null) wl.release();
// our wakelock reference counts. only stop the service if it is in the background and if
// we are the last thread to release the lock.
if (!wl.isHeld()) {
if (NbActivity.getActiveActivityCount() < 1) {
stopSelf(startId);
}
lastStartIdCompleted = startId;
}
}
boolean stopSync() {
if (HaltNow) {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "stopping sync, soft interrupt set.");
return true;
@ -690,7 +646,7 @@ public class NBSyncService extends Service {
* Is the main feed/folder list sync running?
*/
public static boolean isFeedFolderSyncRunning() {
return (ActionsRunning || FFSyncRunning || CleanupRunning || UnreadSyncRunning || StorySyncRunning || OriginalTextSyncRunning || ImagePrefetchRunning);
return (ActionsRunning || FFSyncRunning || CleanupRunning || UnreadSyncRunning || StorySyncRunning || OriginalTextService.running() || ImagePrefetchRunning);
}
/**
@ -708,7 +664,7 @@ public class NBSyncService extends Service {
if (UnreadHashSyncRunning) return "Syncing unread status . . .";
if (UnreadSyncRunning) return "Syncing " + StoryHashQueue.size() + " stories . . .";
if (ImagePrefetchRunning) return "Caching " + ImageQueue.size() + " images . . .";
if (OriginalTextSyncRunning) return "Syncing text for " + OriginalTextQueue.size() + " stories. . .";
if (OriginalTextService.running()) return "Syncing text for " + OriginalTextService.getPendingCount() + " stories. . .";
return null;
}
@ -778,7 +734,7 @@ public class NBSyncService extends Service {
}
public static void getOriginalText(String hash) {
OriginalTextQueue.add(hash);
OriginalTextService.addHash(hash);
}
public static void softInterrupt() {
@ -794,20 +750,14 @@ public class NBSyncService extends Service {
public void onDestroy() {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - stopping execution");
HaltNow = true;
originalTextService.shutdown();
primaryExecutor.shutdown();
secondaryExecutor.shutdown();
try {
primaryExecutor.awaitTermination(SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS);
primaryExecutor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
primaryExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
try {
secondaryExecutor.awaitTermination(SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
secondaryExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - execution halted");
super.onDestroy();

View file

@ -0,0 +1,71 @@
package com.newsblur.service;
import android.util.Log;
import com.newsblur.network.domain.StoryTextResponse;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedUtils;
import java.util.HashSet;
import java.util.Set;
public class OriginalTextService extends SubService {
/** story hashes we need to fetch (from newly found stories) */
private static Set<String> Hashes;
static {Hashes = new HashSet<String>();}
/** story hashes we should fetch ASAP (they are waiting on-screen) */
private static Set<String> PriorityHashes;
static {PriorityHashes = new HashSet<String>();}
public OriginalTextService(NBSyncService parent) {
super(parent);
}
@Override
protected void exec() {
while ((Hashes.size() > 0) || (PriorityHashes.size() > 0)) {
gotWork();
fetchBatch(PriorityHashes);
fetchBatch(Hashes);
}
}
private void fetchBatch(Set<String> hashes) {
Set<String> fetchedHashes = new HashSet<String>();
Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
batchloop: for (String hash : hashes) {
batch.add(hash);
if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
}
try {
fetchloop: for (String hash : batch) {
if (parent.stopSync()) return;
String result = "";
StoryTextResponse response = parent.apiManager.getStoryText(FeedUtils.inferFeedId(hash), hash);
if ((response != null) && (response.originalText != null)) {
result = response.originalText;
}
parent.dbHelper.putStoryText(hash, result);
fetchedHashes.add(hash);
}
} finally {
gotData();
hashes.removeAll(fetchedHashes);
}
}
public static void addHash(String hash) {
Hashes.add(hash);
}
public static void addPriorityHash(String hash) {
PriorityHashes.add(hash);
}
public static int getPendingCount() {
return (Hashes.size() + PriorityHashes.size());
}
}

View file

@ -0,0 +1,98 @@
package com.newsblur.service;
import android.os.Process;
import android.util.Log;
import com.newsblur.activity.NbActivity;
import com.newsblur.util.AppConstants;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
/**
* A utility construct to make NbSyncService a bit more modular by encapsulating sync tasks
* that can be run fully asynchronously from the main sync loop. Like all of the sync service,
* flags and data used by these modules need to be static so that parts of the app without a
* handle to the service object can access them.
*/
public abstract class SubService {
private static volatile boolean Running = false;
protected NBSyncService parent;
private ExecutorService executor;
private int startId;
private SubService() {
; // no default construction
}
SubService(NBSyncService parent) {
this.parent = parent;
executor = Executors.newFixedThreadPool(1);
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService created");
}
public void start(final int startId) {
this.startId = startId;
Runnable r = new Runnable() {
public void run() {
parent.incrementRunningChild();
if (NbActivity.getActiveActivityCount() < 1) {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_LESS_FAVORABLE );
} else {
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE + Process.THREAD_PRIORITY_LESS_FAVORABLE );
}
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService started");
exec_();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService completed");
parent.decrementRunningChild(startId);
}
};
executor.execute(r);
}
private synchronized void exec_() {
try {
exec();
} catch (Exception e) {
Log.e(this.getClass().getName(), "Sync error.", e);
} finally {
if (Running) {
Running = false;
NbActivity.updateAllActivities(false);
}
}
}
protected abstract void exec();
public void shutdown() {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService stopping");
executor.shutdown();
try {
executor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
} finally {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService stopped");
}
}
protected void gotWork() {
Running = true;
NbActivity.updateAllActivities(false);
}
protected void gotData() {
NbActivity.updateAllActivities(true);
}
public static boolean running() {
return Running;
}
}

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;
@ -28,7 +28,7 @@ public class AppConstants {
public static final String LAST_SYNC_TIME = "LAST_SYNC_TIME";
// how long to wait before auto-syncing the feed/folder list
public static final long AUTO_SYNC_TIME_MILLIS = 7L * 60L * 1000L;
public static final long AUTO_SYNC_TIME_MILLIS = 15L * 60L * 1000L;
// how often to trigger the BG service. slightly longer than how often we will find new stories,
// to account for the fact that it is approximate, and missing a cycle is bad.
@ -62,4 +62,9 @@ public class AppConstants {
// link to app feedback page
public static final String FEEDBACK_URL = "https://getsatisfaction.com/newsblur/topics/new?topic[style]=question&from=company&product=NewsBlur+Android+App&topic[additional_detail]=";
// how long to wait for sync threads to shutdown. ideally we would wait the max network timeout,
// but the system like to force-kill terminating services that take too long, so it is often
// moot to tune.
public final static long SHUTDOWN_SLACK_SECONDS = 60L;
}