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); + } + } + +}