/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.gtasks.sync; import java.io.IOException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.Semaphore; import android.content.ContentValues; import android.text.TextUtils; import android.util.Log; 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.actfm.sync.ActFmPreferenceService; import com.todoroo.astrid.dao.MetadataDao; import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.SyncFlags; 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.api.CreateRequest; import com.todoroo.astrid.gtasks.api.GtasksApiUtilities; import com.todoroo.astrid.gtasks.api.GtasksInvoker; import com.todoroo.astrid.gtasks.api.MoveRequest; import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.TaskService; public final class GtasksSyncService { private static final String DEFAULT_LIST = "@default"; //$NON-NLS-1$ @Autowired MetadataService metadataService; @Autowired MetadataDao metadataDao; @Autowired GtasksMetadataService gtasksMetadataService; @Autowired TaskDao taskDao; @Autowired GtasksPreferenceService gtasksPreferenceService; @Autowired ActFmPreferenceService actFmPreferenceService; public GtasksSyncService() { DependencyInjectionService.getInstance().inject(this); } private final LinkedBlockingQueue<SyncOnSaveOperation> operationQueue = new LinkedBlockingQueue<SyncOnSaveOperation>(); private abstract class SyncOnSaveOperation { abstract public void op(GtasksInvoker invoker) throws IOException; } private class TaskPushOp extends SyncOnSaveOperation { protected Task model; protected long creationDate = DateUtilities.now(); public TaskPushOp(Task model) { this.model = model; } @Override public void op(GtasksInvoker invoker) throws IOException { if(DateUtilities.now() - creationDate < 1000) AndroidUtilities.sleepDeep(1000 - (DateUtilities.now() - creationDate)); pushTaskOnSave(model, model.getMergedValues(), invoker, false); } } private class MoveOp extends SyncOnSaveOperation { protected Metadata metadata; public MoveOp(Metadata metadata) { this.metadata = metadata; } @Override public void op(GtasksInvoker invoker) throws IOException { pushMetadataOnSave(metadata, invoker); } } private class NotifyOp extends SyncOnSaveOperation { private final Semaphore sema; public NotifyOp(Semaphore sema) { this.sema = sema; } @Override public void op(GtasksInvoker invoker) throws IOException { sema.release(); } } public void initialize() { new OperationPushThread(operationQueue).start(); taskDao.addListener(new ModelUpdateListener<Task>() { public void onModelUpdated(final Task model, boolean outstandingEntries) { if(model.checkAndClearTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC)) return; if (actFmPreferenceService.isLoggedIn()) return; if (gtasksPreferenceService.isOngoing() && !model.checkTransitory(TaskService.TRANS_REPEAT_COMPLETE)) //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, TASK_PROPERTIES)) //None of the properties we sync were updated return; Task toPush = taskDao.fetch(model.getId(), TASK_PROPERTIES); operationQueue.offer(new TaskPushOp(toPush)); } }); } private class OperationPushThread extends Thread { private final LinkedBlockingQueue<SyncOnSaveOperation> queue; public OperationPushThread(LinkedBlockingQueue<SyncOnSaveOperation> queue) { this.queue = queue; } @SuppressWarnings("nls") @Override public void run() { while (true) { SyncOnSaveOperation op; try { op = queue.take(); } catch (InterruptedException e) { continue; } try { GtasksInvoker invoker = new GtasksInvoker(gtasksPreferenceService.getToken()); op.op(invoker); } catch (IOException e) { Log.w("gtasks-sync-error", "Sync on save failed", e); } } } } public void waitUntilEmpty() { Semaphore sema = new Semaphore(0); operationQueue.offer(new NotifyOp(sema)); try { sema.acquire(); } catch (InterruptedException e) { // Ignored } } private static final Property<?>[] TASK_PROPERTIES = { Task.ID, Task.TITLE, Task.NOTES, Task.DUE_DATE, Task.COMPLETION_DATE, Task.DELETION_DATE, Task.USER_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 (property != Task.ID && values.containsKey(property.name)) return true; } return false; } public void triggerMoveForMetadata(final Metadata metadata) { if (metadata == null) return; if (metadata.checkAndClearTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC)) return; if (actFmPreferenceService.isLoggedIn()) return; if (!metadata.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; if (!checkForToken()) return; operationQueue.offer(new MoveOp(metadata)); } /** * Synchronize with server when data changes */ public void pushTaskOnSave(Task task, ContentValues values, GtasksInvoker invoker, boolean sleep) throws IOException { if (actFmPreferenceService.isLoggedIn()) return; if (sleep) AndroidUtilities.sleepDeep(1000L); //Wait for metadata to be saved Metadata gtasksMetadata = gtasksMetadataService.getTaskMetadata(task.getId()); com.google.api.services.tasks.model.Task remoteModel = null; boolean newlyCreated = false; if (values.containsKey(Task.USER_ID.name) && !Task.USER_ID_SELF.equals(values.getAsString(Task.USER_ID.name))) { if (gtasksMetadata != null && !TextUtils.isEmpty(gtasksMetadata.getValue(GtasksMetadata.ID))) { try { invoker.deleteGtask(gtasksMetadata.getValue(GtasksMetadata.LIST_ID), gtasksMetadata.getValue(GtasksMetadata.ID)); metadataDao.delete(gtasksMetadata.getId()); } catch (IOException e) { // } } return; } String remoteId = null; String listId = Preferences.getStringValue(GtasksPreferenceService.PREF_DEFAULT_LIST); if (listId == null) { com.google.api.services.tasks.model.TaskList defaultList = invoker.getGtaskList(DEFAULT_LIST); if (defaultList != null) { listId = defaultList.getId(); Preferences.setString(GtasksPreferenceService.PREF_DEFAULT_LIST, listId); } else { listId = DEFAULT_LIST; } } 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.model.Task(); newlyCreated = true; } else { //update case remoteId = gtasksMetadata.getValue(GtasksMetadata.ID); listId = gtasksMetadata.getValue(GtasksMetadata.LIST_ID); remoteModel = new com.google.api.services.tasks.model.Task(); remoteModel.setId(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. Also don't sync new but already //deleted tasks if (newlyCreated && (!values.containsKey(Task.TITLE.name) || TextUtils.isEmpty(task.getValue(Task.TITLE)) || task.getValue(Task.DELETION_DATE) > 0)) { return; } //Update the remote model's changed properties if (values.containsKey(Task.DELETION_DATE.name) && task.isDeleted()) { remoteModel.setDeleted(true); } if (values.containsKey(Task.TITLE.name)) { remoteModel.setTitle(task.getValue(Task.TITLE)); } if (values.containsKey(Task.NOTES.name)) { remoteModel.setNotes(task.getValue(Task.NOTES)); } if (values.containsKey(Task.DUE_DATE.name) && task.hasDueDate()) { remoteModel.setDue(GtasksApiUtilities.unixTimeToGtasksDueDate(task.getValue(Task.DUE_DATE))); } if (values.containsKey(Task.COMPLETION_DATE.name)) { if (task.isCompleted()) { remoteModel.setCompleted(GtasksApiUtilities.unixTimeToGtasksCompletionTime(task.getValue(Task.COMPLETION_DATE))); remoteModel.setStatus("completed"); //$NON-NLS-1$ } else { remoteModel.setCompleted(null); remoteModel.setStatus("needsAction"); //$NON-NLS-1$ } } if (!newlyCreated) { invoker.updateGtask(listId, remoteModel); } else { String parent = gtasksMetadataService.getRemoteParentId(gtasksMetadata); String priorSibling = gtasksMetadataService.getRemoteSiblingId(listId, gtasksMetadata); CreateRequest create = new CreateRequest(invoker, listId, remoteModel, parent, priorSibling); com.google.api.services.tasks.model.Task created = create.executePush(); if (created != null) { //Update the metadata for the newly created task gtasksMetadata.setValue(GtasksMetadata.ID, created.getId()); gtasksMetadata.setValue(GtasksMetadata.LIST_ID, listId); } else return; } task.setValue(Task.MODIFICATION_DATE, DateUtilities.now()); gtasksMetadata.setValue(GtasksMetadata.LAST_SYNC, DateUtilities.now() + 1000L); metadataService.save(gtasksMetadata); task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true); taskDao.saveExistingWithSqlConstraintCheck(task); } public void pushMetadataOnSave(Metadata model, GtasksInvoker invoker) throws IOException { if (actFmPreferenceService.isLoggedIn()) return; AndroidUtilities.sleepDeep(1000L); String taskId = model.getValue(GtasksMetadata.ID); String listId = model.getValue(GtasksMetadata.LIST_ID); String parent = gtasksMetadataService.getRemoteParentId(model); String priorSibling = gtasksMetadataService.getRemoteSiblingId(listId, model); MoveRequest move = new MoveRequest(invoker, taskId, listId, parent, priorSibling); com.google.api.services.tasks.model.Task result = move.push(); // Update order metadata from result if (result != null) { model.setValue(GtasksMetadata.GTASKS_ORDER, Long.parseLong(result.getPosition())); model.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true); metadataDao.saveExisting(model); } } private boolean checkForToken() { if (!gtasksPreferenceService.isLoggedIn()) return false; return true; } }