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