/**
* 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.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.json.JSONException;
import android.app.Activity;
import android.text.TextUtils;
import com.google.api.services.tasks.model.Tasks;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.AbstractModel;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Join;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.StoreObjectDao;
import com.todoroo.astrid.dao.TagDataDao;
import com.todoroo.astrid.dao.TagMetadataDao;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.TagData;
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.GtasksTaskListUpdater;
import com.todoroo.astrid.gtasks.api.GoogleTasksException;
import com.todoroo.astrid.gtasks.api.GtasksApiUtilities;
import com.todoroo.astrid.gtasks.api.GtasksInvoker;
import com.todoroo.astrid.gtasks.auth.GtasksTokenValidator;
import com.todoroo.astrid.service.AstridDependencyInjector;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.StatisticsConstants;
import com.todoroo.astrid.service.StatisticsService;
import com.todoroo.astrid.service.SyncResultCallbackWrapper.WidgetUpdatingCallbackWrapper;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.sync.SyncResultCallback;
import com.todoroo.astrid.sync.SyncV2Provider;
import com.todoroo.astrid.tags.TagService;
public class GtasksSyncV2Provider extends SyncV2Provider {
@Autowired TaskService taskService;
@Autowired MetadataService metadataService;
@Autowired MetadataDao metadataDao;
@Autowired StoreObjectDao storeObjectDao;
@Autowired ActFmPreferenceService actFmPreferenceService;
@Autowired GtasksPreferenceService gtasksPreferenceService;
@Autowired GtasksSyncService gtasksSyncService;
@Autowired GtasksListService gtasksListService;
@Autowired GtasksMetadataService gtasksMetadataService;
@Autowired GtasksTaskListUpdater gtasksTaskListUpdater;
@Autowired TagService tagService;
@Autowired TagDataDao tagDataDao;
@Autowired TagMetadataDao tagMetadataDao;
static {
AstridDependencyInjector.initialize();
}
private static GtasksSyncV2Provider instance = null;
protected GtasksSyncV2Provider() {
// prevent multiple sync providers
}
public synchronized static GtasksSyncV2Provider getInstance() {
if(instance == null)
instance = new GtasksSyncV2Provider();
return instance;
}
@Override
public String getName() {
return ContextManager.getString(R.string.gtasks_GPr_header);
}
@Override
public GtasksPreferenceService getUtilities() {
return gtasksPreferenceService;
}
@Override
public void signOut(Activity activity) {
gtasksPreferenceService.clearLastSyncDate();
gtasksPreferenceService.setToken(null);
Preferences.setString(GtasksPreferenceService.PREF_USER_NAME, null);
gtasksMetadataService.clearMetadata();
}
@Override
public boolean isActive() {
return gtasksPreferenceService.isLoggedIn() && !actFmPreferenceService.isLoggedIn();
}
public static class GtasksImportTuple {
public long taskId;
public String taskName;
public String taskUuid;
public String tagUuid;
public String tagName;
}
public static class GtasksImportCallback extends WidgetUpdatingCallbackWrapper {
protected final ArrayList<GtasksImportTuple> importConflicts;
public GtasksImportCallback(SyncResultCallback wrap) {
super(wrap);
importConflicts = new ArrayList<GtasksImportTuple>();
}
public void addImportConflict(GtasksImportTuple tuple) {
importConflicts.add(tuple);
}
}
@Override
public void synchronizeActiveTasks(final boolean manual, final SyncResultCallback callback) {
// TODO: Improve this logic. Should only be able to import from settings or something.
final boolean isImport = actFmPreferenceService.isLoggedIn();
if (isImport && !manual)
return;
callback.started();
callback.incrementMax(100);
gtasksPreferenceService.recordSyncStart();
new Thread(new Runnable() {
public void run() {
callback.incrementProgress(50);
String authToken = getValidatedAuthToken();
final GtasksInvoker invoker = new GtasksInvoker(authToken);
try {
gtasksListService.updateLists(invoker.allGtaskLists());
} catch (GoogleTasksException e) {
handler.handleException("gtasks-sync=io", e, e.getType()); //$NON-NLS-1$
} catch (IOException e) {
handler.handleException("gtasks-sync=io", e, e.toString()); //$NON-NLS-1$
}
StoreObject[] lists = gtasksListService.getLists();
if (lists.length == 0) {
finishSync(callback);
return;
}
callback.incrementMax(25 * lists.length);
final AtomicInteger finisher = new AtomicInteger(lists.length);
for (final StoreObject list : lists) {
new Thread(new Runnable() {
@Override
public void run() {
synchronizeListHelper(list, invoker, manual, handler, callback, isImport);
callback.incrementProgress(25);
if (finisher.decrementAndGet() == 0) {
if (!isImport)
pushUpdated(invoker, callback);
else
finishImport(callback);
finishSync(callback);
}
}
}).start();
}
}
}).start();
}
private synchronized void pushUpdated(GtasksInvoker invoker, SyncResultCallback callback) {
TodorooCursor<Task> queued = taskService.query(Query.select(Task.PROPERTIES).
join(Join.left(Metadata.TABLE, Criterion.and(MetadataCriteria.withKey(GtasksMetadata.METADATA_KEY), Task.ID.eq(Metadata.TASK)))).where(
Criterion.or(Task.MODIFICATION_DATE.gt(GtasksMetadata.LAST_SYNC),
Criterion.and(Task.USER_ID.neq(Task.USER_ID_SELF), GtasksMetadata.ID.isNotNull()),
Metadata.KEY.isNull())));
callback.incrementMax(queued.getCount() * 10);
try {
Task task = new Task();
for (queued.moveToFirst(); !queued.isAfterLast(); queued.moveToNext()) {
task.readFromCursor(queued);
try {
gtasksSyncService.pushTaskOnSave(task, task.getMergedValues(), invoker, false);
} catch (GoogleTasksException e) {
handler.handleException("gtasks-sync-io", e, e.getType()); //$NON-NLS-1$
} catch (IOException e) {
handler.handleException("gtasks-sync-io", e, e.toString()); //$NON-NLS-1$
} finally {
callback.incrementProgress(10);
}
}
} finally {
queued.close();
}
}
@Override
public void synchronizeList(Object list, final boolean manual, final SyncResultCallback callback) {
if (!(list instanceof StoreObject))
return;
final StoreObject gtasksList = (StoreObject) list;
if (!GtasksList.TYPE.equals(gtasksList.getValue(StoreObject.TYPE)))
return;
final boolean isImport = actFmPreferenceService.isLoggedIn();
callback.started();
callback.incrementMax(100);
new Thread(new Runnable() {
public void run() {
callback.incrementProgress(50);
try {
String authToken = getValidatedAuthToken();
callback.incrementProgress(12);
gtasksSyncService.waitUntilEmpty();
callback.incrementProgress(13);
final GtasksInvoker service = new GtasksInvoker(authToken);
synchronizeListHelper(gtasksList, service, manual, null, callback, isImport);
} finally {
callback.incrementProgress(25);
callback.finished();
}
}
}).start();
}
private String getValidatedAuthToken() {
String authToken = gtasksPreferenceService.getToken();
try {
authToken = GtasksTokenValidator.validateAuthToken(ContextManager.getContext(), authToken);
if (authToken != null)
gtasksPreferenceService.setToken(authToken);
} catch (GoogleTasksException e) {
authToken = null;
}
return authToken;
}
private synchronized void synchronizeListHelper(StoreObject list, GtasksInvoker invoker,
boolean manual, SyncExceptionHandler errorHandler, SyncResultCallback callback, boolean isImport) {
String listId = list.getValue(GtasksList.REMOTE_ID);
long lastSyncDate;
if (!manual && list.containsNonNullValue(GtasksList.LAST_SYNC)) {
lastSyncDate = list.getValue(GtasksList.LAST_SYNC);
} else {
lastSyncDate = 0;
}
boolean includeDeletedAndHidden = lastSyncDate != 0;
try {
Tasks taskList = invoker.getAllGtasksFromListId(listId, includeDeletedAndHidden,
includeDeletedAndHidden, lastSyncDate);
List<com.google.api.services.tasks.model.Task> tasks = taskList.getItems();
if (tasks != null) {
callback.incrementMax(tasks.size() * 10);
HashSet<Long> localIds = new HashSet<Long>(tasks.size());
for (com.google.api.services.tasks.model.Task t : tasks) {
GtasksTaskContainer container = parseRemoteTask(t, listId);
gtasksMetadataService.findLocalMatch(container);
container.gtaskMetadata.setValue(GtasksMetadata.GTASKS_ORDER,
Long.parseLong(t.getPosition()));
container.gtaskMetadata.setValue(GtasksMetadata.PARENT_TASK,
gtasksMetadataService.localIdForGtasksId(t.getParent()));
container.gtaskMetadata.setValue(GtasksMetadata.LAST_SYNC,
DateUtilities.now() + 1000L);
write(container);
localIds.add(container.task.getId());
callback.incrementProgress(10);
}
list.setValue(GtasksList.LAST_SYNC, DateUtilities.now());
storeObjectDao.persist(list);
if(lastSyncDate == 0 && !isImport) {
Long[] localIdArray = localIds.toArray(new Long[localIds.size()]);
Criterion delete = Criterion.and(Metadata.KEY.eq(GtasksMetadata.METADATA_KEY),
GtasksMetadata.LIST_ID.eq(listId),
Criterion.not(Metadata.TASK.in(localIdArray)));
taskService.deleteWhere(
Task.ID.in(Query.select(Metadata.TASK).from(Metadata.TABLE).
where(delete)));
metadataService.deleteWhere(delete);
}
gtasksTaskListUpdater.correctOrderAndIndentForList(listId);
}
} catch (GoogleTasksException e) {
if (errorHandler != null)
errorHandler.handleException("gtasks-sync-io", e, e.getType()); //$NON-NLS-1$
} catch (IOException e) {
if (errorHandler != null)
errorHandler.handleException("gtasks-sync-io", e, e.toString()); //$NON-NLS-1$
}
}
/** Create a task container for the given remote task
* @throws JSONException */
private GtasksTaskContainer parseRemoteTask(com.google.api.services.tasks.model.Task remoteTask, String listId) {
Task task = new Task();
ArrayList<Metadata> metadata = new ArrayList<Metadata>();
task.setValue(Task.TITLE, remoteTask.getTitle());
task.setValue(Task.CREATION_DATE, DateUtilities.now());
task.setValue(Task.COMPLETION_DATE, GtasksApiUtilities.gtasksCompletedTimeToUnixTime(remoteTask.getCompleted(), 0));
if (remoteTask.getDeleted() == null || !remoteTask.getDeleted().booleanValue())
task.setValue(Task.DELETION_DATE, 0L);
else if (remoteTask.getDeleted().booleanValue())
task.setValue(Task.DELETION_DATE, DateUtilities.now());
if (remoteTask.getHidden() != null && remoteTask.getHidden().booleanValue())
task.setValue(Task.DELETION_DATE, DateUtilities.now());
long dueDate = GtasksApiUtilities.gtasksDueTimeToUnixTime(remoteTask.getDue(), 0);
long createdDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate);
task.setValue(Task.DUE_DATE, createdDate);
task.setValue(Task.NOTES, remoteTask.getNotes());
Metadata gtasksMetadata = GtasksMetadata.createEmptyMetadata(AbstractModel.NO_ID);
gtasksMetadata.setValue(GtasksMetadata.ID, remoteTask.getId());
gtasksMetadata.setValue(GtasksMetadata.LIST_ID, listId);
GtasksTaskContainer container = new GtasksTaskContainer(task, metadata,
gtasksMetadata);
return container;
}
private void write(GtasksTaskContainer task) throws IOException {
// merge astrid dates with google dates
if (!task.task.isSaved() && actFmPreferenceService.isLoggedIn())
titleMatchWithActFm(task.task);
if(task.task.isSaved()) {
Task local = PluginServices.getTaskService().fetchById(task.task.getId(), Task.DUE_DATE, Task.COMPLETION_DATE);
if (local == null) {
task.task.clearValue(Task.ID);
task.task.clearValue(Task.UUID);
} else {
mergeDates(task.task, local);
if(task.task.isCompleted() && !local.isCompleted())
StatisticsService.reportEvent(StatisticsConstants.GTASKS_TASK_COMPLETED);
}
} else { // Set default importance and reminders for remotely created tasks
task.task.setValue(Task.IMPORTANCE, Preferences.getIntegerFromString(
R.string.p_default_importance_key, Task.IMPORTANCE_SHOULD_DO));
TaskDao.setDefaultReminders(task.task);
}
if (!TextUtils.isEmpty(task.task.getValue(Task.TITLE))) {
task.task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true);
gtasksMetadataService.saveTaskAndMetadata(task);
}
}
private void titleMatchWithActFm(Task task) {
String title = task.getValue(Task.TITLE);
TodorooCursor<Task> match = taskService.query(Query.select(Task.ID, Task.UUID)
.join(Join.left(Metadata.TABLE, Criterion.and(Metadata.KEY.eq(GtasksMetadata.METADATA_KEY), Metadata.TASK.eq(Task.ID))))
.where(Criterion.and(Task.TITLE.eq(title), GtasksMetadata.ID.isNull())));
try {
if (match.getCount() > 0) {
match.moveToFirst();
task.setId(match.get(Task.ID));
task.setUuid(match.get(Task.UUID));
}
} finally {
match.close();
}
}
private void mergeDates(Task remote, Task local) {
if(remote.hasDueDate() && local.hasDueTime()) {
Date newDate = new Date(remote.getValue(Task.DUE_DATE));
Date oldDate = new Date(local.getValue(Task.DUE_DATE));
newDate.setHours(oldDate.getHours());
newDate.setMinutes(oldDate.getMinutes());
newDate.setSeconds(oldDate.getSeconds());
long setDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME,
newDate.getTime());
remote.setValue(Task.DUE_DATE, setDate);
}
}
private void finishImport(SyncResultCallback callback) {
TodorooCursor<Task> tasks = taskService.query(Query.select(Task.ID, Task.UUID, Task.TITLE, GtasksList.NAME)
.join(Join.inner(Metadata.TABLE, Task.ID.eq(Metadata.TASK)))
.join(Join.left(StoreObject.TABLE, GtasksMetadata.LIST_ID.eq(GtasksList.REMOTE_ID)))
.where(MetadataCriteria.withKey(GtasksMetadata.METADATA_KEY)));
GtasksImportCallback gtCallback = null;
if (callback instanceof GtasksImportCallback)
gtCallback = (GtasksImportCallback) callback;
try {
for (tasks.moveToFirst(); !tasks.isAfterLast(); tasks.moveToNext()) {
String listName = tasks.get(GtasksList.NAME);
String tagUuid = RemoteModel.NO_UUID;
if (!TextUtils.isEmpty(listName)) {
TodorooCursor<TagData> existingTag = tagDataDao.query(Query.select(TagData.UUID).where(TagData.NAME.eq(listName)));
try {
if (existingTag.getCount() > 0) {
existingTag.moveToFirst();
tagUuid = existingTag.get(TagData.UUID);
boolean taskIsInTag = metadataDao.taskIsInTag(tasks.get(Task.UUID), tagUuid);
if (tagMetadataDao.tagHasMembers(tagUuid) && !taskIsInTag) {
GtasksImportTuple tuple = new GtasksImportTuple();
tuple.taskId = tasks.get(Task.ID);
tuple.taskName = tasks.get(Task.TITLE);
tuple.taskUuid = tasks.get(Task.UUID);
tuple.tagUuid = tagUuid;
tuple.tagName = listName;
if (gtCallback != null)
gtCallback.addImportConflict(tuple);
continue;
} else if (taskIsInTag) {
continue;
}
} else {
TagData td = new TagData();
td.setValue(TagData.NAME, listName);
tagDataDao.createNew(td);
tagUuid = td.getUuid();
}
} finally {
existingTag.close();
}
if (!RemoteModel.isUuidEmpty(tagUuid)) {
Task task = new Task();
task.setId(tasks.get(Task.ID));
task.setUuid(tasks.get(Task.UUID));
tagService.createLink(task, listName, tagUuid);
}
}
}
} finally {
tasks.close();
}
}
}