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/rmilk-src/org/weloveastrid/rmilk/sync/MilkSyncProvider.java

611 lines
24 KiB
Java

/**
* See the file "LICENSE" for the full license governing this code.
*/
package org.weloveastrid.rmilk.sync;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import org.weloveastrid.rmilk.MilkBackgroundService;
import org.weloveastrid.rmilk.MilkLoginActivity;
import org.weloveastrid.rmilk.MilkPreferences;
import org.weloveastrid.rmilk.MilkUtilities;
import org.weloveastrid.rmilk.MilkLoginActivity.SyncLoginCallback;
import org.weloveastrid.rmilk.api.ApplicationInfo;
import org.weloveastrid.rmilk.api.ServiceImpl;
import org.weloveastrid.rmilk.api.ServiceInternalException;
import org.weloveastrid.rmilk.api.data.RtmList;
import org.weloveastrid.rmilk.api.data.RtmLists;
import org.weloveastrid.rmilk.api.data.RtmTask;
import org.weloveastrid.rmilk.api.data.RtmTaskList;
import org.weloveastrid.rmilk.api.data.RtmTaskNote;
import org.weloveastrid.rmilk.api.data.RtmTaskSeries;
import org.weloveastrid.rmilk.api.data.RtmTasks;
import org.weloveastrid.rmilk.api.data.RtmAuth.Perms;
import org.weloveastrid.rmilk.api.data.RtmTask.Priority;
import org.weloveastrid.rmilk.data.MilkDataService;
import org.weloveastrid.rmilk.data.MilkNoteFields;
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 android.content.res.Resources;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.sync.SyncProvider;
public class MilkSyncProvider extends SyncProvider<MilkTaskContainer> {
private ServiceImpl rtmService = null;
private String timeline = null;
private MilkDataService dataService = null;
// ----------------------------------------------------------------------
// ------------------------------------------------------- public methods
// ----------------------------------------------------------------------
/**
* Sign out of RTM, deleting all synchronization metadata
*/
public void signOut(Context context) {
MilkUtilities.INSTANCE.setToken(null);
MilkUtilities.INSTANCE.clearLastSyncDate();
dataService = MilkDataService.getInstance(context);
dataService.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 showError) {
final Context context = ContextManager.getContext();
MilkUtilities.INSTANCE.setLastError(e.toString());
String message = null;
// occurs when application was closed
if(e instanceof IllegalStateException) {
Log.e(tag, "caught", e); //$NON-NLS-1$
// occurs when network error
} else if(e instanceof ServiceInternalException &&
((ServiceInternalException)e).getEnclosedException() instanceof
IOException) {
Exception enclosedException = ((ServiceInternalException)e).getEnclosedException();
message = context.getString(R.string.rmilk_ioerror);
Log.e(tag, "ioexception", enclosedException); //$NON-NLS-1$
} else {
if(e instanceof ServiceInternalException)
e = ((ServiceInternalException)e).getEnclosedException();
if(e != null)
message = e.toString();
Log.e(tag, "unhandled", e); //$NON-NLS-1$
}
if(showError && context instanceof Activity && message != null) {
DialogUtilities.okDialog((Activity)context, message, null);
}
}
// ----------------------------------------------------------------------
// ------------------------------------------------------ initiating sync
// ----------------------------------------------------------------------
/**
* set up service
*/
@SuppressWarnings("nls")
private void initializeService(String authToken) throws ServiceInternalException {
String appName = null;
String z = stripslashes(0,"q9883o3384n21snq17501qn38oo1r689", "b");
String v = stripslashes(16,"19o2n020345219os","a");
if(authToken == null)
rtmService = new ServiceImpl(new ApplicationInfo(
z, v, appName));
else
rtmService = new ServiceImpl(new ApplicationInfo(
z, v, appName, authToken));
}
/**
* initiate sync in background
*/
@Override
@SuppressWarnings("nls")
protected void initiateBackground(Service service) {
dataService = MilkDataService.getInstance(service);
try {
String authToken = MilkUtilities.INSTANCE.getToken();
// check if we have a token & it works
if(authToken != null) {
initializeService(authToken);
if(!rtmService.isServiceAuthorized()) // re-do login
authToken = null;
}
if(authToken == null) {
// try completing the authorization if it was partial
if(rtmService != null) {
try {
String token = rtmService.completeAuthorization();
MilkUtilities.INSTANCE.setToken(token);
performSync();
return;
} catch (Exception e) {
// didn't work. do the process again.
}
}
// can't do anything, user not logged in
} else {
performSync();
}
} catch (IllegalStateException e) {
// occurs when application was closed
} catch (Exception e) {
handleException("rtm-authenticate", e, true);
} finally {
MilkUtilities.INSTANCE.stopOngoing();
}
}
/**
* If user isn't already signed in, show sign in dialog. Else perform sync.
*/
@SuppressWarnings("nls")
@Override
protected void initiateManual(final Activity activity) {
final Resources r = activity.getResources();
String authToken = MilkUtilities.INSTANCE.getToken();
MilkUtilities.INSTANCE.stopOngoing();
// check if we have a token & it works
if(authToken == null) {
// open up a dialog and have the user go to browser
final String url;
try {
initializeService(null);
url = rtmService.beginAuthorization(Perms.delete);
} catch (Exception e) {
handleException("rmilk-auth", e, true);
return;
}
Intent intent = new Intent(activity, MilkLoginActivity.class);
MilkLoginActivity.setCallback(new SyncLoginCallback() {
public String verifyLogin(final Handler syncLoginHandler) {
if(rtmService == null) {
return null;
}
try {
String token = rtmService.completeAuthorization();
MilkUtilities.INSTANCE.setToken(token);
synchronize(activity);
return null;
} catch (Exception e) {
// didn't work
handleException("rtm-verify-login", e, false);
rtmService = null;
if(e instanceof ServiceInternalException)
e = ((ServiceInternalException)e).getEnclosedException();
return r.getString(R.string.rmilk_MLA_error, e.getMessage());
}
}
});
intent.putExtra(MilkLoginActivity.URL_TOKEN, url);
activity.startActivityForResult(intent, 0);
} else {
activity.startService(new Intent(MilkBackgroundService.SYNC_ACTION, null,
activity, MilkBackgroundService.class));
activity.finish();
}
}
// ----------------------------------------------------------------------
// ----------------------------------------------------- synchronization!
// ----------------------------------------------------------------------
protected void performSync() {
try {
// get RTM timeline
timeline = rtmService.timelines_create();
// load RTM lists
RtmLists lists = rtmService.lists_getList();
dataService.setLists(lists);
// read all tasks
ArrayList<MilkTaskContainer> remoteChanges = new ArrayList<MilkTaskContainer>();
Date lastSyncDate = new Date(MilkUtilities.INSTANCE.getLastSyncDate());
boolean shouldSyncIndividualLists = false;
String filter = null;
if(lastSyncDate.getTime() == 0)
filter = "status:incomplete"; //$NON-NLS-1$ // 1st time sync: get unfinished tasks
// try the quick synchronization
try {
Thread.sleep(2000); // throttle
RtmTasks tasks = rtmService.tasks_getList(null, filter, lastSyncDate);
addTasksToList(tasks, remoteChanges);
} catch (Exception e) {
handleException("rtm-quick-sync", e, false); //$NON-NLS-1$
remoteChanges.clear();
shouldSyncIndividualLists = true;
}
if(shouldSyncIndividualLists) {
for(RtmList list : lists.getLists().values()) {
if(list.isSmart())
continue;
try {
Thread.sleep(1500);
RtmTasks tasks = rtmService.tasks_getList(list.getId(),
filter, lastSyncDate);
addTasksToList(tasks, remoteChanges);
} catch (Exception e) {
handleException("rtm-indiv-sync", e, true); //$NON-NLS-1$
continue;
}
}
}
SyncData<MilkTaskContainer> syncData = populateSyncData(remoteChanges);
try {
synchronizeTasks(syncData);
} finally {
syncData.localCreated.close();
syncData.localUpdated.close();
}
MilkUtilities.INSTANCE.recordSuccessfulSync();
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH);
ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
} catch (IllegalStateException e) {
// occurs when application was closed
} catch (Exception e) {
handleException("rtm-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.NOTES,
};
/**
* Populate SyncData data structure
*/
private SyncData<MilkTaskContainer> populateSyncData(ArrayList<MilkTaskContainer> remoteTasks) {
// fetch locally created tasks
TodorooCursor<Task> localCreated = dataService.getLocallyCreated(PROPERTIES);
// fetch locally updated tasks
TodorooCursor<Task> localUpdated = dataService.getLocallyUpdated(PROPERTIES);
return new SyncData<MilkTaskContainer>(remoteTasks, localCreated, localUpdated);
}
/**
* Add the tasks read from RTM to the given list
*/
private void addTasksToList(RtmTasks tasks, ArrayList<MilkTaskContainer> list) {
for (RtmTaskList taskList : tasks.getLists()) {
for (RtmTaskSeries taskSeries : taskList.getSeries()) {
MilkTaskContainer remote = parseRemoteTask(taskSeries);
// 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);
dataService.findLocalMatch(remote);
list.add(remote);
}
}
}
// ----------------------------------------------------------------------
// ------------------------------------------------- create / push / pull
// ----------------------------------------------------------------------
@Override
protected MilkTaskContainer create(MilkTaskContainer task) throws IOException {
String listId = null;
if(task.listId > 0)
listId = Long.toString(task.listId);
RtmTaskSeries rtmTask = rtmService.tasks_add(timeline, listId,
task.task.getValue(Task.TITLE));
MilkTaskContainer newRemoteTask = parseRemoteTask(rtmTask);
transferIdentifiers(newRemoteTask, task);
push(task, newRemoteTask);
return newRemoteTask;
}
/**
* 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(MilkTaskContainer task, Property<?> property, MilkTaskContainer 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));
}
/**
* 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(MilkTaskContainer local, MilkTaskContainer remote) throws IOException {
boolean remerge = false;
// fetch remote task for comparison
if(remote == null)
remote = pull(local);
String listId = Long.toString(local.listId);
String taskSeriesId = Long.toString(local.taskSeriesId);
String taskId = Long.toString(local.taskId);
if(remote != null && !AndroidUtilities.equals(local.listId, remote.listId))
rtmService.tasks_moveTo(timeline, Long.toString(remote.listId),
listId, taskSeriesId, taskId);
// either delete or re-create if necessary
if(shouldTransmit(local, Task.DELETION_DATE, remote)) {
if(local.task.getValue(Task.DELETION_DATE) > 0)
rtmService.tasks_delete(timeline, listId, taskSeriesId, taskId);
else if(remote == null) {
RtmTaskSeries rtmTask = rtmService.tasks_add(timeline, listId,
local.task.getValue(Task.TITLE));
remote = parseRemoteTask(rtmTask);
transferIdentifiers(remote, local);
}
}
if(shouldTransmit(local, Task.TITLE, remote))
rtmService.tasks_setName(timeline, listId, taskSeriesId,
taskId, local.task.getValue(Task.TITLE));
if(shouldTransmit(local, Task.IMPORTANCE, remote))
rtmService.tasks_setPriority(timeline, listId, taskSeriesId,
taskId, Priority.values(local.task.getValue(Task.IMPORTANCE)));
if(shouldTransmit(local, Task.DUE_DATE, remote))
rtmService.tasks_setDueDate(timeline, listId, taskSeriesId,
taskId, DateUtilities.unixtimeToDate(local.task.getValue(Task.DUE_DATE)),
local.task.hasDueTime());
if(shouldTransmit(local, Task.COMPLETION_DATE, remote)) {
if(local.task.getValue(Task.COMPLETION_DATE) == 0)
rtmService.tasks_uncomplete(timeline, listId, taskSeriesId,
taskId);
else {
rtmService.tasks_complete(timeline, listId, taskSeriesId,
taskId);
// if repeating, pull and merge
if(local.repeating)
remerge = true;
}
}
// tags
HashSet<String> localTags = new HashSet<String>();
HashSet<String> remoteTags = new HashSet<String>();
for(Metadata item : local.metadata)
if(MilkDataService.TAG_KEY.equals(item.getValue(Metadata.KEY)))
localTags.add(item.getValue(Metadata.VALUE1));
if(remote != null && remote.metadata != null) {
for(Metadata item : remote.metadata)
if(MilkDataService.TAG_KEY.equals(item.getValue(Metadata.KEY)))
remoteTags.add(item.getValue(Metadata.VALUE1));
}
if(!localTags.equals(remoteTags)) {
String[] tags = localTags.toArray(new String[localTags.size()]);
rtmService.tasks_setTags(timeline, listId, taskSeriesId,
taskId, tags);
}
// notes
if(shouldTransmit(local, Task.NOTES, remote)) {
String[] titleAndText = MilkNoteFields.fromNoteField(local.task.getValue(Task.NOTES));
List<RtmTaskNote> notes = null;
if(remote != null && remote.remote.getNotes() != null)
notes = remote.remote.getNotes().getNotes();
if(notes != null && notes.size() > 0) {
String remoteNoteId = notes.get(0).getId();
rtmService.tasks_notes_edit(timeline, remoteNoteId, titleAndText[0],
titleAndText[1]);
} else {
rtmService.tasks_notes_add(timeline, listId, taskSeriesId,
taskId, titleAndText[0], titleAndText[1]);
}
}
if(remerge) {
remote = pull(local);
remote.task.setId(local.task.getId());
write(remote);
}
}
/** Create a task container for the given RtmTaskSeries */
private MilkTaskContainer parseRemoteTask(RtmTaskSeries rtmTaskSeries) {
Task task = new Task();
RtmTask rtmTask = rtmTaskSeries.getTask();
ArrayList<Metadata> metadata = new ArrayList<Metadata>();
task.setValue(Task.TITLE, rtmTaskSeries.getName());
task.setValue(Task.CREATION_DATE, DateUtilities.dateToUnixtime(rtmTask.getAdded()));
task.setValue(Task.COMPLETION_DATE, DateUtilities.dateToUnixtime(rtmTask.getCompleted()));
task.setValue(Task.DELETION_DATE, DateUtilities.dateToUnixtime(rtmTask.getDeleted()));
if(rtmTask.getDue() != null) {
task.setValue(Task.DUE_DATE,
task.createDueDate(rtmTask.getHasDueTime() ? Task.URGENCY_SPECIFIC_DAY_TIME :
Task.URGENCY_SPECIFIC_DAY, DateUtilities.dateToUnixtime(rtmTask.getDue())));
} else {
task.setValue(Task.DUE_DATE, 0L);
}
task.setValue(Task.IMPORTANCE, rtmTask.getPriority().ordinal());
if(rtmTaskSeries.getTags() != null) {
for(String tag : rtmTaskSeries.getTags()) {
Metadata tagData = new Metadata();
tagData.setValue(Metadata.KEY, MilkDataService.TAG_KEY);
tagData.setValue(Metadata.VALUE1, tag);
metadata.add(tagData);
}
}
task.setValue(Task.NOTES, ""); //$NON-NLS-1$
if(rtmTaskSeries.getNotes() != null && rtmTaskSeries.getNotes().getNotes().size() > 0) {
boolean firstNote = true;
Collections.reverse(rtmTaskSeries.getNotes().getNotes()); // reverse so oldest is first
for(RtmTaskNote note : rtmTaskSeries.getNotes().getNotes()) {
if(firstNote) {
firstNote = false;
task.setValue(Task.NOTES, MilkNoteFields.toNoteField(note));
} else
metadata.add(MilkNoteFields.create(note));
}
}
MilkTaskContainer container = new MilkTaskContainer(task, metadata, rtmTaskSeries);
return container;
}
@Override
protected MilkTaskContainer pull(MilkTaskContainer task) throws IOException {
if(task.taskSeriesId == 0)
throw new ServiceInternalException("Tried to read an invalid task"); //$NON-NLS-1$
RtmTaskSeries rtmTask = rtmService.tasks_getTask(Long.toString(task.taskSeriesId),
task.task.getValue(Task.TITLE));
if(rtmTask != null)
return parseRemoteTask(rtmTask);
return null;
}
// ----------------------------------------------------------------------
// --------------------------------------------------------- read / write
// ----------------------------------------------------------------------
@Override
protected MilkTaskContainer read(TodorooCursor<Task> cursor) throws IOException {
return dataService.readTaskAndMetadata(cursor);
}
@Override
protected void write(MilkTaskContainer task) throws IOException {
dataService.saveTaskAndMetadata(task);
}
// ----------------------------------------------------------------------
// --------------------------------------------------------- misc helpers
// ----------------------------------------------------------------------
@Override
protected int matchTask(ArrayList<MilkTaskContainer> tasks, MilkTaskContainer target) {
int length = tasks.size();
for(int i = 0; i < length; i++) {
MilkTaskContainer task = tasks.get(i);
if(AndroidUtilities.equals(task.listId, target.listId) &&
AndroidUtilities.equals(task.taskSeriesId, target.taskSeriesId) &&
AndroidUtilities.equals(task.taskId, target.taskId))
return i;
}
return -1;
}
@Override
protected int updateNotification(Context context, Notification notification) {
String notificationTitle = context.getString(R.string.rmilk_notification_title);
Intent intent = new Intent(context, MilkPreferences.class);
PendingIntent notificationIntent = PendingIntent.getActivity(context, 0,
intent, 0);
notification.setLatestEventInfo(context,
notificationTitle, context.getString(R.string.SyP_progress),
notificationIntent);
return 0;
}
@Override
protected void transferIdentifiers(MilkTaskContainer source,
MilkTaskContainer destination) {
destination.listId = source.listId;
destination.taskSeriesId = source.taskSeriesId;
destination.taskId = source.taskId;
}
// ----------------------------------------------------------------------
// ------------------------------------------------------- helper classes
// ----------------------------------------------------------------------
private static final String stripslashes(int ____,String __,String ___) {
int _=__.charAt(____/92);_=_==115?_-1:_;_=((_>=97)&&(_<=123)?
((_-83)%27+97):_);return TextUtils.htmlEncode(____==31?___:
stripslashes(____+1,__.substring(1),___+((char)_)));
}
}