You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tasks/astrid/src/main/java/com/todoroo/astrid/actfm/sync/messages/MakeChanges.java

453 lines
19 KiB
Java

package com.todoroo.astrid.actfm.sync.messages;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Iterator;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.text.TextUtils;
import android.util.Log;
import org.tasks.R;
import com.todoroo.andlib.data.AbstractModel;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Property.StringProperty;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.sync.ActFmInvoker;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.HistoryDao;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.RemoteModelDao;
import com.todoroo.astrid.dao.TagMetadataDao;
import com.todoroo.astrid.dao.TaskListMetadataDao;
import com.todoroo.astrid.dao.UserActivityDao;
import com.todoroo.astrid.data.History;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.TagMetadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskListMetadata;
import com.todoroo.astrid.data.UserActivity;
import com.todoroo.astrid.reminders.Notifications;
import com.todoroo.astrid.reminders.ReminderService;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.tags.TagService;
import com.todoroo.astrid.tags.TaskToTagMetadata;
public class MakeChanges<TYPE extends RemoteModel> extends ServerToClientMessage {
private static final String ERROR_TAG = "actfm-make-changes";
private final RemoteModelDao<TYPE> dao;
private final String table;
public MakeChanges(JSONObject json, RemoteModelDao<TYPE> dao) {
super(json);
this.table = json.optString("table");
this.dao = dao;
}
public static <T extends RemoteModel> T changesToModel(RemoteModelDao<T> dao, JSONObject changes, String table) throws IllegalAccessException, InstantiationException {
T model = dao.getModelClass().newInstance();
JSONChangeToPropertyVisitor visitor = new JSONChangeToPropertyVisitor(model, changes);
Iterator<String> keys = changes.keys();
while (keys.hasNext()) {
String column = keys.next();
Property<?> property = NameMaps.serverColumnNameToLocalProperty(table, column);
if (property != null) { // Unsupported property
property.accept(visitor, column);
}
}
return model;
}
private static <T extends RemoteModel> void saveOrUpdateModelAfterChanges(RemoteModelDao<T> dao, T model, String oldUuid, String uuid, String serverTime, Criterion orCriterion) {
Criterion uuidCriterion;
if (oldUuid == null) {
uuidCriterion = RemoteModel.UUID_PROPERTY.eq(uuid);
} else {
uuidCriterion = RemoteModel.UUID_PROPERTY.eq(oldUuid);
}
if (orCriterion != null) {
uuidCriterion = Criterion.or(uuidCriterion, orCriterion);
}
if (model.getSetValues() != null && model.getSetValues().size() > 0) {
long pushedAt;
try {
pushedAt = DateUtilities.parseIso8601(serverTime);
} catch (ParseException e) {
pushedAt = 0;
}
if (pushedAt > 0) {
model.setValue(RemoteModel.PUSHED_AT_PROPERTY, pushedAt);
}
model.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
if (dao.update(uuidCriterion, model) <= 0) { // If update doesn't update rows. create a new model
model.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
dao.createNew(model);
}
}
}
@Override
public void processMessage(String serverTime) {
JSONObject changes = json.optJSONObject("changes");
String uuid = json.optString("uuid");
if (changes != null && !TextUtils.isEmpty(uuid)) {
if (dao != null) {
try {
TYPE model = changesToModel(dao, changes, table);
StringProperty uuidProperty = (StringProperty) NameMaps.serverColumnNameToLocalProperty(table, "uuid");
String oldUuid = null; // For indicating that a uuid collision has occurred
if (model.getSetValues() != null && model.getSetValues().containsKey(uuidProperty.name)) {
oldUuid = uuid;
uuid = model.getValue(uuidProperty);
}
beforeSaveChanges(changes, model, uuid);
if (model.getSetValues() != null && !model.getSetValues().containsKey(uuidProperty.name)) {
model.setValue(uuidProperty, uuid);
}
saveOrUpdateModelAfterChanges(dao, model, oldUuid, uuid, serverTime, getMatchCriterion(model));
afterSaveChanges(changes, model, uuid, oldUuid);
} catch (IllegalAccessException e) {
Log.e(ERROR_TAG, "Error instantiating model for MakeChanges", e);
} catch (InstantiationException e) {
Log.e(ERROR_TAG, "Error instantiating model for MakeChanges", e);
}
}
}
}
private Criterion getMatchCriterion(TYPE model) {
if (NameMaps.TABLE_ID_TASK_LIST_METADATA.equals(table)) {
if (model.getSetValues().containsKey(TaskListMetadata.FILTER.name)) {
return TaskListMetadata.FILTER.eq(model.getSetValues().getAsString(TaskListMetadata.FILTER.name));
} else if (model.getSetValues().containsKey(TaskListMetadata.TAG_UUID.name)) {
return TaskListMetadata.TAG_UUID.eq(model.getSetValues().getAsString(TaskListMetadata.TAG_UUID.name));
}
}
return null;
}
private void beforeSaveChanges(JSONObject changes, TYPE model, String uuid) {
ChangeHooks beforeSaveChanges = null;
if (NameMaps.TABLE_ID_TASKS.equals(table)) {
beforeSaveChanges = new BeforeSaveTaskChanges(model, changes, uuid);
} else if (NameMaps.TABLE_ID_TAGS.equals(table)) {
beforeSaveChanges = new BeforeSaveTagChanges(model, changes, uuid);
}
if (beforeSaveChanges != null) {
beforeSaveChanges.performChanges();
}
}
private void afterSaveChanges(JSONObject changes, TYPE model, String uuid, String oldUuid) {
ChangeHooks afterSaveChanges = null;
if (NameMaps.TABLE_ID_TASKS.equals(table)) {
afterSaveChanges = new AfterSaveTaskChanges(model, changes, uuid, oldUuid);
} else if (NameMaps.TABLE_ID_TAGS.equals(table)) {
afterSaveChanges = new AfterSaveTagChanges(model, changes, uuid, oldUuid);
} else if (NameMaps.TABLE_ID_USERS.equals(table)) {
afterSaveChanges = new AfterSaveUserChanges(model, changes, uuid);
}
if (afterSaveChanges != null) {
afterSaveChanges.performChanges();
}
}
private abstract class ChangeHooks {
protected final TYPE model;
protected final JSONObject changes;
protected final String uuid;
public ChangeHooks(TYPE model, JSONObject changes, String uuid) {
this.model = model;
this.changes = changes;
this.uuid = uuid;
}
public abstract void performChanges();
protected long getLocalId() {
long localId;
if (!model.isSaved()) { // We don't have the local task id
localId = dao.localIdFromUuid(uuid);
model.setId(localId);
} else {
localId = model.getId();
}
return localId;
}
}
private class BeforeSaveTaskChanges extends ChangeHooks {
public BeforeSaveTaskChanges(TYPE model, JSONObject changes, String uuid) {
super(model, changes, uuid);
}
@Override
public void performChanges() {
//
}
}
private class BeforeSaveTagChanges extends ChangeHooks {
public BeforeSaveTagChanges(TYPE model, JSONObject changes, String uuid) {
super(model, changes, uuid);
}
@Override
public void performChanges() {
JSONArray addMembers = changes.optJSONArray("member_added");
boolean membersAdded = (addMembers != null && addMembers.length() > 0);
if (membersAdded) {
model.setValue(TagData.MEMBERS, ""); // Clear this value for migration purposes
}
}
}
private class AfterSaveTaskChanges extends ChangeHooks {
private final String oldUuid;
public AfterSaveTaskChanges(TYPE model, JSONObject changes, String uuid, String oldUuid) {
super(model, changes, uuid);
this.oldUuid = oldUuid;
}
@Override
public void performChanges() {
if (!TextUtils.isEmpty(oldUuid) && !oldUuid.equals(uuid)) {
uuidChanged(oldUuid, uuid);
}
if (changes.has(NameMaps.localPropertyToServerColumnName(NameMaps.TABLE_ID_TASKS, Task.DUE_DATE)) ||
changes.has(NameMaps.localPropertyToServerColumnName(NameMaps.TABLE_ID_TASKS, Task.COMPLETION_DATE))) {
Task t = PluginServices.getTaskDao().fetch(uuid, ReminderService.NOTIFICATION_PROPERTIES);
if (t != null) {
if ((changes.has("task_repeated") && t.getValue(Task.DUE_DATE) > DateUtilities.now()) || t.getValue(Task.COMPLETION_DATE) > 0) {
Notifications.cancelNotifications(t.getId());
}
ReminderService.getInstance().scheduleAlarm(t);
}
}
JSONArray addTags = changes.optJSONArray("tag_added");
JSONArray removeTags = changes.optJSONArray("tag_removed");
boolean tagsAdded = (addTags != null && addTags.length() > 0);
boolean tagsRemoved = (removeTags != null && removeTags.length() > 0);
if (!tagsAdded && !tagsRemoved) {
return;
}
long localId = AbstractModel.NO_ID;
if (tagsAdded || tagsRemoved) {
localId = getLocalId();
}
if (tagsAdded) {
if (model.isSaved()) {
TagService tagService = TagService.getInstance();
for (int i = 0; i < addTags.length(); i++) {
try {
String tagUuid = addTags.getString(i);
tagService.createLink(model.getId(), uuid, tagUuid, true);
} catch (JSONException e) {
//
}
}
}
}
if (tagsRemoved) {
ArrayList<String> toRemove = new ArrayList<String>(removeTags.length());
for (int i = 0; i < removeTags.length(); i++) {
try {
String tagUuid = removeTags.getString(i);
toRemove.add(tagUuid);
} catch (JSONException e) {
//
}
}
TagService.getInstance().deleteLinks(localId, uuid, toRemove.toArray(new String[toRemove.size()]), true);
}
}
private void uuidChanged(String fromUuid, String toUuid) {
if (ActFmInvoker.SYNC_DEBUG) {
Log.e(ERROR_TAG, "Task UUID collision -- old uuid: " + fromUuid + ", new uuid: " + toUuid);
}
// Update reference from UserActivity to task uuid
UserActivityDao activityDao = PluginServices.getUserActivityDao();
UserActivity activityTemplate = new UserActivity();
activityTemplate.setValue(UserActivity.TARGET_ID, toUuid);
activityTemplate.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
activityDao.update(Criterion.and(UserActivity.ACTION.eq(UserActivity.ACTION_TASK_COMMENT), UserActivity.TARGET_ID.eq(fromUuid)), activityTemplate);
// Update reference from task to tag metadata to task uuid
MetadataService metadataService = PluginServices.getMetadataService();
Metadata taskToTagTemplate = new Metadata();
taskToTagTemplate.setValue(TaskToTagMetadata.TASK_UUID, toUuid);
taskToTagTemplate.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
metadataService.update(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), TaskToTagMetadata.TASK_UUID.eq(fromUuid)), taskToTagTemplate);
HistoryDao historyDao = PluginServices.getHistoryDao();
History histTemplate = new History();
histTemplate.setValue(History.TARGET_ID, toUuid);
historyDao.update(Criterion.and(History.TABLE_ID.eq(NameMaps.TABLE_ID_TAGS), History.TARGET_ID.eq(oldUuid)), histTemplate);
}
}
private class AfterSaveTagChanges extends ChangeHooks {
private final String oldUuid;
public AfterSaveTagChanges(TYPE model, JSONObject changes, String uuid, String oldUuid) {
super(model, changes, uuid);
this.oldUuid = oldUuid;
}
@Override
public void performChanges() {
if (!TextUtils.isEmpty(oldUuid) && !oldUuid.equals(uuid)) {
uuidChanged(oldUuid, uuid);
}
String nameCol = NameMaps.localPropertyToServerColumnName(NameMaps.TABLE_ID_TAGS, TagData.NAME);
String deletedCol = NameMaps.localPropertyToServerColumnName(NameMaps.TABLE_ID_TAGS, TagData.DELETION_DATE);
if (changes.has(nameCol)) {
Metadata template = new Metadata();
template.setValue(TaskToTagMetadata.TAG_NAME, changes.optString(nameCol));
PluginServices.getMetadataService().update(
Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY),
TaskToTagMetadata.TAG_UUID.eq(uuid)), template);
} else if (changes.has(deletedCol)) {
Metadata template = new Metadata();
String valueString = changes.optString(deletedCol);
long deletedValue = 0;
if (!TextUtils.isEmpty(valueString)) {
try {
deletedValue = DateUtilities.parseIso8601(valueString);
} catch (Exception e){
//
}
}
template.setValue(Metadata.DELETION_DATE, deletedValue);
template.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
PluginServices.getMetadataService().update(
Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY),
TaskToTagMetadata.TAG_UUID.eq(uuid)), template);
}
TagMetadataDao tagMetadataDao = PluginServices.getTagMetadataDao();
JSONArray addMembers = changes.optJSONArray("member_added");
JSONArray removeMembers = changes.optJSONArray("member_removed");
boolean membersAdded = (addMembers != null && addMembers.length() > 0);
boolean membersRemoved = (removeMembers != null && removeMembers.length() > 0);
long localId = AbstractModel.NO_ID;
if (membersAdded || membersRemoved) {
localId = getLocalId();
}
if (membersAdded) {
for (int i = 0; i < addMembers.length(); i++) {
try {
String memberId = addMembers.getString(i);
tagMetadataDao.createMemberLink(localId, uuid, memberId, true);
} catch (JSONException e) {
//
}
}
}
if (membersRemoved) {
ArrayList<String> toRemove = new ArrayList<String>(removeMembers.length());
for (int i = 0; i < removeMembers.length(); i++) {
try {
String tagUuid = removeMembers.getString(i);
toRemove.add(tagUuid);
} catch (JSONException e) {
//
}
}
tagMetadataDao.removeMemberLinks(localId, uuid, toRemove.toArray(new String[toRemove.size()]), true);
}
}
private void uuidChanged(String fromUuid, String toUuid) {
if (ActFmInvoker.SYNC_DEBUG) {
Log.e(ERROR_TAG, "Tag UUID collision -- old uuid: " + fromUuid + ", new uuid: " + toUuid);
}
UserActivityDao activityDao = PluginServices.getUserActivityDao();
UserActivity activityTemplate = new UserActivity();
activityTemplate.setValue(UserActivity.TARGET_ID, toUuid);
activityTemplate.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
activityDao.update(Criterion.and(UserActivity.ACTION.eq(UserActivity.ACTION_TAG_COMMENT), UserActivity.TARGET_ID.eq(fromUuid)), activityTemplate);
// Update reference from task to tag metadata to tag uuid
MetadataService metadataService = PluginServices.getMetadataService();
Metadata taskToTagTemplate = new Metadata();
taskToTagTemplate.setValue(TaskToTagMetadata.TAG_UUID, toUuid);
taskToTagTemplate.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
metadataService.update(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), TaskToTagMetadata.TAG_UUID.eq(fromUuid)), taskToTagTemplate);
// Update reference from tag metadata to tag uuid
TagMetadataDao tagMetadataDao = PluginServices.getTagMetadataDao();
TagMetadata memberMetadataTemplate = new TagMetadata();
memberMetadataTemplate.setValue(TagMetadata.TAG_UUID, toUuid);
memberMetadataTemplate.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
tagMetadataDao.update(TagMetadata.TAG_UUID.eq(fromUuid), memberMetadataTemplate);
HistoryDao historyDao = PluginServices.getHistoryDao();
History histTemplate = new History();
histTemplate.setValue(History.TARGET_ID, toUuid);
historyDao.update(Criterion.and(History.TABLE_ID.eq(NameMaps.TABLE_ID_TAGS), History.TARGET_ID.eq(oldUuid)), histTemplate);
TaskListMetadataDao taskListMetadataDao = PluginServices.getTaskListMetadataDao();
TaskListMetadata tlm = new TaskListMetadata();
tlm.setValue(TaskListMetadata.TAG_UUID, toUuid);
tlm.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
taskListMetadataDao.update(TaskListMetadata.TAG_UUID.eq(fromUuid), tlm);
}
}
private class AfterSaveUserChanges extends ChangeHooks {
public AfterSaveUserChanges(TYPE model, JSONObject changes, String uuid) {
super(model, changes, uuid);
}
@Override
public void performChanges() {
}
}
}