diff --git a/astrid/.classpath b/astrid/.classpath
index 0025f06fe..22ecde1ad 100644
--- a/astrid/.classpath
+++ b/astrid/.classpath
@@ -14,5 +14,6 @@
+
diff --git a/astrid/AndroidManifest.xml b/astrid/AndroidManifest.xml
index 1f3fadb87..1e9da83d4 100644
--- a/astrid/AndroidManifest.xml
+++ b/astrid/AndroidManifest.xml
@@ -212,7 +212,7 @@
-
+
diff --git a/astrid/libs/todoroo-g.jar b/astrid/libs/todoroo-g.jar
new file mode 100644
index 000000000..4978ab38d
Binary files /dev/null and b/astrid/libs/todoroo-g.jar differ
diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksListService.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksListService.java
index e75d8116f..7eb7780a7 100644
--- a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksListService.java
+++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksListService.java
@@ -1,8 +1,6 @@
package com.todoroo.astrid.gtasks;
-import org.json.JSONArray;
import org.json.JSONException;
-import org.json.JSONObject;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
@@ -11,6 +9,7 @@ import com.todoroo.andlib.sql.Query;
import com.todoroo.astrid.dao.StoreObjectDao;
import com.todoroo.astrid.dao.StoreObjectDao.StoreObjectCriteria;
import com.todoroo.astrid.data.StoreObject;
+import com.todoroo.gtasks.GoogleTaskListInfo;
public class GtasksListService {
@@ -62,12 +61,12 @@ public class GtasksListService {
}
@SuppressWarnings("nls")
- public void updateLists(JSONArray newLists) throws JSONException {
+ public void updateLists(GoogleTaskListInfo[] remoteLists) throws JSONException {
readLists();
- for(int i = 0; i < newLists.length(); i++) {
- JSONObject remote = newLists.getJSONObject(i);
+ for(int i = 0; i < remoteLists.length; i++) {
+ GoogleTaskListInfo remote = remoteLists[i];
- String id = remote.getString("id");
+ String id = remote.getId();
StoreObject local = null;
for(StoreObject list : lists) {
if(list.getValue(GtasksList.REMOTE_ID).equals(id)) {
@@ -81,7 +80,7 @@ public class GtasksListService {
local.setValue(StoreObject.TYPE, GtasksList.TYPE);
local.setValue(GtasksList.REMOTE_ID, id);
- local.setValue(GtasksList.NAME, remote.getString("title"));
+ local.setValue(GtasksList.NAME, remote.getName());
local.setValue(GtasksList.ORDER, i);
storeObjectDao.persist(local);
}
diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadata.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadata.java
index a362c0984..3d5a189ef 100644
--- a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadata.java
+++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksMetadata.java
@@ -20,7 +20,7 @@ public class GtasksMetadata {
public static final String METADATA_KEY = "gtasks"; //$NON-NLS-1$
/** task id in google */
- public static final LongProperty ID = new LongProperty(Metadata.TABLE,
+ public static final StringProperty ID = new StringProperty(Metadata.TABLE,
Metadata.VALUE1.name);
public static final StringProperty LIST_ID = new StringProperty(Metadata.TABLE,
diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncProvider.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncProvider.java
new file mode 100644
index 000000000..9e764cff0
--- /dev/null
+++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncProvider.java
@@ -0,0 +1,472 @@
+/**
+ * See the file "LICENSE" for the full license governing this code.
+ */
+package com.todoroo.astrid.gtasks.sync;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.json.JSONException;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+
+import com.flurry.android.FlurryAgent;
+import com.timsu.astrid.R;
+import com.todoroo.andlib.data.AbstractModel;
+import com.todoroo.andlib.data.Property;
+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.service.ExceptionService;
+import com.todoroo.andlib.utility.AndroidUtilities;
+import com.todoroo.andlib.utility.DateUtilities;
+import com.todoroo.andlib.utility.DialogUtilities;
+import com.todoroo.astrid.data.Metadata;
+import com.todoroo.astrid.data.StoreObject;
+import com.todoroo.astrid.data.Task;
+import com.todoroo.astrid.gtasks.GtasksList;
+import com.todoroo.astrid.gtasks.GtasksListService;
+import com.todoroo.astrid.gtasks.GtasksMetadata;
+import com.todoroo.astrid.gtasks.GtasksMetadataService;
+import com.todoroo.astrid.gtasks.GtasksPreferenceService;
+import com.todoroo.astrid.gtasks.GtasksPreferences;
+import com.todoroo.astrid.producteev.ProducteevBackgroundService;
+import com.todoroo.astrid.producteev.ProducteevLoginActivity;
+import com.todoroo.astrid.producteev.ProducteevUtilities;
+import com.todoroo.astrid.producteev.api.ApiServiceException;
+import com.todoroo.astrid.service.AstridDependencyInjector;
+import com.todoroo.astrid.sync.SyncContainer;
+import com.todoroo.astrid.sync.SyncProvider;
+import com.todoroo.astrid.utility.Constants;
+import com.todoroo.astrid.utility.Preferences;
+import com.todoroo.gtasks.GoogleTaskService;
+import com.todoroo.gtasks.GoogleTaskTask;
+import com.todoroo.gtasks.GoogleTaskView;
+import com.todoroo.gtasks.GoogleTasksException;
+import com.todoroo.gtasks.GoogleTaskService.ConvenientTaskCreator;
+import com.todoroo.gtasks.actions.Action;
+import com.todoroo.gtasks.actions.Actions;
+import com.todoroo.gtasks.actions.GetTasksAction;
+import com.todoroo.gtasks.actions.ListAction;
+import com.todoroo.gtasks.actions.ListActions;
+import com.todoroo.gtasks.actions.ListActions.TaskBuilder;
+import com.todoroo.gtasks.actions.ListActions.TaskModifier;
+
+@SuppressWarnings("nls")
+public class GtasksSyncProvider extends SyncProvider {
+
+ @Autowired private GtasksListService gtasksListService;
+ @Autowired private GtasksMetadataService gtasksMetadataService;
+ @Autowired private GtasksPreferenceService gtasksPreferenceService;
+
+ /** google task service fields */
+ private GoogleTaskService taskService = null;
+ private static final Actions a = new Actions();
+ private static final ListActions l = new ListActions();
+
+ /** batched actions to execute */
+ private final ArrayList actions = new ArrayList();
+ private final HashMap> listActions =
+ new HashMap>();
+
+
+ static {
+ AstridDependencyInjector.initialize();
+ }
+
+ @Autowired
+ protected ExceptionService exceptionService;
+
+ public GtasksSyncProvider() {
+ super();
+ DependencyInjectionService.getInstance().inject(this);
+ }
+
+ // ----------------------------------------------------------------------
+ // ------------------------------------------------------ utility methods
+ // ----------------------------------------------------------------------
+
+ /**
+ * Sign out of service, deleting all synchronization metadata
+ */
+ public void signOut() {
+ gtasksPreferenceService.clearLastSyncDate();
+ gtasksPreferenceService.setToken(null);
+
+ gtasksMetadataService.clearMetadata();
+ }
+
+ /**
+ * Deal with a synchronization exception. If requested, will show an error
+ * to the user (unless synchronization is happening in background)
+ *
+ * @param context
+ * @param tag
+ * error tag
+ * @param e
+ * exception
+ * @param showError
+ * whether to display a dialog
+ */
+ @Override
+ protected void handleException(String tag, Exception e, boolean displayError) {
+ final Context context = ContextManager.getContext();
+ gtasksPreferenceService.setLastError(e.toString());
+
+ String message = null;
+
+ // occurs when application was closed
+ if(e instanceof IllegalStateException) {
+ exceptionService.reportError(tag + "-caught", e); //$NON-NLS-1$
+
+ // occurs when network error
+ } else if(!(e instanceof ApiServiceException) && e instanceof IOException) {
+ message = context.getString(R.string.producteev_ioerror);
+ } else {
+ message = context.getString(R.string.DLG_error, e.toString());
+ exceptionService.reportError(tag + "-unhandled", e); //$NON-NLS-1$
+ }
+
+ if(displayError && context instanceof Activity && message != null) {
+ DialogUtilities.okDialog((Activity)context,
+ message, null);
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // ------------------------------------------------------ initiating sync
+ // ----------------------------------------------------------------------
+
+ /**
+ * initiate sync in background
+ */
+ @Override
+ protected void initiateBackground(Service service) {
+ try {
+ String authToken = gtasksPreferenceService.getToken();
+
+ String email = "tasktest@todoroo.com"; // TODO
+ String password = "tasktest0000";
+
+ taskService = new GoogleTaskService(email, password);
+
+ // check if we have a token & it works
+ if(authToken != null) {
+ taskService.getTaskView();
+ performSync();
+ } else {
+ if (email == null && password == null) {
+ // we can't do anything, user is not logged in
+ } else {
+ authToken = null; // TODO set up auth token
+ performSync();
+ }
+ }
+ } catch (IllegalStateException e) {
+ // occurs when application was closed
+ } catch (Exception e) {
+ handleException("gtasks-authenticate", e, true);
+ } finally {
+ gtasksPreferenceService.stopOngoing();
+ }
+ }
+
+ /**
+ * If user isn't already signed in, show sign in dialog. Else perform sync.
+ */
+ @Override
+ protected void initiateManual(Activity activity) {
+ String authToken = gtasksPreferenceService.getToken();
+ ProducteevUtilities.INSTANCE.stopOngoing();
+
+ // check if we have a token & it works
+ if(authToken == null) {
+ // display login-activity
+ Intent intent = new Intent(activity, ProducteevLoginActivity.class);
+ activity.startActivityForResult(intent, 0);
+ } else {
+ activity.startService(new Intent(ProducteevBackgroundService.SYNC_ACTION, null,
+ activity, ProducteevBackgroundService.class));
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // ----------------------------------------------------- synchronization!
+ // ----------------------------------------------------------------------
+
+ protected void performSync() {
+ FlurryAgent.onEvent("gtasks-started");
+ gtasksPreferenceService.recordSyncStart();
+
+ try {
+ GoogleTaskView taskView = taskService.getTaskView();
+ Preferences.setString(GtasksPreferenceService.PREF_DEFAULT_LIST,
+ taskView.getActiveTaskList().getInfo().getId());
+
+ gtasksListService.updateLists(taskView.getAllLists());
+
+ // batched read tasks for each list
+ ArrayList remoteTasks = new ArrayList();
+ ArrayList getTasksActions = new ArrayList();
+ for(StoreObject dashboard : gtasksListService.getLists()) {
+ String listId = dashboard.getValue(GtasksList.REMOTE_ID);
+ getTasksActions.add(a.getTasks(listId, true));
+ }
+ taskService.executeActions(getTasksActions.toArray(new GetTasksAction[getTasksActions.size()]));
+ for(GetTasksAction action : getTasksActions) {
+ List remoteTasksInList = action.getGoogleTasks();
+ for(GoogleTaskTask remoteTask : remoteTasksInList) {
+ GtasksTaskContainer remote = parseRemoteTask(remoteTask);
+ // update reminder flags for incoming remote tasks to prevent annoying
+ if(remote.task.hasDueDate() && remote.task.getValue(Task.DUE_DATE) < DateUtilities.now())
+ remote.task.setFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AFTER_DEADLINE, false);
+ gtasksMetadataService.findLocalMatch(remote);
+ remoteTasks.add(remote);
+ }
+ }
+
+ SyncData syncData = populateSyncData(remoteTasks);
+ try {
+ synchronizeTasks(syncData);
+ } finally {
+ syncData.localCreated.close();
+ syncData.localUpdated.close();
+ }
+
+ gtasksPreferenceService.recordSuccessfulSync();
+ FlurryAgent.onEvent("gtasks-sync-finished"); //$NON-NLS-1$
+ } catch (IllegalStateException e) {
+ // occurs when application was closed
+ } catch (Exception e) {
+ handleException("gtasks-sync", e, true); //$NON-NLS-1$
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // ------------------------------------------------------------ sync data
+ // ----------------------------------------------------------------------
+
+ // all synchronized properties
+ private static final Property>[] PROPERTIES = new Property>[] {
+ Task.ID,
+ Task.TITLE,
+ Task.IMPORTANCE,
+ Task.DUE_DATE,
+ Task.CREATION_DATE,
+ Task.COMPLETION_DATE,
+ Task.DELETION_DATE,
+ Task.REMINDER_FLAGS,
+ Task.NOTES,
+ };
+
+ /**
+ * Populate SyncData data structure
+ * @throws JSONException
+ */
+ private SyncData populateSyncData(ArrayList remoteTasks) throws JSONException {
+ // fetch locally created tasks
+ TodorooCursor localCreated = gtasksMetadataService.getLocallyCreated(PROPERTIES);
+
+ // fetch locally updated tasks
+ TodorooCursor localUpdated = gtasksMetadataService.getLocallyUpdated(PROPERTIES);
+
+ return new SyncData(remoteTasks, localCreated, localUpdated);
+ }
+
+ // ----------------------------------------------------------------------
+ // ------------------------------------------------- create / push / pull
+ // ----------------------------------------------------------------------
+
+ @Override
+ protected GtasksTaskContainer create(GtasksTaskContainer local) throws IOException {
+ String list = Preferences.getStringValue(GtasksPreferenceService.PREF_DEFAULT_LIST);
+ if(local.gtaskMetadata.containsNonNullValue(GtasksMetadata.LIST_ID))
+ list = local.gtaskMetadata.getValue(GtasksMetadata.LIST_ID);
+
+ ConvenientTaskCreator createdTask;
+ try {
+ createdTask = taskService.createTask(list, local.task.getValue(Task.TITLE));
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+
+ updateTaskHelper(local, null, createdTask);
+ String remoteId = createdTask.go();
+ local.gtaskMetadata.setValue(GtasksMetadata.LIST_ID, remoteId);
+
+ return local;
+ }
+
+ private void updateTaskHelper(GtasksTaskContainer local,
+ GtasksTaskContainer remote, TaskBuilder> builder) {
+
+ String idTask = local.gtaskMetadata.getValue(GtasksMetadata.ID);
+ String idList = local.gtaskMetadata.getValue(GtasksMetadata.LIST_ID);
+
+ // fetch remote task for comparison
+ if(remote == null && idTask != null)
+ remote = pull(local);
+
+ // moving between lists
+ if(remote != null && !idList.equals(remote.gtaskMetadata.getValue(GtasksMetadata.LIST_ID))) {
+ a.moveTask(idTask, idList, remote.gtaskMetadata.getValue(GtasksMetadata.LIST_ID), null);
+ }
+
+ // other properties
+ if(shouldTransmit(local, Task.TITLE, remote))
+ ((TaskModifier)builder).name(local.task.getValue(Task.TITLE));
+ if(shouldTransmit(local, Task.DUE_DATE, remote))
+ builder.taskDate(local.task.getValue(Task.DUE_DATE));
+ if(shouldTransmit(local, Task.COMPLETION_DATE, remote))
+ builder.completed(local.task.isCompleted());
+ if(shouldTransmit(local, Task.DELETION_DATE, remote))
+ builder.deleted(local.task.isDeleted());
+ if(shouldTransmit(local, Task.NOTES, remote))
+ builder.notes(local.task.getValue(Task.NOTES));
+
+ // TODO indentation
+ }
+
+ /** Create a task container for the given RtmTaskSeries
+ * @throws JSONException */
+ private GtasksTaskContainer parseRemoteTask(GoogleTaskTask remoteTask) {
+ Task task = new Task();
+ ArrayList metadata = new ArrayList();
+
+ task.setValue(Task.TITLE, remoteTask.getName());
+ task.setValue(Task.CREATION_DATE, DateUtilities.now());
+ task.setValue(Task.COMPLETION_DATE, remoteTask.getCompleted_date());
+ task.setValue(Task.DELETION_DATE, remoteTask.isDeleted() ? DateUtilities.now() : 0);
+
+ long dueDate = remoteTask.getTask_date();
+ task.setValue(Task.DUE_DATE, task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, dueDate));
+ task.setValue(Task.NOTES, remoteTask.getNotes());
+
+ Metadata gtasksMetadata = GtasksMetadata.createEmptyMetadata(AbstractModel.NO_ID);
+ gtasksMetadata.setValue(GtasksMetadata.LIST_ID, remoteTask.getList_id());
+ // TODO gtasksMetadata.setValue(GtasksMetadata.INDENT, remoteTask.???);
+
+ GtasksTaskContainer container = new GtasksTaskContainer(task, metadata,
+ gtasksMetadata);
+
+ return container;
+ }
+
+ @Override
+ protected GtasksTaskContainer pull(GtasksTaskContainer task) throws IOException {
+ if(!task.gtaskMetadata.containsNonNullValue(GtasksMetadata.ID) ||
+ !task.gtaskMetadata.containsNonNullValue(GtasksMetadata.LIST_ID))
+ throw new ApiServiceException("Tried to read an invalid task"); //$NON-NLS-1$
+
+ String idToMatch = task.gtaskMetadata.getValue(GtasksMetadata.ID);
+ List tasks = taskService.getTasks(task.gtaskMetadata.getValue(GtasksMetadata.LIST_ID));
+ for(GoogleTaskTask remoteTask : tasks) {
+ if(remoteTask.getId().equals(idToMatch)) {
+ return parseRemoteTask(remoteTask);
+ }
+ }
+
+ throw new GoogleTasksException("Could not find remote task to pull.");
+ }
+
+ /**
+ * Send changes for the given Task across the wire. If a remoteTask is
+ * supplied, we attempt to intelligently only transmit the values that
+ * have changed.
+ */
+ @Override
+ protected void push(GtasksTaskContainer local, GtasksTaskContainer remote) throws IOException {
+ TaskModifier modifyTask = l.modifyTask(remote.gtaskMetadata.getValue(GtasksMetadata.ID));
+ updateTaskHelper(local, remote, modifyTask);
+ ListAction action = modifyTask.done();
+ batch(local.gtaskMetadata.getValue(GtasksMetadata.LIST_ID), action);
+ }
+
+ /** add action to batch */
+ private void batch(String list, ListAction action) {
+ if(!listActions.containsKey(list))
+ listActions.put(list, new ArrayList());
+ listActions.get(list).add(action);
+ }
+
+ // ----------------------------------------------------------------------
+ // --------------------------------------------------------- read / write
+ // ----------------------------------------------------------------------
+
+ @Override
+ protected GtasksTaskContainer read(TodorooCursor cursor) throws IOException {
+ return gtasksMetadataService.readTaskAndMetadata(cursor);
+ }
+
+ @Override
+ protected void write(GtasksTaskContainer task) throws IOException {
+ gtasksMetadataService.saveTaskAndMetadata(task);
+ }
+
+ // ----------------------------------------------------------------------
+ // --------------------------------------------------------- misc helpers
+ // ----------------------------------------------------------------------
+
+ @Override
+ protected int matchTask(ArrayList tasks, GtasksTaskContainer target) {
+ int length = tasks.size();
+ for(int i = 0; i < length; i++) {
+ GtasksTaskContainer task = tasks.get(i);
+ if(AndroidUtilities.equals(task.gtaskMetadata, target.gtaskMetadata))
+ return i;
+ }
+ return -1;
+ }
+
+ /**
+ * Determine whether this task's property should be transmitted
+ * @param task task to consider
+ * @param property property to consider
+ * @param remoteTask remote task proxy
+ * @return
+ */
+ private boolean shouldTransmit(SyncContainer task, Property> property, SyncContainer remoteTask) {
+ if(!task.task.containsValue(property))
+ return false;
+
+ if(remoteTask == null)
+ return true;
+ if(!remoteTask.task.containsValue(property))
+ return true;
+
+ // special cases - match if they're zero or nonzero
+ if(property == Task.COMPLETION_DATE ||
+ property == Task.DELETION_DATE)
+ return !AndroidUtilities.equals((Long)task.task.getValue(property) == 0,
+ (Long)remoteTask.task.getValue(property) == 0);
+
+ return !AndroidUtilities.equals(task.task.getValue(property),
+ remoteTask.task.getValue(property));
+ }
+
+ @Override
+ protected int updateNotification(Context context, Notification notification) {
+ String notificationTitle = context.getString(R.string.gtasks_notification_title);
+ Intent intent = new Intent(context, GtasksPreferences.class);
+ PendingIntent notificationIntent = PendingIntent.getActivity(context, 0,
+ intent, 0);
+ notification.setLatestEventInfo(context,
+ notificationTitle, context.getString(R.string.SyP_progress),
+ notificationIntent);
+ return Constants.NOTIFICATION_SYNC;
+ }
+
+ @Override
+ protected void transferIdentifiers(GtasksTaskContainer source,
+ GtasksTaskContainer destination) {
+ destination.gtaskMetadata = source.gtaskMetadata;
+ }
+}