diff --git a/api/src/com/todoroo/andlib/utility/TodorooPreferenceActivity.java b/api/src/com/todoroo/andlib/utility/TodorooPreferenceActivity.java index f857247dd..d59edeb01 100644 --- a/api/src/com/todoroo/andlib/utility/TodorooPreferenceActivity.java +++ b/api/src/com/todoroo/andlib/utility/TodorooPreferenceActivity.java @@ -71,12 +71,14 @@ abstract public class TodorooPreferenceActivity extends PreferenceActivity { updatePreferences(preference, value); - preference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { - public boolean onPreferenceChange(Preference myPreference, Object newValue) { - updatePreferences(myPreference, newValue); - return true; - } - }); + if (preference.getOnPreferenceChangeListener() == null) { + preference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference myPreference, Object newValue) { + updatePreferences(myPreference, newValue); + return true; + } + }); + } } } diff --git a/api/src/com/todoroo/astrid/sync/SyncMetadataService.java b/api/src/com/todoroo/astrid/sync/SyncMetadataService.java index 81c02b5cc..d270efb2a 100644 --- a/api/src/com/todoroo/astrid/sync/SyncMetadataService.java +++ b/api/src/com/todoroo/astrid/sync/SyncMetadataService.java @@ -97,8 +97,9 @@ abstract public class SyncMetadataService { if(lastSyncDate == 0) tasks = taskDao.query(Query.select(Task.ID).where(Criterion.none)); else - tasks = taskDao.query(Query.select(Task.ID).where(Task.MODIFICATION_DATE. - gt(lastSyncDate)).orderBy(Order.asc(Task.ID))); + tasks = taskDao.query(Query.select(Task.ID).where(Criterion.and( + Task.MODIFICATION_DATE.gt(lastSyncDate), + Task.LAST_SYNC.lte(lastSyncDate))).orderBy(Order.asc(Task.ID))); return joinWithMetadata(tasks, true, properties); } diff --git a/api/src/com/todoroo/astrid/sync/SyncProviderUtilities.java b/api/src/com/todoroo/astrid/sync/SyncProviderUtilities.java index 6624f9cca..b6a020117 100644 --- a/api/src/com/todoroo/astrid/sync/SyncProviderUtilities.java +++ b/api/src/com/todoroo/astrid/sync/SyncProviderUtilities.java @@ -30,7 +30,7 @@ abstract public class SyncProviderUtilities { protected static final String PREF_LAST_ATTEMPTED_SYNC = "_last_attempted"; //$NON-NLS-1$ - protected static final String PREF_LAST_ERROR = "_last_error"; //$NON-NLS-1$ + protected static final String PREF_LAST_ERROR = "_last_err"; //$NON-NLS-1$ protected static final String PREF_ONGOING = "_ongoing"; //$NON-NLS-1$ @@ -70,7 +70,7 @@ abstract public class SyncProviderUtilities { /** @return Last Error, or null if no last error */ public String getLastError() { - return getPrefs().getString(PREF_LAST_ERROR, null); + return getPrefs().getString(getIdentifier() + PREF_LAST_ERROR, null); } /** @return Last Error, or null if no last error */ @@ -112,7 +112,7 @@ abstract public class SyncProviderUtilities { Editor editor = getPrefs().edit(); editor.putLong(getIdentifier() + PREF_LAST_ATTEMPTED_SYNC, DateUtilities.now()); - editor.putString(getIdentifier() + PREF_LAST_ERROR, null); + editor.remove(getIdentifier() + PREF_LAST_ERROR); editor.putBoolean(getIdentifier() + PREF_ONGOING, true); editor.commit(); } diff --git a/astrid/plugin-src/com/timsu/astrid/C2DMReceiver.java b/astrid/plugin-src/com/timsu/astrid/C2DMReceiver.java index 468adb462..1340995ac 100644 --- a/astrid/plugin-src/com/timsu/astrid/C2DMReceiver.java +++ b/astrid/plugin-src/com/timsu/astrid/C2DMReceiver.java @@ -91,7 +91,7 @@ public class C2DMReceiver extends BroadcastReceiver { TagData tagData = new TagData(); if(cursor.getCount() == 0) { tagData.setValue(Task.REMOTE_ID, Long.parseLong(intent.getStringExtra("tag_id"))); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); tagDataService.save(tagData); } else { cursor.moveToNext(); @@ -110,7 +110,7 @@ public class C2DMReceiver extends BroadcastReceiver { final Task task = new Task(); if(cursor.getCount() == 0) { task.setValue(Task.REMOTE_ID, Long.parseLong(intent.getStringExtra("task_id"))); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); taskService.save(task); } else { cursor.moveToNext(); @@ -204,7 +204,7 @@ public class C2DMReceiver extends BroadcastReceiver { task.setValue(Task.TITLE, intent.getStringExtra("title")); task.setValue(Task.REMOTE_ID, Long.parseLong(intent.getStringExtra("task_id"))); task.setValue(Task.USER_ID, -1L); // set it to invalid number because we don't know whose it is - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); taskService.save(task); new Thread(new Runnable() { @@ -244,7 +244,7 @@ public class C2DMReceiver extends BroadcastReceiver { if(cursor.getCount() == 0) { tagData.setValue(TagData.NAME, intent.getStringExtra("title")); tagData.setValue(TagData.REMOTE_ID, Long.parseLong(intent.getStringExtra("tag_id"))); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); tagDataService.save(tagData); new Thread(new Runnable() { diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java b/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java index 0b3cca324..16122be3f 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java @@ -494,7 +494,7 @@ public class EditPeopleControlSet implements TaskEditControlSet { readTagData(result.getJSONArray("tags")); JsonHelper.readUser(result.getJSONObject("assignee"), task, Task.USER_ID, Task.USER); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); taskService.save(task); int count = result.optInt("shared", 0); diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/TagViewActivity.java b/astrid/plugin-src/com/todoroo/astrid/actfm/TagViewActivity.java index a955fdd61..9397c0b02 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/TagViewActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/TagViewActivity.java @@ -689,7 +689,7 @@ public class TagViewActivity extends TaskListActivity implements OnTabChangeList try { String url = actFmSyncService.setTagPicture(tagData.getValue(TagData.REMOTE_ID), bitmap); tagData.setValue(TagData.PICTURE, url); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); tagDataService.save(tagData); } catch (IOException e) { DialogUtilities.okDialog(TagViewActivity.this, e.toString(), null); @@ -709,7 +709,7 @@ public class TagViewActivity extends TaskListActivity implements OnTabChangeList update.setValue(Update.USER_ID, 0L); update.setValue(Update.TAGS, "," + tagData.getValue(TagData.REMOTE_ID) + ","); update.setValue(Update.CREATION_DATE, DateUtilities.now()); - Flags.checkAndClear(Flags.SUPPRESS_SYNC); + Flags.checkAndClear(Flags.ACTFM_SUPPRESS_SYNC); updateDao.createNew(update); addCommentField.setText(""); //$NON-NLS-1$ 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 976db9e32..fd9e0e34d 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncService.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncService.java @@ -88,7 +88,7 @@ public final class ActFmSyncService { taskDao.addListener(new ModelUpdateListener() { @Override public void onModelUpdated(final Task model) { - if(Flags.checkAndClear(Flags.SUPPRESS_SYNC)) + if(Flags.checkAndClear(Flags.ACTFM_SUPPRESS_SYNC)) return; final ContentValues setValues = model.getSetValues(); if(setValues == null || !checkForToken() || setValues.containsKey(RemoteModel.REMOTE_ID_PROPERTY_NAME)) @@ -108,7 +108,7 @@ public final class ActFmSyncService { updateDao.addListener(new ModelUpdateListener() { @Override public void onModelUpdated(final Update model) { - if(Flags.checkAndClear(Flags.SUPPRESS_SYNC)) + if(Flags.checkAndClear(Flags.ACTFM_SUPPRESS_SYNC)) return; final ContentValues setValues = model.getSetValues(); if(setValues == null || !checkForToken() || model.getValue(Update.REMOTE_ID) > 0) @@ -126,7 +126,7 @@ public final class ActFmSyncService { tagDataDao.addListener(new ModelUpdateListener() { @Override public void onModelUpdated(final TagData model) { - if(Flags.checkAndClear(Flags.SUPPRESS_SYNC)) + if(Flags.checkAndClear(Flags.ACTFM_SUPPRESS_SYNC)) return; final ContentValues setValues = model.getSetValues(); if(setValues == null || !checkForToken() || setValues.containsKey(RemoteModel.REMOTE_ID_PROPERTY_NAME)) @@ -265,7 +265,7 @@ public final class ActFmSyncService { JsonHelper.taskFromJson(result, task, metadata); task.setValue(Task.MODIFICATION_DATE, DateUtilities.now()); task.setValue(Task.LAST_SYNC, DateUtilities.now()); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); taskDao.saveExisting(task); } catch (JSONException e) { handleException("task-save-json", e); @@ -355,7 +355,7 @@ public final class ActFmSyncService { JSONObject result = actFmInvoker.invoke("tag_save", params.toArray(new Object[params.size()])); if(newlyCreated) { tagData.setValue(TagData.REMOTE_ID, result.optLong("id")); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); tagDataDao.saveExisting(tagData); } success = true; @@ -395,7 +395,7 @@ public final class ActFmSyncService { JSONObject item = list.getJSONObject(i); readIds(locals, item, remote); JsonHelper.tagFromJson(item, remote); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); tagDataService.save(remote); } } @@ -431,7 +431,7 @@ public final class ActFmSyncService { "token", token); JsonHelper.tagFromJson(result, tagData); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); tagDataService.save(tagData); } @@ -453,7 +453,7 @@ public final class ActFmSyncService { ArrayList metadata = new ArrayList(); JsonHelper.taskFromJson(result, task, metadata); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); taskService.save(task); metadataService.synchronizeMetadata(task.getId(), metadata, Metadata.KEY.eq(TagService.KEY)); } @@ -505,7 +505,7 @@ public final class ActFmSyncService { } - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); taskService.save(remote); metadataService.synchronizeMetadata(remote.getId(), metadata, MetadataCriteria.withKey(TagService.KEY)); remote.clear(); @@ -543,7 +543,7 @@ public final class ActFmSyncService { readIds(locals, item, remote); JsonHelper.updateFromJson(item, remote); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); if(remote.getId() == AbstractModel.NO_ID) updateDao.createNew(remote); else @@ -578,7 +578,7 @@ public final class ActFmSyncService { readIds(locals, item, remote); JsonHelper.updateFromJson(item, remote); - Flags.set(Flags.SUPPRESS_SYNC); + Flags.set(Flags.ACTFM_SUPPRESS_SYNC); if(remote.getId() == AbstractModel.NO_ID) updateDao.createNew(remote); else diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksFilterExposer.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksFilterExposer.java index 01837098b..f99311701 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksFilterExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksFilterExposer.java @@ -26,6 +26,7 @@ import com.todoroo.astrid.api.FilterCategory; import com.todoroo.astrid.api.FilterListHeader; import com.todoroo.astrid.api.FilterListItem; import com.todoroo.astrid.api.FilterWithCustomIntent; +import com.todoroo.astrid.api.PermaSql; import com.todoroo.astrid.dao.TaskDao.TaskCriteria; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.MetadataApiDao.MetadataCriteria; @@ -56,13 +57,14 @@ public class GtasksFilterExposer extends BroadcastReceiver { values.putAll(GtasksMetadata.createEmptyMetadata(AbstractModel.NO_ID).getMergedValues()); values.remove(Metadata.TASK.name); values.put(GtasksMetadata.LIST_ID.name, list.getValue(GtasksList.REMOTE_ID)); + values.put(GtasksMetadata.ORDER.name, PermaSql.VALUE_NOW); FilterWithCustomIntent filter = new FilterWithCustomIntent(listName, ContextManager.getString(R.string.gtasks_FEx_title, listName), new QueryTemplate().join( Join.left(Metadata.TABLE, Task.ID.eq(Metadata.TASK))).where(Criterion.and( MetadataCriteria.withKey(GtasksMetadata.METADATA_KEY), TaskCriteria.activeAndVisible(), GtasksMetadata.LIST_ID.eq(list.getValue(GtasksList.REMOTE_ID)))).orderBy( - Order.asc(Functions.cast(GtasksMetadata.ORDER, "INTEGER"))), //$NON-NLS-1$ + Order.asc(Functions.cast(GtasksMetadata.ORDER, "LONG"))), //$NON-NLS-1$ values); filter.listingIcon = ((BitmapDrawable)context.getResources().getDrawable(R.drawable.gtasks_icon)).getBitmap(); filter.customTaskList = new ComponentName(ContextManager.getContext(), GtasksListActivity.class); diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadata.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadata.java index 2cdd3a74d..0675d180a 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadata.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadata.java @@ -34,7 +34,7 @@ public class GtasksMetadata { public static final IntegerProperty INDENT = new IntegerProperty(Metadata.TABLE, Metadata.VALUE4.name); - public static final IntegerProperty ORDER = new IntegerProperty(Metadata.TABLE, + public static final LongProperty ORDER = new LongProperty(Metadata.TABLE, Metadata.VALUE5.name); /** @@ -52,9 +52,9 @@ public class GtasksMetadata { defaultList = ""; //$NON-NLS-1$ metadata.setValue(LIST_ID, defaultList); - metadata.setValue(PARENT_TASK, (long)VALUE_UNSET); + metadata.setValue(PARENT_TASK, AbstractModel.NO_ID); metadata.setValue(INDENT, 0); - metadata.setValue(ORDER, (int)(DateUtilities.now() / 1000L)); + metadata.setValue(ORDER, DateUtilities.now()); if(taskId > AbstractModel.NO_ID) metadata.setValue(Metadata.TASK, taskId); return metadata; diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadataService.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadataService.java index 2edd7d0f9..d537126d2 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadataService.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadataService.java @@ -4,13 +4,25 @@ package com.todoroo.astrid.gtasks; import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import android.text.TextUtils; + +import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.sql.Criterion; +import com.todoroo.andlib.sql.Field; +import com.todoroo.andlib.sql.Functions; +import com.todoroo.andlib.sql.Order; +import com.todoroo.andlib.sql.Query; +import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.StoreObject; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.gtasks.sync.GtasksTaskContainer; import com.todoroo.astrid.sync.SyncMetadataService; @@ -62,4 +74,94 @@ public final class GtasksMetadataService extends SyncMetadataService cursor = PluginServices.getMetadataService().query(query); + try { + for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + long taskId = cursor.get(Metadata.TASK); + Metadata metadata = getTaskMetadata(taskId); + if(metadata == null) + continue; + iterator.processTask(taskId, metadata); + } + + } finally { + cursor.close(); + } + } + + /** + * Gets the remote id string of the parent task + * @param gtasksMetadata + * @return + */ + public String getRemoteParentId(Metadata gtasksMetadata) { + String parent = null; + if (gtasksMetadata.containsNonNullValue(GtasksMetadata.PARENT_TASK)) { + long parentId = gtasksMetadata.getValue(GtasksMetadata.PARENT_TASK); + Metadata parentMetadata = getTaskMetadata(parentId); + if (parentMetadata != null && parentMetadata.containsNonNullValue(GtasksMetadata.ID)) { + parent = parentMetadata.getValue(GtasksMetadata.ID); + if (TextUtils.isEmpty(parent)) { + parent = null; + } + } + } + return parent; + } + + /** + * Gets the remote id string of the previous sibling task + * @param listId + * @param gtasksMetadata + * @return + */ + public String getRemoteSiblingId(String listId, Metadata gtasksMetadata) { + final AtomicInteger indentToMatch = new AtomicInteger(gtasksMetadata.getValue(GtasksMetadata.INDENT).intValue()); + final AtomicLong parentToMatch = new AtomicLong(gtasksMetadata.getValue(GtasksMetadata.PARENT_TASK).longValue()); + final AtomicReference sibling = new AtomicReference(); + + ListIterator iterator = new ListIterator() { + @Override + public void processTask(long taskId, Metadata metadata) { + Task t = taskDao.fetch(taskId, Task.TITLE, Task.DELETION_DATE); + if (t.isDeleted()) return; + int currIndent = metadata.getValue(GtasksMetadata.INDENT).intValue(); + long currParent = metadata.getValue(GtasksMetadata.PARENT_TASK); + + if (currIndent == indentToMatch.get() && currParent == parentToMatch.get()) { + if (sibling.get() == null) { + sibling.set(metadata.getValue(GtasksMetadata.ID)); + } + } + } + }; + + this.iterateThroughList(listId, iterator, gtasksMetadata.getValue(GtasksMetadata.ORDER), true); + return sibling.get(); + } + } diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferenceService.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferenceService.java index 66f75cdcc..72161096f 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferenceService.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferenceService.java @@ -24,6 +24,10 @@ public class GtasksPreferenceService extends SyncProviderUtilities { return R.string.gtasks_GPr_interval_key; } + public int getSyncOnSaveKey() { + return R.string.gtasks_GPr_sync_on_save_key; + } + /** GTasks user's default list id */ public static final String PREF_DEFAULT_LIST = IDENTIFIER + "_defaultlist"; //$NON-NLS-1$ diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferences.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferences.java index 8d2c76fa0..9f83c4a4b 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferences.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferences.java @@ -1,6 +1,9 @@ package com.todoroo.astrid.gtasks; +import android.content.res.Resources; import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; import com.timsu.astrid.R; import com.todoroo.andlib.service.Autowired; @@ -57,4 +60,24 @@ public class GtasksPreferences extends SyncProviderPreferences { new GtasksBackgroundService().scheduleService(); } + @Override + public void updatePreferences(Preference preference, Object value) { + final Resources r = getResources(); + + if (r.getString(R.string.gtasks_GPr_sync_on_save_key).equals(preference.getKey())) { + preference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (((Boolean) newValue).booleanValue()) { + startSync(); + } + return true; + } + + }); + } else { + super.updatePreferences(preference, value); + } + } + } \ No newline at end of file diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksTaskListUpdater.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksTaskListUpdater.java index fe28c2891..ffcc6f814 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksTaskListUpdater.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksTaskListUpdater.java @@ -2,6 +2,7 @@ package com.todoroo.astrid.gtasks; import java.util.ArrayList; import java.util.HashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -9,12 +10,9 @@ import java.util.concurrent.atomic.AtomicReference; import android.text.TextUtils; import android.util.Log; -import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.service.Autowired; -import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.utility.DateUtilities; -import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.StoreObject; @@ -57,8 +55,9 @@ public class GtasksTaskListUpdater { final AtomicInteger targetTaskIndent = new AtomicInteger(-1); final AtomicInteger previousIndent = new AtomicInteger(-1); final AtomicLong previousTask = new AtomicLong(-1); + final AtomicReference listRef = new AtomicReference(list); - iterateThroughList(list, new ListIterator() { + gtasksMetadataService.iterateThroughList(list, new GtasksMetadataService.ListIterator() { @Override public void processTask(long taskId, Metadata metadata) { int indent = metadata.getValue(GtasksMetadata.INDENT); @@ -68,13 +67,14 @@ public class GtasksTaskListUpdater { if(indent + delta <= previousIndent.get() + 1 && indent + delta >= 0) { targetTaskIndent.set(indent); metadata.setValue(GtasksMetadata.INDENT, indent + delta); - if(delta > 0) - metadata.setValue(GtasksMetadata.PARENT_TASK, previousTask.get()); - else if(parents.containsKey(taskId)) - metadata.setValue(GtasksMetadata.PARENT_TASK, - parents.get(parents.get(taskId))); - else + + long newParent = computeNewParent(listRef.get(), taskId, indent + delta - 1); + if (newParent == taskId) { + System.err.println("Tried to set parent to self"); metadata.setValue(GtasksMetadata.PARENT_TASK, Task.NO_ID); + } else { + metadata.setValue(GtasksMetadata.PARENT_TASK, newParent); + } saveAndUpdateModifiedDate(metadata, taskId); } } else if(targetTaskIndent.get() > -1) { @@ -94,6 +94,39 @@ public class GtasksTaskListUpdater { }); } + /** + * Helper function to iterate through a list and compute a new parent for the target task + * based on the target parent's indent + * @param list + * @param targetTaskId + * @param newIndent + * @return + */ + private long computeNewParent(StoreObject list, long targetTaskId, int targetParentIndent) { + final AtomicInteger desiredParentIndent = new AtomicInteger(targetParentIndent); + final AtomicLong targetTask = new AtomicLong(targetTaskId); + final AtomicLong lastPotentialParent = new AtomicLong(-1); + final AtomicBoolean computedParent = new AtomicBoolean(false); + + GtasksMetadataService.ListIterator iterator = new GtasksMetadataService.ListIterator() { + @Override + public void processTask(long taskId, Metadata metadata) { + if (targetTask.get() == taskId) { + computedParent.set(true); + } + + int indent = metadata.getValue(GtasksMetadata.INDENT); + if (!computedParent.get() && indent == desiredParentIndent.get()) { + lastPotentialParent.set(taskId); + } + } + }; + + gtasksMetadataService.iterateThroughList(list, iterator); + if (lastPotentialParent.get() == -1) return Task.NO_ID; + return lastPotentialParent.get(); + } + // --- task moving private static class Node { @@ -138,10 +171,10 @@ public class GtasksTaskListUpdater { } } - traverseTreeAndWriteValues(root, new AtomicInteger(0), -1); + traverseTreeAndWriteValues(root, new AtomicLong(0), -1); } - private void traverseTreeAndWriteValues(Node node, AtomicInteger order, int indent) { + private void traverseTreeAndWriteValues(Node node, AtomicLong order, int indent) { if(node.taskId != -1) { Metadata metadata = gtasksMetadataService.getTaskMetadata(node.taskId); if(metadata == null) @@ -173,7 +206,7 @@ public class GtasksTaskListUpdater { final AtomicInteger previoustIndent = new AtomicInteger(-1); final AtomicReference currentNode = new AtomicReference(root); - iterateThroughList(list, new ListIterator() { + gtasksMetadataService.iterateThroughList(list, new GtasksMetadataService.ListIterator() { @Override public void processTask(long taskId, Metadata metadata) { int indent = metadata.getValue(GtasksMetadata.INDENT); @@ -208,7 +241,7 @@ public class GtasksTaskListUpdater { if(list == GtasksListService.LIST_NOT_FOUND_OBJECT) return; - iterateThroughList(list, new ListIterator() { + gtasksMetadataService.iterateThroughList(list, new GtasksMetadataService.ListIterator() { public void processTask(long taskId, Metadata metadata) { System.err.format("id %d: order %d, indent:%d, parent:%d\n", taskId, //$NON-NLS-1$ metadata.getValue(GtasksMetadata.ORDER), @@ -252,10 +285,10 @@ public class GtasksTaskListUpdater { updateParentSiblingMapsFor(list); - final AtomicInteger order = new AtomicInteger(0); + final AtomicLong order = new AtomicLong(0); final AtomicInteger previousIndent = new AtomicInteger(-1); - iterateThroughList(list, new ListIterator() { + gtasksMetadataService.iterateThroughList(list, new GtasksMetadataService.ListIterator() { @Override public void processTask(long taskId, Metadata metadata) { metadata.setValue(GtasksMetadata.ORDER, order.getAndAdd(1)); @@ -288,7 +321,7 @@ public class GtasksTaskListUpdater { final AtomicLong previousTask = new AtomicLong(-1L); final AtomicInteger previousIndent = new AtomicInteger(-1); - iterateThroughList(list, new ListIterator() { + gtasksMetadataService.iterateThroughList(list, new GtasksMetadataService.ListIterator() { @Override public void processTask(long taskId, Metadata metadata) { int indent = metadata.getValue(GtasksMetadata.INDENT); @@ -349,28 +382,6 @@ public class GtasksTaskListUpdater { localToRemoteIdMap.put(id, remoteId); } - // --- private helpers - - private interface ListIterator { - public void processTask(long taskId, Metadata metadata); - } - - private void iterateThroughList(StoreObject list, ListIterator iterator) { - Filter filter = GtasksFilterExposer.filterFromList(ContextManager.getContext(), list); - TodorooCursor cursor = PluginServices.getTaskService().fetchFiltered(filter.sqlQuery, null, Task.ID); - try { - for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { - long taskId = cursor.getLong(0); - Metadata metadata = gtasksMetadataService.getTaskMetadata(taskId); - if(metadata == null) - continue; - iterator.processTask(taskId, metadata); - } - - } finally { - cursor.close(); - } - } } diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/api/GtasksService.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/api/GtasksService.java index 71c2ceefe..6890ebe31 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/api/GtasksService.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/api/GtasksService.java @@ -44,6 +44,16 @@ public class GtasksService { service.setApplicationName("Astrid"); } + /** + * A simple service query that will throw an exception if anything goes wrong. + * Useful for checking if token needs revalidating or if there are network problems-- + * no exception means all is well + * @throws IOException + */ + public void ping() throws IOException { + service.tasklists.get("@default").execute(); + } + public TaskLists allGtaskLists() throws IOException { TaskLists toReturn; try { diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/auth/GtasksTokenValidator.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/auth/GtasksTokenValidator.java index 3d3a74b96..827334ebb 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/auth/GtasksTokenValidator.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/auth/GtasksTokenValidator.java @@ -1,5 +1,7 @@ package com.todoroo.astrid.gtasks.auth; +import java.io.IOException; + import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerFuture; @@ -17,25 +19,41 @@ public class GtasksTokenValidator { /** * Invalidates and then revalidates the auth token for the currently logged in user + * Shouldn't be called from the main thread--will block on network calls * @param token - * @return + * @return valid token on success, null on failure */ public static String validateAuthToken(String token) { - Account a = accountManager.getAccountByName(Preferences.getStringValue(GtasksPreferenceService.PREF_USER_NAME)); - if (a == null) return null; - - accountManager.invalidateAuthToken(token); - AccountManagerFuture future = accountManager.manager.getAuthToken(a, GtasksService.AUTH_TOKEN_TYPE, true, null, null); - + GtasksService testService = new GtasksService(token); try { - if (future.getResult().containsKey(AccountManager.KEY_AUTHTOKEN)) { - Bundle result = future.getResult(); - return result.getString(AccountManager.KEY_AUTHTOKEN); + testService.ping(); + return token; + } catch (IOException i) { //If fail, token may have expired -- get a new one and return that + Account a = accountManager.getAccountByName(Preferences.getStringValue(GtasksPreferenceService.PREF_USER_NAME)); + if (a == null) return null; + + accountManager.invalidateAuthToken(token); + AccountManagerFuture future = accountManager.manager.getAuthToken(a, GtasksService.AUTH_TOKEN_TYPE, true, null, null); + + try { + if (future.getResult().containsKey(AccountManager.KEY_AUTHTOKEN)) { + Bundle result = future.getResult(); + token = result.getString(AccountManager.KEY_AUTHTOKEN); + testService = new GtasksService(token); + try { //Make sure the new token works--if not, we may have network problems + testService.ping(); + return token; + } catch (IOException i2) { + return null; + } + } + } catch (Exception e) { + e.printStackTrace(); + return null; } - } catch (Exception e) { - e.printStackTrace(); - return null; + } + return null; } diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncOnSaveService.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncOnSaveService.java new file mode 100644 index 000000000..6f4f239b8 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncOnSaveService.java @@ -0,0 +1,326 @@ +package com.todoroo.astrid.gtasks.sync; + +import java.io.IOException; +import java.util.concurrent.Semaphore; + +import android.content.ContentValues; +import android.text.TextUtils; + +import com.todoroo.andlib.data.DatabaseDao.ModelUpdateListener; +import com.todoroo.andlib.data.Property; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.utility.AndroidUtilities; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.andlib.utility.Preferences; +import com.todoroo.astrid.dao.MetadataDao; +import com.todoroo.astrid.dao.TaskDao; +import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.gtasks.GtasksMetadata; +import com.todoroo.astrid.gtasks.GtasksMetadataService; +import com.todoroo.astrid.gtasks.GtasksPreferenceService; +import com.todoroo.astrid.gtasks.GtasksTaskListUpdater; +import com.todoroo.astrid.gtasks.api.GoogleTasksException; +import com.todoroo.astrid.gtasks.api.GtasksApiUtilities; +import com.todoroo.astrid.gtasks.api.GtasksService; +import com.todoroo.astrid.gtasks.api.MoveRequest; +import com.todoroo.astrid.gtasks.auth.GtasksTokenValidator; +import com.todoroo.astrid.service.MetadataService; +import com.todoroo.astrid.utility.Flags; + +public final class GtasksSyncOnSaveService { + + @Autowired MetadataService metadataService; + @Autowired MetadataDao metadataDao; + @Autowired GtasksMetadataService gtasksMetadataService; + @Autowired TaskDao taskDao; + @Autowired GtasksPreferenceService gtasksPreferenceService; + @Autowired GtasksTaskListUpdater gtasksTaskListUpdater; + + public GtasksSyncOnSaveService() { + DependencyInjectionService.getInstance().inject(this); + } + + private final Semaphore syncOnSaveSema = new Semaphore(1); + + public void initialize() { + taskDao.addListener(new ModelUpdateListener() { + public void onModelUpdated(final Task model) { + if (!syncOnSaveEnabled()) + return; + if (gtasksPreferenceService.isOngoing()) //Don't try and sync changes that occur during a normal sync + return; + if(Flags.checkAndClear(Flags.GTASKS_SUPPRESS_SYNC)) + return; + final ContentValues setValues = model.getSetValues(); + if(setValues == null || !checkForToken()) + return; + if (!checkValuesForProperties(setValues, TASK_PROPERTIES)) //None of the properties we sync were updated + return; + + new Thread(new Runnable() { + @Override + public void run() { + // sleep so metadata associated with task is saved + AndroidUtilities.sleepDeep(1000L); + outer: + try { + try { + syncOnSaveSema.acquire(); + } catch (InterruptedException ingored) { + break outer; + } + pushTaskOnSave(model, setValues); + } catch (IOException e) { + e.printStackTrace(); + System.err.println("Sync on save failed"); //$NON-NLS-1$ + return; + } finally { + syncOnSaveSema.release(); + } + } + }).start(); + + } + });//*/ + + metadataDao.addListener(new ModelUpdateListener() { + public void onModelUpdated(final Metadata model) { + if (!syncOnSaveEnabled()) + return; + if (!model.getValue(Metadata.KEY).equals(GtasksMetadata.METADATA_KEY)) //Don't care about non-gtasks metadata + return; + if (gtasksPreferenceService.isOngoing()) //Don't try and sync changes that occur during a normal sync + return; + final ContentValues setValues = model.getSetValues(); + if (setValues == null || !checkForToken()) + return; + + if (checkValuesForProperties(setValues, METADATA_IGNORE_PROPERTIES)) // don't sync the move cases we don't handle + return; + if (!checkValuesForProperties(setValues, METADATA_PROPERTIES)) + return; + + if (Flags.checkAndClear(Flags.GTASKS_SUPPRESS_SYNC)) + return; + + new Thread(new Runnable() { + @Override + public void run() { + // sleep so metadata associated with task is saved + AndroidUtilities.sleepDeep(1000L); + outer: + try { + try { + syncOnSaveSema.acquire(); + } catch (InterruptedException ignored) { + break outer; + } + pushMetadataOnSave(model); + } catch (IOException e) { + e.printStackTrace(); + System.err.println("Sync on save failed"); //$NON-NLS-1$ + return; + } finally { + syncOnSaveSema.release(); + } + } + }).start(); + } + + }); + } + + private static final Property[] TASK_PROPERTIES = + { Task.TITLE, + Task.NOTES, + Task.DUE_DATE, + Task.COMPLETION_DATE, + Task.DELETION_DATE }; + + private static final Property[] METADATA_PROPERTIES = + { GtasksMetadata.INDENT, + GtasksMetadata.PARENT_TASK }; + + private static final Property[] METADATA_IGNORE_PROPERTIES = + { GtasksMetadata.ORDER, + GtasksMetadata.LIST_ID }; + + /** + * Checks to see if any of the values changed are among the properties we sync + * @param values + * @param properties + * @return false if none of the properties we sync were changed, true otherwise + */ + private boolean checkValuesForProperties(ContentValues values, Property[] properties) { + for (Property property : properties) { + if (values.containsKey(property.name)) + return true; + } + return false; + } + + + /** + * Synchronize with server when data changes + */ + private void pushTaskOnSave(Task task, ContentValues values) throws IOException { + Metadata gtasksMetadata = gtasksMetadataService.getTaskMetadata(task.getId()); + com.google.api.services.tasks.v1.model.Task remoteModel = null; + boolean newlyCreated = false; + + //Initialize the gtasks api service + String token = gtasksPreferenceService.getToken(); + token = GtasksTokenValidator.validateAuthToken(token); + if (token == null) { + throw new GoogleTasksException("Failed to establish connection for sync on save"); //$NON-NLS-1$ + } + gtasksPreferenceService.setToken(token); + GtasksService gtasksService = new GtasksService(token); + + String remoteId = null; + String listId = Preferences.getStringValue(GtasksPreferenceService.PREF_DEFAULT_LIST); + if (listId == null) { + listId = gtasksService.getGtaskList("@default").id; //$NON-NLS-1$ + Preferences.setString(GtasksPreferenceService.PREF_DEFAULT_LIST, listId); + } + + if (gtasksMetadata == null || !gtasksMetadata.containsNonNullValue(GtasksMetadata.ID) || + TextUtils.isEmpty(gtasksMetadata.getValue(GtasksMetadata.ID))) { //Create case + if (gtasksMetadata == null) { + gtasksMetadata = GtasksMetadata.createEmptyMetadata(task.getId()); + } + if (gtasksMetadata.containsNonNullValue(GtasksMetadata.LIST_ID)) { + listId = gtasksMetadata.getValue(GtasksMetadata.LIST_ID); + } + + remoteModel = new com.google.api.services.tasks.v1.model.Task(); + newlyCreated = true; + } else { //update case + remoteId = gtasksMetadata.getValue(GtasksMetadata.ID); + listId = gtasksMetadata.getValue(GtasksMetadata.LIST_ID); + remoteModel = gtasksService.getGtask(listId, remoteId); + } + + //If task was newly created but without a title, don't sync--we're in the middle of + //creating a task which may end up being cancelled + if (newlyCreated && + (!values.containsKey(Task.TITLE.name) || TextUtils.isEmpty(task.getValue(Task.TITLE)))) { + return; + } + + //Update the remote model's changed properties + if (values.containsKey(Task.DELETION_DATE.name) && task.isDeleted()) { + remoteModel.deleted = true; + } + + if (values.containsKey(Task.TITLE.name)) { + remoteModel.title = task.getValue(Task.TITLE); + } + if (values.containsKey(Task.NOTES.name)) { + remoteModel.notes = task.getValue(Task.NOTES); + } + if (values.containsKey(Task.DUE_DATE.name)) { + remoteModel.due = GtasksApiUtilities.unixTimeToGtasksTime(task.getValue(Task.DUE_DATE)); + } + if (values.containsKey(Task.COMPLETION_DATE.name)) { + if (task.isCompleted()) { + remoteModel.completed = GtasksApiUtilities.unixTimeToGtasksTime(task.getValue(Task.COMPLETION_DATE)); + remoteModel.status = "completed"; //$NON-NLS-1$ + } else { + remoteModel.completed = null; + remoteModel.status = "needsAction"; //$NON-NLS-1$ + } + } + + if (!newlyCreated) { + gtasksService.updateGtask(listId, remoteModel); + } else { + String parent = gtasksMetadataService.getRemoteParentId(gtasksMetadata); + String priorSibling = gtasksMetadataService.getRemoteSiblingId(listId, gtasksMetadata); + + try { //Make sure the parent task exists on the target list + if (parent != null) { + com.google.api.services.tasks.v1.model.Task remoteParent = gtasksService.getGtask(listId, parent); + if (remoteParent == null || (remoteParent.deleted != null && remoteParent.deleted)) + parent = null; + } + } catch (IOException e) { + parent = null; + } + + try { + if (priorSibling != null) { + com.google.api.services.tasks.v1.model.Task remoteSibling = gtasksService.getGtask(listId, priorSibling); + if (remoteSibling == null || (remoteSibling.deleted != null && remoteSibling.deleted)) + priorSibling = null; + } + } catch (IOException e) { + priorSibling = null; + } + + + com.google.api.services.tasks.v1.model.Task created = gtasksService.createGtask(listId, remoteModel, parent, priorSibling); + + //Update the metadata for the newly created task + gtasksMetadata.setValue(GtasksMetadata.ID, created.id); + gtasksMetadata.setValue(GtasksMetadata.LIST_ID, listId); + metadataService.save(gtasksMetadata); + } + + task.setValue(Task.MODIFICATION_DATE, DateUtilities.now()); + task.setValue(Task.LAST_SYNC, DateUtilities.now()); + Flags.set(Flags.GTASKS_SUPPRESS_SYNC); + taskDao.saveExisting(task); + } + + private void pushMetadataOnSave(Metadata model) throws IOException { + //Initialize the gtasks api service + String token = gtasksPreferenceService.getToken(); + token = GtasksTokenValidator.validateAuthToken(token); + if (token == null) { + throw new GoogleTasksException("Failed to establish connection for sync on save"); //$NON-NLS-1$ + } + gtasksPreferenceService.setToken(token); + GtasksService gtasksService = new GtasksService(token); + + String taskId = model.getValue(GtasksMetadata.ID); + String listId = model.getValue(GtasksMetadata.LIST_ID); + String parent = gtasksMetadataService.getRemoteParentId(model); + String priorSibling = gtasksMetadataService.getRemoteSiblingId(listId, model); + + try { //Make sure the parent task exists on the target list + if (parent != null) { + com.google.api.services.tasks.v1.model.Task remoteParent = gtasksService.getGtask(listId, parent); + if (remoteParent == null || (remoteParent.deleted != null && remoteParent.deleted)) + parent = null; + } + } catch (IOException e) { + parent = null; + } + + try { + if (priorSibling != null) { + com.google.api.services.tasks.v1.model.Task remoteSibling = gtasksService.getGtask(listId, priorSibling); + if (remoteSibling == null || (remoteSibling.deleted != null && remoteSibling.deleted)) + priorSibling = null; + } + } catch (IOException e) { + priorSibling = null; + } + + MoveRequest move = new MoveRequest(gtasksService, taskId, listId, parent, priorSibling); + move.executePush(); + } + + private boolean syncOnSaveEnabled() { + return Preferences.getBoolean(gtasksPreferenceService.getSyncOnSaveKey(), false); + } + + private boolean checkForToken() { + if (!gtasksPreferenceService.isLoggedIn()) + return false; + return true; + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncProvider.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncProvider.java index d9ba74800..f9977a3e9 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncProvider.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncProvider.java @@ -5,6 +5,8 @@ package com.todoroo.astrid.gtasks.sync; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -80,7 +82,7 @@ public class GtasksSyncProvider extends SyncProvider { /** tasks to read id for */ ArrayList createdWithoutId; - ArrayList createdWithoutParent; + ArrayList createdWithoutOrder; Semaphore pushedTaskSemaphore = new Semaphore(0); AtomicInteger pushedTaskCount = new AtomicInteger(0); @@ -161,7 +163,7 @@ public class GtasksSyncProvider extends SyncProvider { if(Constants.DEBUG) Log.e("gtasks-debug", "- -------- SYNC STARTED"); createdWithoutId = new ArrayList(); - createdWithoutParent = new ArrayList(); + createdWithoutOrder = new ArrayList(); try { TaskLists allTaskLists = taskService.allGtaskLists(); @@ -213,7 +215,7 @@ public class GtasksSyncProvider extends SyncProvider { } else if (Preferences.getStringValue(GtasksPreferenceService.PREF_DEFAULT_LIST) != null) { listId = Preferences.getStringValue(GtasksPreferenceService.PREF_DEFAULT_LIST); } else { - listId = "@default"; + listId = taskService.getGtaskList("@default").id; } Preferences.setString(GtasksPreferenceService.PREF_DEFAULT_LIST, listId); @@ -233,13 +235,11 @@ public class GtasksSyncProvider extends SyncProvider { // match remote tasks to locally created tasks HashMap locals = new HashMap(); - HashMap localIdsToRemoteIds = new HashMap(); for(GtasksTaskContainer task : createdWithoutId) { locals.put(task.gtaskMetadata.getValue(GtasksMetadata.ID), task); - localIdsToRemoteIds.put(task.task.getId(), task.gtaskMetadata.getValue(GtasksMetadata.ID)); } - verifyCreatedOrder(localIdsToRemoteIds); + verifyCreatedOrder(); // first, pull all tasks. then we can write them // include deleted tasks so we can delete them in astrid @@ -259,16 +259,27 @@ public class GtasksSyncProvider extends SyncProvider { super.readRemotelyUpdated(data); } - private void verifyCreatedOrder(HashMap localIdsToRemoteIds) throws IOException { - for (GtasksTaskContainer t : createdWithoutParent) { + private void verifyCreatedOrder() throws IOException { + Collections.sort(createdWithoutOrder, new Comparator() { + @Override + public int compare(GtasksTaskContainer arg0, GtasksTaskContainer arg1) { + long order0 = arg0.gtaskMetadata.getValue(GtasksMetadata.ORDER); + long order1 = arg1.gtaskMetadata.getValue(GtasksMetadata.ORDER); + + if (order0 == order1) return 0; + if (order0 < order1) return -1; + else return 1; + } + + }); + for (GtasksTaskContainer t : createdWithoutOrder) { String toMove = t.gtaskMetadata.getValue(GtasksMetadata.ID); String listId = t.gtaskMetadata.getValue(GtasksMetadata.LIST_ID); - long parentTask = t.gtaskMetadata.getValue(GtasksMetadata.PARENT_TASK); - if (parentTask > 0) { - String remoteParent = localIdsToRemoteIds.get(parentTask); - MoveRequest move = new MoveRequest(taskService, toMove, listId, remoteParent, null); - move.executePush(); - } + String remoteParent = gtasksMetadataService.getRemoteParentId(t.gtaskMetadata); + String remoteSibling = gtasksMetadataService.getRemoteSiblingId(listId, t.gtaskMetadata); + + MoveRequest move = new MoveRequest(taskService, toMove, listId, remoteParent, remoteSibling); + move.executePush(); } } @@ -303,7 +314,7 @@ public class GtasksSyncProvider extends SyncProvider { // ---------------------------------------------------------------------- private ArrayList readAllRemoteTasks(final boolean includeDeleted) { - final ArrayList remoteTasks = new ArrayList(); + final ArrayList list = new ArrayList(); final Semaphore listsFinished = new Semaphore(0); // launch threads @@ -316,8 +327,8 @@ public class GtasksSyncProvider extends SyncProvider { String listId = dashboard.getValue(GtasksList.REMOTE_ID); if(Constants.DEBUG) Log.e("gtasks-debug", "ACTION: getTasks, " + listId); - List list = taskService.getAllGtasksFromListId(listId, includeDeleted).items; - addRemoteTasksToList(list, remoteTasks); + List remoteTasks = taskService.getAllGtasksFromListId(listId, includeDeleted).items; + addRemoteTasksToList(remoteTasks, list); } catch (Exception e) { handleException("read-remotes", e, false); } finally { @@ -332,14 +343,14 @@ public class GtasksSyncProvider extends SyncProvider { } catch (InterruptedException e) { handleException("wait-for-remotes", e, false); } - return remoteTasks; + return list; } private void addRemoteTasksToList(List remoteTasks, ArrayList list) { if (remoteTasks != null) { - int order = 0; + long order = 0; HashMap idsToTasks = new HashMap(); HashMap indentation = new HashMap(); HashMap parentToPriorSiblingMap = new HashMap(); @@ -385,6 +396,7 @@ public class GtasksSyncProvider extends SyncProvider { private int findIndentation(HashMap idsToTasks, HashMap indentation, com.google.api.services.tasks.v1.model.Task task) { + if (task == null) return 0; //TODO: Not sure why this is happening... if(indentation.containsKey(task.id)) return indentation.get(task.id); @@ -404,13 +416,17 @@ public class GtasksSyncProvider extends SyncProvider { local.gtaskMetadata.setValue(GtasksMetadata.LIST_ID, listId); createdWithoutId.add(local); - if (local.gtaskMetadata.containsNonNullValue(GtasksMetadata.PARENT_TASK)) { - createdWithoutParent.add(local); - } + createdWithoutOrder.add(local); + com.google.api.services.tasks.v1.model.Task createdTask = new com.google.api.services.tasks.v1.model.Task(); CreateRequest createRequest = new CreateRequest(taskService, listId, createdTask, local.parentId, local.priorSiblingId); - updateTaskHelper(local, null, createRequest); + //updateTaskHelper(local, null, createRequest); + localPropertiesToModel(local, null, createRequest.getToPush()); + com.google.api.services.tasks.v1.model.Task createResult = createRequest.executePush(); + String newIdTask = createResult.id; + local.gtaskMetadata.setValue(GtasksMetadata.ID, newIdTask); + return local; }//*/ @@ -456,12 +472,13 @@ public class GtasksSyncProvider extends SyncProvider { public void run() { String newIdTask = idTask; try { - if (request instanceof CreateRequest) { + /*if (request instanceof CreateRequest) { //This is commented out until bugs with multithreaded task creation are resolved com.google.api.services.tasks.v1.model.Task createResult = request.executePush(); newIdTask = createResult.id; + System.err.println("Created " + createResult.title + " successfully, remote id: " + createResult.id); local.gtaskMetadata.setValue(GtasksMetadata.ID, newIdTask); - } - if(!TextUtils.isEmpty(newIdTask) && (remote == null || local.parentId != remote.parentId || + }//*/ + if(!TextUtils.isEmpty(newIdTask) && remote != null && (local.parentId != remote.parentId || local.priorSiblingId != remote.priorSiblingId)) { if(Constants.DEBUG) Log.e("gtasks-debug", "ACTION: move(1) - " + newIdTask + ", " + local.parentId + ", " + local.priorSiblingId); diff --git a/astrid/plugin-src/com/todoroo/astrid/notes/EditNoteActivity.java b/astrid/plugin-src/com/todoroo/astrid/notes/EditNoteActivity.java index e5cc10b87..9b3d8dcae 100644 --- a/astrid/plugin-src/com/todoroo/astrid/notes/EditNoteActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/notes/EditNoteActivity.java @@ -255,7 +255,7 @@ public class EditNoteActivity extends ListActivity { update.setValue(Update.USER_ID, 0L); update.setValue(Update.TASK, task.getValue(Task.REMOTE_ID)); update.setValue(Update.CREATION_DATE, DateUtilities.now()); - Flags.checkAndClear(Flags.SUPPRESS_SYNC); + Flags.checkAndClear(Flags.ACTFM_SUPPRESS_SYNC); updateDao.createNew(update); commentField.setText(""); //$NON-NLS-1$ diff --git a/astrid/res/values/keys.xml b/astrid/res/values/keys.xml index 6434e9b81..977862494 100644 --- a/astrid/res/values/keys.xml +++ b/astrid/res/values/keys.xml @@ -273,6 +273,9 @@ gtasks_sync_freq + + gtasks_sync_on_save + actfm_sync_freq diff --git a/astrid/res/values/strings-gtasks.xml b/astrid/res/values/strings-gtasks.xml index 7c1779527..a56082d70 100644 --- a/astrid/res/values/strings-gtasks.xml +++ b/astrid/res/values/strings-gtasks.xml @@ -70,6 +70,10 @@ Google Tasks (Beta!) + Sync on Save + + Sync individual tasks as they are saved + diff --git a/astrid/res/xml/preferences_gtasks.xml b/astrid/res/xml/preferences_gtasks.xml index b6572b7f7..76ab5ab30 100644 --- a/astrid/res/xml/preferences_gtasks.xml +++ b/astrid/res/xml/preferences_gtasks.xml @@ -22,6 +22,11 @@ android:entryValues="@array/sync_SPr_interval_values" android:title="@string/sync_SPr_interval_title" /> + + 0; state &= ~flag; diff --git a/tests/src/com/todoroo/astrid/gtasks/GtasksIndentActionTest.java b/tests/src/com/todoroo/astrid/gtasks/GtasksIndentActionTest.java index 7ec637845..bd345d5bc 100644 --- a/tests/src/com/todoroo/astrid/gtasks/GtasksIndentActionTest.java +++ b/tests/src/com/todoroo/astrid/gtasks/GtasksIndentActionTest.java @@ -139,7 +139,7 @@ public class GtasksIndentActionTest extends DatabaseTestCase { gtasksListService.updateLists(lists); } - private Task taskWithMetadata(int order, int indentation) { + private Task taskWithMetadata(long order, int indentation) { Task task = new Task(); PluginServices.getTaskService().save(task); Metadata metadata = GtasksMetadata.createEmptyMetadata(task.getId()); diff --git a/tests/src/com/todoroo/astrid/gtasks/GtasksNewSyncTest.java b/tests/src/com/todoroo/astrid/gtasks/GtasksNewSyncTest.java index 4d4ddcbaf..fba3de05a 100644 --- a/tests/src/com/todoroo/astrid/gtasks/GtasksNewSyncTest.java +++ b/tests/src/com/todoroo/astrid/gtasks/GtasksNewSyncTest.java @@ -36,7 +36,7 @@ public class GtasksNewSyncTest extends DatabaseTestCase { private static boolean initialized = false; private static String DEFAULT_LIST = "@default"; - private static final String TEST_ACCOUNT = "sync_tester@astrid.com"; + private static final String TEST_ACCOUNT = "sync_tester2@astrid.com"; private static final long TIME_BETWEEN_SYNCS = 3000l; @Autowired TaskService taskService; @@ -497,6 +497,7 @@ public class GtasksNewSyncTest extends DatabaseTestCase { } String authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); authToken = GtasksTokenValidator.validateAuthToken(authToken); + gtasksPreferenceService.setToken(authToken); gtasksService = new GtasksService(authToken); diff --git a/tests/src/com/todoroo/astrid/gtasks/GtasksSyncOnSaveTest.java b/tests/src/com/todoroo/astrid/gtasks/GtasksSyncOnSaveTest.java new file mode 100644 index 000000000..df7140229 --- /dev/null +++ b/tests/src/com/todoroo/astrid/gtasks/GtasksSyncOnSaveTest.java @@ -0,0 +1,208 @@ +package com.todoroo.astrid.gtasks; + +import java.io.IOException; +import java.util.Date; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.content.Intent; +import android.os.Bundle; + +import com.google.api.client.googleapis.extensions.android2.auth.GoogleAccountManager; +import com.google.api.services.tasks.v1.model.Tasks; +import com.timsu.astrid.R; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.ContextManager; +import com.todoroo.andlib.utility.AndroidUtilities; +import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.andlib.utility.Preferences; +import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.gtasks.api.GtasksApiUtilities; +import com.todoroo.astrid.gtasks.api.GtasksService; +import com.todoroo.astrid.gtasks.auth.GtasksTokenValidator; +import com.todoroo.astrid.gtasks.sync.GtasksSyncOnSaveService; +import com.todoroo.astrid.service.TaskService; +import com.todoroo.astrid.test.DatabaseTestCase; + +public class GtasksSyncOnSaveTest extends DatabaseTestCase { + + @Autowired TaskService taskService; + @Autowired GtasksSyncOnSaveService gtasksSyncOnSaveService; + @Autowired GtasksMetadataService gtasksMetadataService; + @Autowired GtasksPreferenceService gtasksPreferenceService; + + private GtasksService gtasksService; + private boolean initialized = false; + private static final String TEST_ACCOUNT = "sync_tester2@astrid.com"; + private static String DEFAULT_LIST = "@default"; + + //Have to wait a long time because sync on save happens in another thread--currently no way to know when finished + private static final long TIME_TO_WAIT = 8000L; + + + public void testSyncOnCreate() throws IOException { + performBasicCreation(""); + } + + private Task performBasicCreation(String appendToTitle) throws IOException { + String title = "Created task" + appendToTitle; + Task localTask = setupLocalTaskModel(title); + taskService.save(localTask); + + AndroidUtilities.sleepDeep(TIME_TO_WAIT); + + com.google.api.services.tasks.v1.model.Task remoteTask = getRemoteTaskForLocalId(localTask.getId()); + assertEquals(title, remoteTask.title); + return localTask; + } + + private Task setupLocalTaskModel(String title) { + Task localTask = new Task(); + localTask.setValue(Task.TITLE, title); + return localTask; + } + + private com.google.api.services.tasks.v1.model.Task getRemoteTaskForLocalId(long localId) throws IOException { + Metadata gtasksMetadata = gtasksMetadataService.getTaskMetadata(localId); + assertNotNull(gtasksMetadata); + com.google.api.services.tasks.v1.model.Task remoteTask = gtasksService.getGtask(DEFAULT_LIST, gtasksMetadata.getValue(GtasksMetadata.ID)); + assertNotNull(remoteTask); + return remoteTask; + } + + public void testSyncOnTitleUpdate() throws IOException { + Task localTask = performBasicCreation("-title will change"); + + String newTitle = "Title has changed!"; + localTask.setValue(Task.TITLE, newTitle); + taskService.save(localTask); + + AndroidUtilities.sleepDeep(TIME_TO_WAIT); + + com.google.api.services.tasks.v1.model.Task remoteTask = getRemoteTaskForLocalId(localTask.getId()); + assertEquals(newTitle, remoteTask.title); + } + + public void testSyncOnDueDateUpdate() throws IOException { + Task localTask = performBasicCreation("-due date will change"); + + long dueDate = new Date(115, 2, 14).getTime(); + localTask.setValue(Task.DUE_DATE, dueDate); + taskService.save(localTask); + + AndroidUtilities.sleepDeep(TIME_TO_WAIT); + + com.google.api.services.tasks.v1.model.Task remoteTask = getRemoteTaskForLocalId(localTask.getId()); + assertEquals(dueDate, GtasksApiUtilities.gtasksDueTimeToUnixTime(remoteTask.due, 0)); + } + + public void testSyncOnNotesUpdate() throws IOException { + Task localTask = performBasicCreation("-notes will change"); + + String notes = "Noted!"; + localTask.setValue(Task.NOTES, notes); + taskService.save(localTask); + + AndroidUtilities.sleepDeep(TIME_TO_WAIT); + + com.google.api.services.tasks.v1.model.Task remoteTask = getRemoteTaskForLocalId(localTask.getId()); + assertEquals(notes, remoteTask.notes); + } + + public void testSyncOnCompleted() throws IOException { + Task localTask = performBasicCreation("-will be completed"); + + long completionDate = DateUtilities.now(); + localTask.setValue(Task.COMPLETION_DATE, completionDate); + taskService.save(localTask); + + AndroidUtilities.sleepDeep(TIME_TO_WAIT); + + com.google.api.services.tasks.v1.model.Task remoteTask = getRemoteTaskForLocalId(localTask.getId()); + assertEquals("completed", remoteTask.status); + assertEquals(completionDate, GtasksApiUtilities.gtasksCompletedTimeToUnixTime(remoteTask.completed, 0)); + } + + public void testSyncOnDeleted() throws IOException { + Task localTask = performBasicCreation("-will be deleted"); + + long deletionDate = DateUtilities.now(); + localTask.setValue(Task.DELETION_DATE, deletionDate); + taskService.save(localTask); + + AndroidUtilities.sleepDeep(TIME_TO_WAIT); + + com.google.api.services.tasks.v1.model.Task remoteTask = getRemoteTaskForLocalId(localTask.getId()); + assertTrue(remoteTask.deleted); + assertFalse(taskWithTitleExists(localTask.getValue(Task.TITLE))); + } + + private boolean taskWithTitleExists(String title) throws IOException { + Tasks allTasks = gtasksService.getAllGtasksFromListId(DEFAULT_LIST, false); + if (allTasks.items != null) { + for (com.google.api.services.tasks.v1.model.Task t : allTasks.items) { + if (t.title.equals(title)) + return true; + } + } + return false; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (!initialized) { + initializeTestService(); + gtasksSyncOnSaveService.initialize(); + initialized = true; + Preferences.setBoolean(R.string.gtasks_GPr_sync_on_save_key, true); + } + + setupTestList(); + } + + private void initializeTestService() throws Exception { + GoogleAccountManager manager = new GoogleAccountManager(ContextManager.getContext()); + Account[] accounts = manager.getAccounts(); + + Account toUse = null; + for (Account a : accounts) { + if (a.name.equals(TEST_ACCOUNT)) { + toUse = a; + break; + } + } + if (toUse == null) { + toUse = accounts[0]; + } + + Preferences.setString(GtasksPreferenceService.PREF_USER_NAME, toUse.name); + AccountManagerFuture accountManagerFuture = manager.manager.getAuthToken(toUse, "oauth2:https://www.googleapis.com/auth/tasks", true, null, null); + + Bundle authTokenBundle = accountManagerFuture.getResult(); + if (authTokenBundle.containsKey(AccountManager.KEY_INTENT)) { + Intent i = (Intent) authTokenBundle.get(AccountManager.KEY_INTENT); + ContextManager.getContext().startActivity(i); + return; + } + String authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); + authToken = GtasksTokenValidator.validateAuthToken(authToken); + gtasksPreferenceService.setToken(authToken); + + gtasksService = new GtasksService(authToken); + + initialized = true; + } + + private void setupTestList() throws Exception { + Tasks defaultListTasks = gtasksService.getAllGtasksFromListId(DEFAULT_LIST, false); + if (defaultListTasks.items != null) { + for (com.google.api.services.tasks.v1.model.Task t : defaultListTasks.items) { + gtasksService.deleteGtask(DEFAULT_LIST, t.id); + } + } + } +} diff --git a/tests/src/com/todoroo/astrid/gtasks/GtasksTaskListUpdaterTest.java b/tests/src/com/todoroo/astrid/gtasks/GtasksTaskListUpdaterTest.java index 0bce81158..9f10e60fa 100644 --- a/tests/src/com/todoroo/astrid/gtasks/GtasksTaskListUpdaterTest.java +++ b/tests/src/com/todoroo/astrid/gtasks/GtasksTaskListUpdaterTest.java @@ -79,10 +79,10 @@ public class GtasksTaskListUpdaterTest extends DatabaseTestCase { // --- helpers - private void thenExpectMetadataIndentAndOrder(Task task, int order, int indent) { + private void thenExpectMetadataIndentAndOrder(Task task, long order, int indent) { Metadata metadata = gtasksMetadataService.getTaskMetadata(task.getId()); assertNotNull("metadata was found", metadata); - assertEquals("order", order, (int)metadata.getValue(GtasksMetadata.ORDER)); + assertEquals("order", order, metadata.getValue(GtasksMetadata.ORDER).longValue()); assertEquals("indentation", indent, (int)metadata.getValue(GtasksMetadata.INDENT)); } @@ -156,7 +156,7 @@ public class GtasksTaskListUpdaterTest extends DatabaseTestCase { } - private Task createTask(String title, int order, int indent) { + private Task createTask(String title, long order, int indent) { Task task = new Task(); task.setValue(Task.TITLE, title); PluginServices.getTaskService().save(task); diff --git a/tests/src/com/todoroo/astrid/gtasks/GtasksTaskMovingTest.java b/tests/src/com/todoroo/astrid/gtasks/GtasksTaskMovingTest.java index de7179001..d66fe8d9d 100644 --- a/tests/src/com/todoroo/astrid/gtasks/GtasksTaskMovingTest.java +++ b/tests/src/com/todoroo/astrid/gtasks/GtasksTaskMovingTest.java @@ -223,10 +223,10 @@ public class GtasksTaskMovingTest extends DatabaseTestCase { gtasksTaskListUpdater.debugPrint("1"); } - private void thenExpectMetadataOrderAndIndent(Task task, int order, int indent) { + private void thenExpectMetadataOrderAndIndent(Task task, long order, int indent) { Metadata metadata = gtasksMetadataService.getTaskMetadata(task.getId()); assertNotNull("metadata was found", metadata); - assertEquals("order", order, (int)metadata.getValue(GtasksMetadata.ORDER)); + assertEquals("order", order, metadata.getValue(GtasksMetadata.ORDER).longValue()); assertEquals("indentation", indent, (int)metadata.getValue(GtasksMetadata.INDENT)); } @@ -265,7 +265,7 @@ public class GtasksTaskMovingTest extends DatabaseTestCase { return tasks; } - private Task createTask(String title, int order, int indent) { + private Task createTask(String title, long order, int indent) { Task task = new Task(); task.setValue(Task.TITLE, title); PluginServices.getTaskService().save(task);