Replace old sync thread with sync service.

This commit is contained in:
dosiecki 2014-06-02 13:43:49 -07:00
parent a710bb3128
commit 3f1c1bfd5d
10 changed files with 106 additions and 346 deletions

View file

@ -122,8 +122,6 @@
<activity
android:name=".activity.SocialFeedReading"/>
<service android:name=".service.SyncService" />
<service android:name=".service.NBSyncService" />
<receiver android:name=".service.BootReceiver">

View file

@ -15,20 +15,17 @@ import android.view.Window;
import com.newsblur.R;
import com.newsblur.fragment.FolderListFragment;
import com.newsblur.fragment.LogoutDialogFragment;
import com.newsblur.fragment.SyncUpdateFragment;
import com.newsblur.service.BootReceiver;
import com.newsblur.service.SyncService;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.view.StateToggleButton.StateChangedListener;
public class Main extends NbActivity implements StateChangedListener, SyncUpdateFragment.SyncUpdateFragmentInterface {
public class Main extends NbActivity implements StateChangedListener {
private ActionBar actionBar;
private FolderListFragment folderFeedList;
private FragmentManager fragmentManager;
private SyncUpdateFragment syncFragment;
private static final String TAG = "MainActivity";
private Menu menu;
@ -48,18 +45,9 @@ public class Main extends NbActivity implements StateChangedListener, SyncUpdate
fragmentManager = getFragmentManager();
folderFeedList = (FolderListFragment) fragmentManager.findFragmentByTag("folderFeedListFragment");
folderFeedList.setRetainInstance(true);
syncFragment = (SyncUpdateFragment) fragmentManager.findFragmentByTag(SyncUpdateFragment.TAG);
if (syncFragment == null) {
syncFragment = new SyncUpdateFragment();
fragmentManager.beginTransaction().add(syncFragment, SyncUpdateFragment.TAG).commit();
// for our first sync, don't just trigger a heavyweight refresh, do it in two steps
// so the UI appears more quickly (per the docs at newsblur.com/api)
if (PrefsUtils.isTimeToAutoSync(this)) {
triggerFirstSync();
}
}
//also make sure the interval sync is scheduled, in case it was just now enabled
BootReceiver.scheduleSyncService(this);
}
@Override
@ -71,40 +59,11 @@ public class Main extends NbActivity implements StateChangedListener, SyncUpdate
// clear all stories from the DB, the story activities will load them.
FeedUtils.clearStories(this);
//trigger an autosync right now, just in case
Intent i = new Intent(this, NBSyncService.class);
startService(i);
//also make sure the interval sync is scheduled, in case it was just now enabled
BootReceiver.scheduleSyncService(this);
}
/**
* Triggers an initial two-phase sync, so the UI can display quickly using /reader/feeds and
* then call /reader/refresh_feeds to get updated counts.
*/
private void triggerFirstSync() {
PrefsUtils.updateLastSyncTime(this);
setProgressBarIndeterminateVisibility(true);
setRefreshEnabled(false);
final Intent intent = new Intent(Intent.ACTION_SYNC, null, this, SyncService.class);
intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, syncFragment.receiver);
intent.putExtra(SyncService.EXTRA_TASK_TYPE, SyncService.TaskType.FOLDER_UPDATE_TWO_STEP);
startService(intent);
}
/**
* Triggers a full, manually requested refresh of feed/folder data and counts.
*/
private void triggerRefresh() {
PrefsUtils.updateLastSyncTime(this);
setProgressBarIndeterminateVisibility(true);
setRefreshEnabled(false);
final Intent intent = new Intent(Intent.ACTION_SYNC, null, this, SyncService.class);
intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, syncFragment.receiver);
intent.putExtra(SyncService.EXTRA_TASK_TYPE, SyncService.TaskType.FOLDER_UPDATE_WITH_COUNT);
startService(intent);
Intent i = new Intent(this, NBSyncService.class);
startService(i);
}
private void setupActionBar() {
@ -156,39 +115,18 @@ public class Main extends NbActivity implements StateChangedListener, SyncUpdate
}
}
/**
* Called after the sync service completely finishes a task.
*/
@Override
public void updateAfterSync() {
public void handleUpdate() {
folderFeedList.hasUpdated();
setProgressBarIndeterminateVisibility(false);
setRefreshEnabled(true);
}
/**
* Called when the sync service has made enough progress to update the UI but not
* enough to stop the progress indicator.
*/
@Override
public void updatePartialSync() {
// TODO: move 2-step sync to new async lib and remove this method entirely
// folderFeedList.hasUpdated();
}
@Override
public void updateSyncStatus(boolean syncRunning) {
// TODO: the progress bar is activated manually elsewhere in this activity. this
// interface method may be redundant.
if (syncRunning) {
setProgressBarIndeterminateVisibility(true);
if (NBSyncService.isSyncRunning()) {
setProgressBarIndeterminateVisibility(true);
setRefreshEnabled(false);
}
} else {
setProgressBarIndeterminateVisibility(false);
setRefreshEnabled(true);
}
}
@Override
public void setNothingMoreToUpdate() { }
private void setRefreshEnabled(boolean enabled) {
if (menu != null) {
MenuItem item = menu.findItem(R.id.menu_refresh);

View file

@ -7,10 +7,19 @@ import android.util.Log;
import com.newsblur.util.AppConstants;
import com.newsblur.util.PrefsUtils;
import java.util.ArrayList;
public class NbActivity extends Activity {
private final static String UNIQUE_LOGIN_KEY = "uniqueLoginKey";
private String uniqueLoginKey;
/**
* Keep track of all activie activities so they can be notified when the sync service
* has updated the DB. This is essentially an ultra-lightweight implementation of a
* local, unfiltered broadcast manager.
*/
private static ArrayList<NbActivity> AllActivities = new ArrayList<NbActivity>();
@Override
protected void onCreate(Bundle bundle) {
@ -31,6 +40,20 @@ public class NbActivity extends Activity {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onResume");
super.onResume();
finishIfNotLoggedIn();
synchronized (AllActivities) {
AllActivities.add(this);
}
}
@Override
protected void onPause() {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onSuspend");
super.onPause();
synchronized (AllActivities) {
AllActivities.remove(this);
}
}
protected void finishIfNotLoggedIn() {
@ -48,4 +71,31 @@ public class NbActivity extends Activity {
super.onSaveInstanceState(savedInstanceState);
}
/**
* 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() {;}
private void _handleUpdate() {
runOnUiThread(new Runnable() {
public void run() {
handleUpdate();
}
});
}
/**
* Notify all activities in the app that the DB has been updated.
*/
public static void updateAllActivities() {
Log.d(NbActivity.class.getName(), "updating all activities . . .");
synchronized (AllActivities) {
for (NbActivity activity : AllActivities) {
activity._handleUpdate();
}
}
Log.d(NbActivity.class.getName(), " . . . done updating all activities");
}
}

View file

@ -11,7 +11,6 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -35,20 +34,10 @@ public class DeleteFeedFragment extends DialogFragment {
return frag;
}
private FragmentManager fragmentManager;
private SyncUpdateFragment syncFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
setStyle(DialogFragment.STYLE_NO_TITLE, R.style.dialog);
super.onCreate(savedInstanceState);
fragmentManager = super.getFragmentManager();
syncFragment = (SyncUpdateFragment) fragmentManager.findFragmentByTag(SyncUpdateFragment.TAG);
if (syncFragment == null) {
syncFragment = new SyncUpdateFragment();
}
}
@Override
@ -66,7 +55,7 @@ public class DeleteFeedFragment extends DialogFragment {
// called from the feed view so finish
Activity activity = DeleteFeedFragment.this.getActivity();
if (activity instanceof Main) {
((Main)activity).updateAfterSync();
((Main)activity).handleUpdate();
} else {
activity.finish();
}

View file

@ -20,7 +20,6 @@ import com.newsblur.R;
import com.newsblur.activity.LoginProgress;
import com.newsblur.activity.RegisterProgress;
import com.newsblur.network.APIManager;
import com.newsblur.service.DetachableResultReceiver;
public class LoginRegisterFragment extends Fragment implements OnClickListener {
@ -28,10 +27,8 @@ public class LoginRegisterFragment extends Fragment implements OnClickListener {
private EditText username, password;
private ViewSwitcher viewSwitcher;
DetachableResultReceiver receiver;
private EditText register_username, register_password, register_email;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.fragment_loginregister, container, false);

View file

@ -1,81 +0,0 @@
package com.newsblur.fragment;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.app.Fragment;
import android.util.Log;
import com.newsblur.service.DetachableResultReceiver;
import com.newsblur.service.DetachableResultReceiver.Receiver;
import com.newsblur.service.SyncService;
public class SyncUpdateFragment extends Fragment implements Receiver {
public static final String TAG = SyncUpdateFragment.class.getName();
public DetachableResultReceiver receiver;
public boolean syncRunning = false;
public SyncUpdateFragment() {
receiver = new DetachableResultReceiver(new Handler());
receiver.setReceiver(this);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
}
@Override
public void onReceiverResult(int resultCode, Bundle resultData) {
final SyncService.SyncStatus resultStatus = SyncService.SyncStatus.values()[resultCode];
switch (resultStatus) {
case STATUS_FINISHED:
syncRunning = false;
if (getActivity() != null) {
((SyncUpdateFragmentInterface) getActivity()).updateAfterSync();
}
break;
case STATUS_PARTIAL_PROGRESS:
syncRunning = true;
if (getActivity() != null) {
((SyncUpdateFragmentInterface) getActivity()).updatePartialSync();
}
break;
case STATUS_RUNNING:
syncRunning = true;
break;
case STATUS_NO_MORE_UPDATES:
syncRunning = false;
if (getActivity() != null) {
((SyncUpdateFragmentInterface) getActivity()).setNothingMoreToUpdate();
}
break;
default:
syncRunning = false;
Log.e(this.getClass().getName(), "Sync completed with an unknown result.");
break;
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
((SyncUpdateFragmentInterface) getActivity()).updateSyncStatus(syncRunning);
}
public interface SyncUpdateFragmentInterface {
public void updateAfterSync();
public void updatePartialSync();
public void setNothingMoreToUpdate();
public void updateSyncStatus(boolean syncRunning);
}
}

View file

@ -1,42 +0,0 @@
package com.newsblur.service;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.util.Log;
/**
* This class is based on the Google I/O 2011 app's class of the same name. It allows for
* a resultreceiver to attach/detach to an activity and thus elegantly handle configuration changes.
*/
public class DetachableResultReceiver extends ResultReceiver {
private final static String TAG = "DetachableResultReceiver";
private Receiver receiver;
public DetachableResultReceiver(Handler handler) {
super(handler);
}
public void clearReceiver() {
receiver = null;
}
public void setReceiver(final Receiver resultReceiver) {
receiver = resultReceiver;
}
public interface Receiver {
public void onReceiverResult(int resultCode, Bundle resultData);
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (receiver != null) {
receiver.onReceiverResult(resultCode, resultData);
} else {
Log.w(TAG, "No receiver to handle result: " + resultCode + " " + resultData.toString());
}
}
}

View file

@ -7,41 +7,73 @@ import android.os.PowerManager;
import android.util.Log;
import com.newsblur.R;
import com.newsblur.activity.NbActivity;
import com.newsblur.network.APIManager;
public class NBSyncService extends Service {
private static boolean SyncRunning = false;
private APIManager apiManager;
@Override
public void onCreate() {
super.onCreate();
Log.d(this.getClass().getName(), "onCreate");
apiManager = new APIManager(this);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(this.getClass().getName(), "onStartCommand");
// TODO: check for sync flag or realtime flag
PowerManager pm = (PowerManager) getApplicationContext().getSystemService(POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getSimpleName());
wl.acquire();
new Thread(new Runnable() {
public void run() {
doSync();
}
}).start();
// TODO: stuff!
wl.release();
Log.d(this.getClass().getName(), "onStartCommand complete");
return Service.START_NOT_STICKY;
}
private synchronized void doSync() {
Log.d(this.getClass().getName(), "starting sync . . .");
PowerManager pm = (PowerManager) getApplicationContext().getSystemService(POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getSimpleName());
try {
wl.acquire();
SyncRunning = true;
NbActivity.updateAllActivities();
apiManager.getFolderFeedMapping(true);
SyncRunning = false;
NbActivity.updateAllActivities();
} finally {
wl.release();
Log.d(this.getClass().getName(), " . . . sync done");
}
}
public static boolean isSyncRunning() {
return SyncRunning;
}
@Override
public void onDestroy() {
Log.d(this.getClass().getName(), "onDestroy");
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

View file

@ -1,120 +0,0 @@
package com.newsblur.service;
import java.util.List;
import android.app.IntentService;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.newsblur.R;
import com.newsblur.database.FeedProvider;
import com.newsblur.domain.ValueMultimap;
import com.newsblur.network.APIConstants;
import com.newsblur.network.APIManager;
import com.newsblur.network.domain.SocialFeedResponse;
import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.util.ReadFilter;
import com.newsblur.util.StoryOrder;
/**
* A background-sync intent that, by virtue of extending IntentService, has several notable
* features:
* * invocations are FIFO and executed serially
* * the OS picks an appropriate thread for execution that won't block the UI, but recycles
* * supports callbacks where necessary
*/
public class SyncService extends IntentService {
public static final String EXTRA_TASK_TYPE = "syncServiceTaskType";
public static final String EXTRA_STATUS_RECEIVER = "resultReceiverExtra";
public enum SyncStatus {
STATUS_RUNNING,
STATUS_FINISHED,
STATUS_NO_MORE_UPDATES,
NOT_RUNNING,
STATUS_PARTIAL_PROGRESS,
};
public enum TaskType {
FOLDER_UPDATE_TWO_STEP,
FOLDER_UPDATE_WITH_COUNT
};
private APIManager apiManager;
private ContentResolver contentResolver;
public SyncService() {
super(SyncService.class.getName());
}
@Override
public void onCreate() {
super.onCreate();
apiManager = new APIManager(this);
contentResolver = getContentResolver();
}
@Override
protected void onHandleIntent(Intent intent) {
final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_STATUS_RECEIVER);
try {
TaskType taskType = (TaskType) intent.getSerializableExtra(EXTRA_TASK_TYPE);
Log.d( this.getClass().getName(), "Sync Intent: " + taskType );
if (receiver != null) {
receiver.send(SyncStatus.STATUS_RUNNING.ordinal(), Bundle.EMPTY);
}
// an extra result code to callback before the final STATUS_FINISHED that is always sent
SyncStatus resultStatus = null;
switch (taskType) {
case FOLDER_UPDATE_TWO_STEP:
// do a quick fetch of folders/feeds
apiManager.getFolderFeedMapping(false);
// notify UI of progress
if (receiver != null) {
receiver.send(SyncStatus.STATUS_PARTIAL_PROGRESS.ordinal(), Bundle.EMPTY);
}
// update feed counts
apiManager.refreshFeedCounts();
// UI will be notified again by default
break;
case FOLDER_UPDATE_WITH_COUNT:
apiManager.getFolderFeedMapping(true);
break;
default:
Log.e(this.getClass().getName(), "SyncService called without relevant task assignment");
break;
}
// send the first result code if it was set. The STATUS_FINISHED is sent below
if ((receiver != null) && (resultStatus != null)) {
receiver.send(resultStatus.ordinal(), Bundle.EMPTY);
}
Log.d( this.getClass().getName(), "Sync Intent complete");
} catch (Exception e) {
Log.e(this.getClass().getName(), "Couldn't synchronise with NewsBlur servers: " + e.getMessage(), e.getCause());
e.printStackTrace();
} finally {
if (receiver != null) {
receiver.send(SyncStatus.STATUS_FINISHED.ordinal(), Bundle.EMPTY);
}
}
}
}

View file

@ -32,7 +32,6 @@ import com.newsblur.network.APIManager;
import com.newsblur.network.domain.NewsBlurResponse;
import com.newsblur.network.domain.SocialFeedResponse;
import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.service.SyncService;
import com.newsblur.util.AppConstants;
public class FeedUtils {