diff --git a/api/src/com/todoroo/andlib/utility/AndroidUtilities.java b/api/src/com/todoroo/andlib/utility/AndroidUtilities.java
index 81cf3def7..53d708de8 100644
--- a/api/src/com/todoroo/andlib/utility/AndroidUtilities.java
+++ b/api/src/com/todoroo/andlib/utility/AndroidUtilities.java
@@ -510,16 +510,55 @@ public class AndroidUtilities {
* @param args arguments
* @return method return value, or null if nothing was called or exception
*/
- @SuppressWarnings("nls")
public static Object callApiMethod(int minSdk, Object receiver,
String methodName, Class>[] params, Object... args) {
if(getSdkVersion() < minSdk)
return null;
- Method method;
+ return AndroidUtilities.callMethod(receiver.getClass(),
+ receiver, methodName, params, args);
+ }
+
+ /**
+ * Call a static method via reflection if API level is at least minSdk
+ * @param minSdk minimum sdk number (i.e. 8)
+ * @param className fully qualified class to call method on
+ * @param methodName method name to call
+ * @param params method parameter types
+ * @param args arguments
+ * @return method return value, or null if nothing was called or exception
+ */
+ @SuppressWarnings("nls")
+ public static Object callApiStaticMethod(int minSdk, String className,
+ String methodName, Class>[] params, Object... args) {
+ if(getSdkVersion() < minSdk)
+ return null;
+
+ try {
+ return AndroidUtilities.callMethod(Class.forName(className),
+ null, methodName, params, args);
+ } catch (ClassNotFoundException e) {
+ getExceptionService().reportError("call-method", e);
+ return null;
+ }
+ }
+
+ /**
+ * Call a method via reflection
+ * @param class class to call method on
+ * @param receiver object to call method on (can be null)
+ * @param methodName method name to call
+ * @param params method parameter types
+ * @param args arguments
+ * @return method return value, or null if nothing was called or exception
+ */
+ @SuppressWarnings("nls")
+ public static Object callMethod(Class> cls, Object receiver,
+ String methodName, Class>[] params, Object... args) {
try {
- method = receiver.getClass().getMethod(methodName, params);
- return method.invoke(receiver, args);
+ Method method = cls.getMethod(methodName, params);
+ Object result = method.invoke(receiver, args);
+ return result;
} catch (SecurityException e) {
getExceptionService().reportError("call-method", e);
} catch (NoSuchMethodException e) {
diff --git a/astrid/AndroidManifest.xml b/astrid/AndroidManifest.xml
index 6fb0366cc..e13742870 100644
--- a/astrid/AndroidManifest.xml
+++ b/astrid/AndroidManifest.xml
@@ -331,12 +331,6 @@
-
-
-
-
-
-
diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncService.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncService.java
index 4382389d3..3e25f3e76 100644
--- a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncService.java
+++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncService.java
@@ -647,10 +647,11 @@ public final class ActFmSyncService {
/**
* Fetch all tags
* @param serverTime
+ * @return new serverTime
*/
- public void fetchTags(int serverTime) throws JSONException, IOException {
+ public int fetchTags(int serverTime) throws JSONException, IOException {
if(!checkForToken())
- return;
+ return 0;
JSONObject result = actFmInvoker.invoke("tag_list",
"token", token, "modified_after", serverTime);
@@ -666,6 +667,50 @@ public final class ActFmSyncService {
Long[] remoteIdArray = remoteIds.toArray(new Long[remoteIds.size()]);
tagDataService.deleteWhere(Criterion.not(TagData.REMOTE_ID.in(remoteIdArray)));
}
+
+ return result.optInt("time", 0);
+ }
+
+ /**
+ * Fetch active tasks asynchronously
+ * @param manual
+ * @param done
+ */
+ public void fetchActiveTasks(final boolean manual, Runnable done) {
+ invokeFetchList("task", manual, new ListItemProcessor() {
+ @Override
+ protected void mergeAndSave(JSONArray list, HashMap locals) throws JSONException {
+ Task remote = new Task();
+
+ ArrayList metadata = new ArrayList();
+ for(int i = 0; i < list.length(); i++) {
+ JSONObject item = list.getJSONObject(i);
+ readIds(locals, item, remote);
+ JsonHelper.taskFromJson(item, remote, metadata);
+
+ if(remote.getValue(Task.USER_ID) == 0) {
+ if(!remote.isSaved())
+ StatisticsService.reportEvent(StatisticsConstants.ACTFM_TASK_CREATED);
+ else if(remote.isCompleted())
+ StatisticsService.reportEvent(StatisticsConstants.ACTFM_TASK_COMPLETED);
+ }
+
+
+ Flags.set(Flags.ACTFM_SUPPRESS_SYNC);
+ taskService.save(remote);
+ metadataService.synchronizeMetadata(remote.getId(), metadata, MetadataCriteria.withKey(TagService.KEY));
+ remote.clear();
+ }
+ }
+
+ @Override
+ protected HashMap getLocalModels() {
+ TodorooCursor cursor = taskService.query(Query.select(Task.ID,
+ Task.REMOTE_ID).where(Task.REMOTE_ID.in(remoteIds)).orderBy(
+ Order.asc(Task.REMOTE_ID)));
+ return cursorToMap(cursor, taskDao, Task.REMOTE_ID, Task.ID);
+ }
+ }, done, "active_tasks");
}
/**
diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncV2Provider.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncV2Provider.java
new file mode 100644
index 000000000..4fc2132d0
--- /dev/null
+++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncV2Provider.java
@@ -0,0 +1,145 @@
+/**
+ * See the file "LICENSE" for the full license governing this code.
+ */
+package com.todoroo.astrid.actfm.sync;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.json.JSONException;
+
+import com.todoroo.andlib.data.TodorooCursor;
+import com.todoroo.andlib.service.Autowired;
+import com.todoroo.andlib.service.DependencyInjectionService;
+import com.todoroo.andlib.service.ExceptionService;
+import com.todoroo.andlib.sql.Criterion;
+import com.todoroo.andlib.sql.Query;
+import com.todoroo.andlib.utility.Preferences;
+import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
+import com.todoroo.astrid.data.Task;
+import com.todoroo.astrid.service.AstridDependencyInjector;
+import com.todoroo.astrid.service.StartupService;
+import com.todoroo.astrid.service.SyncV2Service.SyncResultCallback;
+import com.todoroo.astrid.service.SyncV2Service.SyncV2Provider;
+import com.todoroo.astrid.service.TaskService;
+
+/**
+ * Exposes sync action
+ *
+ */
+public class ActFmSyncV2Provider implements SyncV2Provider {
+
+ @Autowired ActFmPreferenceService actFmPreferenceService;
+
+ @Autowired ActFmSyncService actFmSyncService;
+
+ @Autowired ExceptionService exceptionService;
+
+ @Autowired TaskService taskService;
+
+ static {
+ AstridDependencyInjector.initialize();
+ }
+
+ public ActFmSyncV2Provider() {
+ DependencyInjectionService.getInstance().inject(this);
+ }
+
+ @Override
+ public boolean isActive() {
+ return actFmPreferenceService.isLoggedIn();
+ }
+
+ private static final String LAST_TAG_FETCH_TIME = "actfm_lastTag"; //$NON-NLS-1$
+
+ // --- synchronize active
+
+ @Override
+ public void synchronizeActiveTasks(boolean manual,
+ final SyncResultCallback callback) {
+
+ if(!manual)
+ return;
+
+ callback.started();
+ callback.incrementMax(50);
+
+ final AtomicInteger finisher = new AtomicInteger(2);
+
+ new Thread(tagFetcher(callback, finisher)).start();
+
+ startTaskFetcher(manual, callback, finisher);
+
+ pushQueued(callback, finisher);
+ }
+
+ private Runnable tagFetcher(final SyncResultCallback callback,
+ final AtomicInteger finisher) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ int time = Preferences.getInt(LAST_TAG_FETCH_TIME, 0);
+ try {
+ time = actFmSyncService.fetchTags(time);
+ Preferences.setInt(LAST_TAG_FETCH_TIME, time);
+ } catch (JSONException e) {
+ exceptionService.reportError("actfm-sync", e); //$NON-NLS-1$
+ } catch (IOException e) {
+ exceptionService.reportError("actfm-sync", e); //$NON-NLS-1$
+ } finally {
+ callback.incrementProgress(20);
+ if(finisher.decrementAndGet() == 0)
+ callback.finished();
+ }
+ }
+ };
+ }
+
+ private void startTaskFetcher(final boolean manual, final SyncResultCallback callback,
+ final AtomicInteger finisher) {
+ actFmSyncService.fetchActiveTasks(manual, new Runnable() {
+ @Override
+ public void run() {
+ callback.incrementProgress(30);
+ if(finisher.decrementAndGet() == 0)
+ callback.finished();
+ }
+ });
+ }
+
+ private void pushQueued(final SyncResultCallback callback,
+ final AtomicInteger finisher) {
+ TodorooCursor cursor = taskService.query(Query.select(Task.PROPERTIES).
+ where(Criterion.or(
+ Criterion.and(TaskCriteria.isActive(),
+ Task.ID.gt(StartupService.INTRO_TASK_SIZE),
+ Task.REMOTE_ID.eq(0)),
+ Criterion.and(Task.REMOTE_ID.gt(0),
+ Task.MODIFICATION_DATE.gt(Task.LAST_SYNC)))));
+
+ try {
+ callback.incrementMax(cursor.getCount() * 20);
+ finisher.addAndGet(cursor.getCount());
+
+ for(int i = 0; i < cursor.getCount(); i++) {
+ cursor.moveToNext();
+ final Task task = new Task(cursor);
+
+ new Thread(new Runnable() {
+ public void run() {
+ try {
+ actFmSyncService.pushTaskOnSave(task, task.getMergedValues());
+ } finally {
+ callback.incrementProgress(20);
+ if(finisher.decrementAndGet() == 0)
+ callback.finished();
+ }
+ }
+ }).start();
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+}
diff --git a/astrid/res/layout/task_list_activity.xml b/astrid/res/layout/task_list_activity.xml
index fd58f4c46..27d9c841f 100644
--- a/astrid/res/layout/task_list_activity.xml
+++ b/astrid/res/layout/task_list_activity.xml
@@ -86,6 +86,14 @@
+
+
diff --git a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java
index e0b254b88..277deab0d 100644
--- a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java
+++ b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java
@@ -42,6 +42,7 @@ import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
+import android.view.animation.AlphaAnimation;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
@@ -53,6 +54,7 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.PopupWindow.OnDismissListener;
+import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;
@@ -74,8 +76,6 @@ import com.todoroo.andlib.utility.Preferences;
import com.todoroo.andlib.widget.GestureService;
import com.todoroo.andlib.widget.GestureService.GestureInterface;
import com.todoroo.astrid.actfm.ActFmLoginActivity;
-import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
-import com.todoroo.astrid.actfm.sync.ActFmSyncProvider;
import com.todoroo.astrid.activity.SortSelectionActivity.OnSortSelectedListener;
import com.todoroo.astrid.adapter.TaskAdapter;
import com.todoroo.astrid.adapter.TaskAdapter.OnCompletedTaskListener;
@@ -105,6 +105,8 @@ import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.StartupService;
import com.todoroo.astrid.service.StatisticsConstants;
import com.todoroo.astrid.service.StatisticsService;
+import com.todoroo.astrid.service.SyncV2Service;
+import com.todoroo.astrid.service.SyncV2Service.SyncResultCallback;
import com.todoroo.astrid.service.TagDataService;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.service.ThemeService;
@@ -163,8 +165,6 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
public static final String TOKEN_OVERRIDE_ANIM = "finishAnim"; //$NON-NLS-1$
- private static final String LAST_AUTOSYNC_ATTEMPT = "last-autosync"; //$NON-NLS-1$
-
// --- instance variables
@Autowired ExceptionService exceptionService;
@@ -179,7 +179,7 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
@Autowired UpgradeService upgradeService;
- @Autowired ActFmPreferenceService actFmPreferenceService;
+ @Autowired protected SyncV2Service syncService;
@Autowired TagDataService tagDataService;
@@ -239,7 +239,7 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
new StartupService().onStartupApplication(this);
ThemeService.applyTheme(this);
ViewGroup parent = (ViewGroup) getLayoutInflater().inflate(R.layout.task_list_activity, null);
- parent.addView(getListBody(parent), 1);
+ parent.addView(getListBody(parent), 2);
setContentView(parent);
if(database == null)
@@ -502,23 +502,6 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DITHER);
}
- private void initiateAutomaticSync() {
- if (!actFmPreferenceService.isLoggedIn()) return;
- long lastFetchDate = actFmPreferenceService.getLastSyncDate();
- long lastAutosyncAttempt = Preferences.getLong(LAST_AUTOSYNC_ATTEMPT, 0);
-
- long lastTry = Math.max(lastFetchDate, lastAutosyncAttempt);
- if(DateUtilities.now() < lastTry + 300000L)
- return;
- new Thread() {
- @Override
- public void run() {
- Preferences.setLong(LAST_AUTOSYNC_ATTEMPT, DateUtilities.now());
- new ActFmSyncProvider().synchronize(TaskListActivity.this, false);
- }
- }.start();
- }
-
// Subclasses can override these to customize extras in quickadd intent
protected Intent getOnClickQuickAddIntent(Task t) {
Intent intent = new Intent(TaskListActivity.this, TaskEditActivity.class);
@@ -1136,8 +1119,94 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
}
}
+ private class TaskListSyncResultCallback implements SyncResultCallback {
+
+ private final ProgressBar progressBar;
+ private int providers = 0;
+
+ public TaskListSyncResultCallback() {
+ progressBar = (ProgressBar) findViewById(R.id.progressBar);
+ progressBar.setProgress(0);
+ progressBar.setMax(0);
+ }
+
+ @Override
+ public void finished() {
+ providers--;
+ if(providers == 0) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ progressBar.setMax(100);
+ progressBar.setProgress(100);
+ AlphaAnimation animation = new AlphaAnimation(1, 0);
+ animation.setFillAfter(true);
+ animation.setDuration(1000L);
+ progressBar.startAnimation(animation);
+
+ loadTaskListContent(true);
+ }
+ });
+ new Thread() {
+ @Override
+ public void run() {
+ AndroidUtilities.sleepDeep(1000);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ progressBar.setVisibility(View.GONE);
+ }
+ });
+ }
+ }.start();
+ }
+ }
+
+ @Override
+ public void incrementMax(final int incrementBy) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ progressBar.setMax(progressBar.getMax() + incrementBy);
+ }
+ });
+ }
+
+ @Override
+ public void incrementProgress(final int incrementBy) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ progressBar.incrementProgressBy(incrementBy);
+ }
+ });
+ }
+
+ @Override
+ public void started() {
+ if(providers == 0) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ progressBar.setVisibility(View.VISIBLE);
+ AlphaAnimation animation = new AlphaAnimation(0, 1);
+ animation.setFillAfter(true);
+ animation.setDuration(1000L);
+ progressBar.startAnimation(animation);
+ }
+ });
+ }
+
+ providers++;
+ }
+ }
+
+ private void initiateAutomaticSync() {
+ syncService.synchronizeActiveTasks(false, new TaskListSyncResultCallback());
+ }
+
private void performSyncAction() {
- if (syncActions.size() == 0) {
+ if (syncActions.size() == 0 && !syncService.isActive()) {
String desiredCategory = getString(R.string.SyP_label);
// Get a list of all sync plugins and bring user to the prefs pane
@@ -1178,32 +1247,20 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
showSyncOptionMenu(actions, listener);
}
- else if(syncActions.size() == 1) {
- SyncAction syncAction = syncActions.iterator().next();
- try {
- syncAction.intent.send();
- Toast.makeText(this, R.string.SyP_progress_toast,
- Toast.LENGTH_LONG).show();
- } catch (CanceledException e) {
- //
- }
- } else {
- // We have >1 sync actions, pop up a dialogue so the user can
- // select just one of them (only sync one at a time)
- final SyncAction[] actions = syncActions.toArray(new SyncAction[syncActions.size()]);
- DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface click, int which) {
+ else {
+ syncService.synchronizeActiveTasks(true, new TaskListSyncResultCallback());
+
+ if(syncActions.size() > 0) {
+ for(SyncAction syncAction : syncActions) {
try {
- actions[which].intent.send();
- Toast.makeText(TaskListActivity.this, R.string.SyP_progress_toast,
- Toast.LENGTH_LONG).show();
+ syncAction.intent.send();
} catch (CanceledException e) {
//
}
}
- };
- showSyncOptionMenu(actions, listener);
+ Toast.makeText(TaskListActivity.this, R.string.SyP_progress_toast,
+ Toast.LENGTH_LONG).show();
+ }
}
}
diff --git a/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java b/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java
index bc355cebf..b849a3783 100644
--- a/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java
+++ b/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java
@@ -73,6 +73,7 @@ public class AstridDependencyInjector extends AbstractDependencyInjector {
injectables.put("tagDataService", TagDataService.class);
injectables.put("upgradeService", UpgradeService.class);
injectables.put("addOnService", AddOnService.class);
+ injectables.put("syncService", SyncV2Service.class);
// com.timsu.astrid.data
injectables.put("tasksTable", "tasks");
diff --git a/astrid/src/com/todoroo/astrid/service/SyncV2Service.java b/astrid/src/com/todoroo/astrid/service/SyncV2Service.java
new file mode 100644
index 000000000..54b156495
--- /dev/null
+++ b/astrid/src/com/todoroo/astrid/service/SyncV2Service.java
@@ -0,0 +1,78 @@
+package com.todoroo.astrid.service;
+
+import com.todoroo.astrid.actfm.sync.ActFmSyncV2Provider;
+
+/**
+ * SyncV2Service is a simplified synchronization interface for supporting
+ * next-generation sync interfaces such as Google Tasks and Astrid.com
+ *
+ * @author Tim Su
+ *
+ */
+public class SyncV2Service {
+
+ public interface SyncResultCallback {
+ /**
+ * Increment max sync progress
+ * @param incrementBy
+ */
+ public void incrementMax(int incrementBy);
+
+ /**
+ * Increment current sync progress
+ * @param incrementBy
+ */
+ public void incrementProgress(int incrementBy);
+
+ /**
+ * Provider started sync
+ */
+ public void started();
+
+ /**
+ * Provider finished sync
+ */
+ public void finished();
+ }
+
+ public interface SyncV2Provider {
+ public boolean isActive();
+ public void synchronizeActiveTasks(boolean manual, SyncResultCallback callback);
+ }
+
+ /*
+ * At present, sync provider interactions are handled through code. If
+ * there is enough interest, the Astrid team could create an interface
+ * for responding to sync requests through this new API.
+ */
+ private final SyncV2Provider[] providers = new SyncV2Provider[] {
+ new ActFmSyncV2Provider()
+ };
+
+ /**
+ * Determine if synchronization is available
+ *
+ * @param callback
+ */
+ public boolean isActive() {
+ for(SyncV2Provider provider : providers) {
+ if(provider.isActive())
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Initiate synchronization of active tasks
+ *
+ * @param manual if manual sync
+ * @param callback result callback
+ */
+ public void synchronizeActiveTasks(boolean manual, SyncResultCallback callback) {
+ for(SyncV2Provider provider : providers) {
+ if(provider.isActive())
+ provider.synchronizeActiveTasks(manual, callback);
+ }
+ }
+
+}