First pass at synchronization. All the structural stuff is done, needs to be tested.

pull/14/head
Tim Su 16 years ago
parent db20f92769
commit fd78e22cd8

@ -19,11 +19,13 @@
*/
package com.mdt.rtm;
import java.io.IOException;
/**
*
*
* @author Will Ross Jun 21, 2007
*/
public class ServiceException extends Exception {
public class ServiceException extends IOException {
private static final long serialVersionUID = -6711156026040643361L;

@ -40,6 +40,7 @@ abstract public class AbstractController {
protected static final String TAG_TABLE_NAME = "tags";
protected static final String TAG_TASK_MAP_NAME = "tagTaskMap";
protected static final String ALERT_TABLE_NAME = "alerts";
protected static final String SYNC_TABLE_NAME = "sync";
// cursor iterator

@ -33,7 +33,7 @@ import com.timsu.astrid.data.AbstractModel;
import com.timsu.astrid.data.task.TaskIdentifier;
/** A single tag on a task */
/** A single alert on a task */
public class Alert extends AbstractModel {
/** Version number of this model */
@ -116,7 +116,7 @@ public class Alert extends AbstractModel {
}
public TaskIdentifier getTask() {
return new TaskIdentifier(retrieveInteger(TASK));
return new TaskIdentifier(retrieveLong(TASK));
}
public Date getDate() {

@ -0,0 +1,127 @@
/*
* ASTRID: Android's Simple Task Recording Dashboard
*
* Copyright (c) 2009 Tim Su
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.timsu.astrid.data.sync;
import java.util.HashSet;
import java.util.Set;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import com.timsu.astrid.data.AbstractController;
import com.timsu.astrid.data.sync.SyncMapping.SyncMappingDatabaseHelper;
import com.timsu.astrid.data.task.TaskIdentifier;
/** Controller for Tag-related operations */
public class SyncDataController extends AbstractController {
private SQLiteDatabase syncDatabase;
// --- updated tasks list
/** Mark all updated tasks as finished synchronizing */
public boolean clearUpdatedTaskList(int syncServiceId) throws SQLException {
ContentValues values = new ContentValues();
values.put(SyncMapping.UPDATED, 0);
return syncDatabase.update(SYNC_TABLE_NAME, values,
SyncMapping.SYNC_SERVICE + " = " + syncServiceId, null) > 0;
}
/** Indicate that this task's properties were updated */
public boolean addToUpdatedList(TaskIdentifier taskId) throws SQLException {
ContentValues values = new ContentValues();
values.put(SyncMapping.UPDATED, 1);
return syncDatabase.update(SYNC_TABLE_NAME, values,
SyncMapping.TASK + " = " + taskId.getId(), null) > 0;
}
// --- sync mapping
/** Get all mappings for the given synchronization service */
public Set<SyncMapping> getSyncMapping(int syncServiceId) throws SQLException {
Set<SyncMapping> list = new HashSet<SyncMapping>();
Cursor cursor = syncDatabase.query(SYNC_TABLE_NAME,
SyncMapping.FIELD_LIST,
SyncMapping.SYNC_SERVICE + " = " + syncServiceId,
null, null, null, null);
try {
if(cursor.getCount() == 0)
return list;
do {
cursor.moveToNext();
list.add(new SyncMapping(cursor));
} while(!cursor.isLast());
return list;
} finally {
cursor.close();
}
}
/** Saves the given task to the database. Returns true on success. */
public boolean saveSyncMapping(SyncMapping mapping) {
long newRow = syncDatabase.insert(SYNC_TABLE_NAME, SyncMapping.TASK,
mapping.getMergedValues());
return newRow >= 0;
}
/** Deletes the given mapping. Returns true on success */
public boolean deleteSyncMapping(SyncMapping mapping) {
return syncDatabase.delete(SYNC_TABLE_NAME, KEY_ROWID + "=" +
mapping.getId(), null) > 0;
}
// --- boilerplate
/**
* Constructor - takes the context to allow the database to be
* opened/created
*/
public SyncDataController(Context context) {
this.context = context;
}
/**
* Open the notes database. If it cannot be opened, try to create a new
* instance of the database. If it cannot be created, throw an exception to
* signal the failure
*
* @return this (self reference, allowing this to be chained in an
* initialization call)
* @throws SQLException if the database could be neither opened or created
*/
public SyncDataController open() throws SQLException {
syncDatabase = new SyncMappingDatabaseHelper(context,
SYNC_TABLE_NAME, SYNC_TABLE_NAME).getWritableDatabase();
return this;
}
/** Closes database resource */
public void close() {
syncDatabase.close();
}
}

@ -0,0 +1,167 @@
/*
* ASTRID: Android's Simple Task Recording Dashboard
*
* Copyright (c) 2009 Tim Su
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.timsu.astrid.data.sync;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import com.timsu.astrid.data.AbstractController;
import com.timsu.astrid.data.AbstractModel;
import com.timsu.astrid.data.task.TaskIdentifier;
import com.timsu.astrid.sync.TaskProxy;
/** A single tag on a task */
public class SyncMapping extends AbstractModel {
/** Version number of this model */
static final int VERSION = 1;
// field names
static final String TASK = "task";
static final String SYNC_SERVICE = "service";
static final String REMOTE_ID = "remoteId";
static final String UPDATED = "updated";
/** Default values container */
private static final ContentValues defaultValues = new ContentValues();
static {
defaultValues.put(UPDATED, 0);
}
@Override
public ContentValues getDefaultValues() {
return defaultValues;
}
static String[] FIELD_LIST = new String[] {
AbstractController.KEY_ROWID,
TASK,
SYNC_SERVICE,
REMOTE_ID,
UPDATED,
};
// --- database helper
/** Database Helper manages creating new tables and updating old ones */
static class SyncMappingDatabaseHelper extends SQLiteOpenHelper {
String tableName;
SyncMappingDatabaseHelper(Context context, String databaseName, String tableName) {
super(context, databaseName, null, VERSION);
this.tableName = tableName;
}
@Override
public void onCreate(SQLiteDatabase db) {
String sql = new StringBuilder().
append("CREATE TABLE ").append(tableName).append(" (").
append(AbstractController.KEY_ROWID).append(" integer primary key autoincrement, ").
append(TASK).append(" integer not null,").
append(SYNC_SERVICE).append(" integer not null,").
append(REMOTE_ID).append(" text not null,").
append(UPDATED).append(" integer not null,").
append("unique (").append(TASK).append(",").append(SYNC_SERVICE).append(")").
append(");").toString();
db.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(getClass().getSimpleName(), "Upgrading database from version " +
oldVersion + " to " + newVersion + ".");
switch(oldVersion) {
default:
// we don't know how to handle it... do the unfortunate thing
Log.e(getClass().getSimpleName(), "Unsupported migration, table dropped!");
db.execSQL("DROP TABLE IF EXISTS " + tableName);
onCreate(db);
}
}
}
// --- constructor pass-through
public SyncMapping(TaskIdentifier task, TaskProxy taskProxy) {
this(task, taskProxy.getSyncServiceId(), taskProxy.getRemoteId());
}
public SyncMapping(TaskIdentifier task, int syncServiceId, String remoteId) {
super();
setTask(task);
setSyncServiceId(syncServiceId);
setRemoteId(remoteId);
}
SyncMapping(Cursor cursor) {
super(cursor);
getId();
getTask();
getSyncServiceId();
getRemoteId();
}
// --- getters and setters
public long getId() {
return retrieveLong(AbstractController.KEY_ROWID);
}
public TaskIdentifier getTask() {
return new TaskIdentifier(retrieveLong(TASK));
}
public int getSyncServiceId() {
return retrieveInteger(SYNC_SERVICE);
}
public String getRemoteId() {
return retrieveString(REMOTE_ID);
}
public boolean isUpdated() {
return retrieveInteger(UPDATED) == 1;
}
private void setTask(TaskIdentifier task) {
setValues.put(TASK, task.getId());
}
private void setSyncServiceId(int id) {
setValues.put(SYNC_SERVICE, id);
}
private void setRemoteId(String remoteId) {
setValues.put(REMOTE_ID, remoteId);
}
private void setUpdated(boolean updated) {
setValues.put(UPDATED, updated ? 1 : 0);
}
}

@ -103,7 +103,7 @@ public class TaskController extends AbstractController {
null, null, null, null, null, null);
}
/** Create a weighted list of tasks from the db cursor given */
/** Create a list of tasks from the db cursor given */
public List<TaskModelForList> createTaskListFromCursor(Cursor cursor) {
List<TaskModelForList> list = new ArrayList<TaskModelForList>();
@ -118,6 +118,23 @@ public class TaskController extends AbstractController {
return list;
}
/** Get identifiers for all tasks */
public Set<TaskIdentifier> getAllTaskIdentifiers() {
Set<TaskIdentifier> list = new HashSet<TaskIdentifier>();
Cursor cursor = database.query(TASK_TABLE_NAME, new String[] { KEY_ROWID },
null, null, null, null, null, null);
if(cursor.getCount() == 0)
return list;
do {
cursor.moveToNext();
list.add(new TaskIdentifier(cursor.getInt(
cursor.getColumnIndexOrThrow(KEY_ROWID))));
} while(!cursor.isLast());
return list;
}
/** Create a weighted list of tasks from the db cursor given */
public Cursor getTaskListCursorById(List<TaskIdentifier> idList) {
@ -244,6 +261,14 @@ public class TaskController extends AbstractController {
return model;
}
/** Returns a TaskModelForView corresponding to the given TaskIdentifier */
public TaskModelForSync fetchTaskForSync(TaskIdentifier taskId) throws SQLException {
Cursor cursor = fetchTaskCursor(taskId, TaskModelForSync.FIELD_LIST);
TaskModelForSync model = new TaskModelForSync(cursor);
cursor.close();
return model;
}
/** Returns a TaskModelForView corresponding to the given TaskIdentifier */
public TaskModelForNotify fetchTaskForNotify(TaskIdentifier taskId) throws SQLException {
Cursor cursor = fetchTaskCursor(taskId, TaskModelForNotify.FIELD_LIST);

@ -0,0 +1,200 @@
/*
* ASTRID: Android's Simple Task Recording Dashboard
*
* Copyright (c) 2009 Tim Su
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.timsu.astrid.data.task;
import java.util.Date;
import android.database.Cursor;
import com.timsu.astrid.data.enums.Importance;
/** Fields that you would want to synchronize in the TaskModel */
public class TaskModelForSync extends AbstractTaskModel {
static String[] FIELD_LIST = new String[] {
NAME,
IMPORTANCE,
ESTIMATED_SECONDS,
ELAPSED_SECONDS,
DEFINITE_DUE_DATE,
PREFERRED_DUE_DATE,
HIDDEN_UNTIL,
BLOCKING_ON,
PROGRESS_PERCENTAGE,
CREATION_DATE,
COMPLETION_DATE,
NOTES,
REPEAT,
};
// --- constructors
public TaskModelForSync() {
super();
setCreationDate(new Date());
}
public TaskModelForSync(Cursor cursor) {
super(cursor);
prefetchData(FIELD_LIST);
}
// --- getters and setters
@Override
public boolean isTaskCompleted() {
return super.isTaskCompleted();
}
@Override
public Date getDefiniteDueDate() {
return super.getDefiniteDueDate();
}
@Override
public Integer getEstimatedSeconds() {
return super.getEstimatedSeconds();
}
@Override
public int getProgressPercentage() {
return super.getProgressPercentage();
}
@Override
public Date getCreationDate() {
return super.getCreationDate();
}
@Override
public Date getCompletionDate() {
return super.getCompletionDate();
}
@Override
public Integer getElapsedSeconds() {
return super.getElapsedSeconds();
}
@Override
public Date getHiddenUntil() {
return super.getHiddenUntil();
}
@Override
public Importance getImportance() {
return super.getImportance();
}
@Override
public String getName() {
return super.getName();
}
@Override
public String getNotes() {
return super.getNotes();
}
@Override
public Date getPreferredDueDate() {
return super.getPreferredDueDate();
}
@Override
public TaskIdentifier getBlockingOn() {
return super.getBlockingOn();
}
@Override
public RepeatInfo getRepeat() {
return super.getRepeat();
}
@Override
public void setDefiniteDueDate(Date definiteDueDate) {
super.setDefiniteDueDate(definiteDueDate);
}
@Override
public void setEstimatedSeconds(Integer estimatedSeconds) {
super.setEstimatedSeconds(estimatedSeconds);
}
@Override
public void setElapsedSeconds(int elapsedSeconds) {
super.setElapsedSeconds(elapsedSeconds);
}
@Override
public void setHiddenUntil(Date hiddenUntil) {
super.setHiddenUntil(hiddenUntil);
}
@Override
public void setImportance(Importance importance) {
super.setImportance(importance);
}
@Override
public void setName(String name) {
super.setName(name);
}
@Override
public void setNotes(String notes) {
super.setNotes(notes);
}
@Override
public void setPreferredDueDate(Date preferredDueDate) {
super.setPreferredDueDate(preferredDueDate);
}
@Override
public void setBlockingOn(TaskIdentifier blockingOn) {
super.setBlockingOn(blockingOn);
}
@Override
public void setRepeat(RepeatInfo taskRepeat) {
super.setRepeat(taskRepeat);
}
@Override
public void setCompletionDate(Date completionDate) {
super.setCompletionDate(completionDate);
}
@Override
public void setCreationDate(Date creationDate) {
super.setCreationDate(creationDate);
}
@Override
public void setProgressPercentage(int progressPercentage) {
super.setProgressPercentage(progressPercentage);
}
}

@ -1,6 +1,9 @@
package com.timsu.astrid.sync;
import java.util.Map.Entry;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import android.app.Activity;
import android.content.DialogInterface;
@ -10,21 +13,35 @@ import android.net.Uri;
import android.util.Log;
import com.mdt.rtm.ApplicationInfo;
import com.mdt.rtm.ServiceException;
import com.mdt.rtm.ServiceImpl;
import com.mdt.rtm.data.RtmList;
import com.mdt.rtm.data.RtmLists;
import com.mdt.rtm.data.RtmTask;
import com.mdt.rtm.data.RtmTaskList;
import com.mdt.rtm.data.RtmTaskNote;
import com.mdt.rtm.data.RtmTaskSeries;
import com.mdt.rtm.data.RtmTasks;
import com.mdt.rtm.data.RtmAuth.Perms;
import com.mdt.rtm.data.RtmTask.Priority;
import com.timsu.astrid.R;
import com.timsu.astrid.data.enums.Importance;
import com.timsu.astrid.data.sync.SyncMapping;
import com.timsu.astrid.utilities.DialogUtilities;
import com.timsu.astrid.utilities.Preferences;
public class RTMSyncService implements SynchronizationService {
public class RTMSyncService extends SynchronizationService {
private ServiceImpl rtmService = null;
private int id;
private static final String ASTRID_LIST_NAME = "Astrid";
public RTMSyncService(int id) {
this.id = id;
super(id);
}
@Override
String getName() {
return "RTM";
}
@Override
@ -33,7 +50,7 @@ public class RTMSyncService implements SynchronizationService {
}
@Override
public void synchronizationDisabled(Activity activity) {
public void clearPersonalData(Activity activity) {
Preferences.setSyncRTMToken(activity, null);
}
@ -45,7 +62,7 @@ public class RTMSyncService implements SynchronizationService {
String appName = null;
String authToken = Preferences.getSyncRTMToken(activity);
// check if our auth token works
// check if we have a token & it works
if(authToken != null) {
rtmService = new ServiceImpl(new ApplicationInfo(
apiKey, sharedSecret, appName, authToken));
@ -54,7 +71,7 @@ public class RTMSyncService implements SynchronizationService {
}
if(authToken == null) {
// try completing the authorization.
// try completing the authorization if it was partial
if(rtmService != null) {
try {
String token = rtmService.completeAuthorization();
@ -88,32 +105,141 @@ public class RTMSyncService implements SynchronizationService {
}
} catch (Exception e) {
Synchronizer.showError(activity, e);
showError(activity, e);
}
}
private void performSync(Activity activity) {
try {
Log.i("astrid", "isAuthorized: " + rtmService.isServiceAuthorized());
// get RTM timeline
final String timeline = rtmService.timelines_create();
// get / create astrid list
RtmList astridList = null;
RtmLists lists = rtmService.lists_getList();
for(Entry<String, RtmList> list : lists.getLists().entrySet()) {
Log.i("astrid", "look, " + list.getKey());
for(RtmList list : lists.getLists().values()) {
if(ASTRID_LIST_NAME.equals(list.getName())) {
astridList = list;
break;
}
}
if(astridList == null)
astridList = rtmService.lists_add(timeline, ASTRID_LIST_NAME);
final RtmList newTaskCreationList = astridList;
RtmTasks tasks = rtmService.tasks_getList(null, null,
Preferences.getSyncRTMLastSync(activity));
List<TaskProxy> remoteChanges = new LinkedList<TaskProxy>();
for(RtmTaskList taskList : tasks.getLists()) {
for(RtmTaskSeries taskSeries : taskList.getSeries()) {
TaskProxy remoteTask = parseRemoteTask(taskList.getId(), taskSeries);
remoteChanges.add(remoteTask);
}
}
// fetch tasks that've changed since last sync
synchronizeTasks(activity, remoteChanges, new SynchronizeHelper() {
@Override
public String createTask() throws IOException {
RtmTaskSeries s = rtmService.tasks_add(timeline,
newTaskCreationList.getId(), "tmp");
return new RtmId(newTaskCreationList.getId(), s).toString();
}
@Override
public void deleteTask(SyncMapping mapping) throws IOException {
RtmId id = new RtmId(mapping.getRemoteId());
rtmService.tasks_delete(timeline, id.listId, id.taskSeriesId,
id.taskId);
}
@Override
public void pushTask(TaskProxy task, SyncMapping mapping) throws IOException {
pushLocalTask(timeline, task, mapping);
}
@Override
public TaskProxy refetchTask(TaskProxy task) throws IOException {
RtmId id = new RtmId(task.getRemoteId());
RtmTaskSeries rtmTask = rtmService.tasks_getTask(task.getRemoteId(),
task.name);
return parseRemoteTask(id.listId, rtmTask);
}
});
// grab my own list of tasks that have changed since last sync
} catch (Exception e) {
showError(activity, e);
}
}
/** Helper class for processing RTM id's into one field */
private static class RtmId {
String taskId;
String taskSeriesId;
String listId;
public RtmId(String listId, RtmTaskSeries taskSeries) {
this.taskId = taskSeries.getTask().getId();
this.taskSeriesId = taskSeries.getId();
this.listId = listId;
}
// if we find a conflict... remember and ignore
public RtmId(String id) {
StringTokenizer strtok = new StringTokenizer(id, "|");
taskId = strtok.nextToken();
taskSeriesId = strtok.nextToken();
listId = strtok.nextToken();
}
@Override
public String toString() {
return taskId + "|" + taskSeriesId + "|" + listId;
}
}
// update tasks that have changed
/** Send changes for the given TaskProxy across the wire */
public void pushLocalTask(String timeline, TaskProxy task, SyncMapping mapping)
throws ServiceException {
RtmId id = new RtmId(mapping.getRemoteId());
if(task.name != null)
rtmService.tasks_setName(timeline, id.listId, id.taskSeriesId,
id.taskId, task.name);
if(task.importance != null)
rtmService.tasks_setPriority(timeline, id.listId, id.taskSeriesId,
id.taskId, Priority.values()[task.importance.ordinal()]);
rtmService.tasks_setDueDate(timeline, id.listId, id.taskSeriesId,
id.taskId, task.definiteDueDate, task.definiteDueDate != null);
if(task.progressPercentage != null) {
if(task.progressPercentage == 100)
rtmService.tasks_complete(timeline, id.listId, id.taskSeriesId,
id.taskId);
else
rtmService.tasks_uncomplete(timeline, id.listId, id.taskSeriesId,
id.taskId);
}
}
/** Create a task proxy for the given RtmTaskSeries */
private TaskProxy parseRemoteTask(String listId, RtmTaskSeries rtmTaskSeries) {
TaskProxy task = new TaskProxy(getId(),
new RtmId(listId, rtmTaskSeries).toString(),
rtmTaskSeries.getTask().getDeleted() != null);
} catch (Exception e) {
Synchronizer.showError(activity, e);
task.name = rtmTaskSeries.getName();
StringBuilder sb = new StringBuilder();
for(RtmTaskNote note: rtmTaskSeries.getNotes().getNotes()) {
sb.append(note.getText() + "\n");
}
task.notes = sb.toString();
RtmTask rtmTask = rtmTaskSeries.getTask();
task.creationDate = rtmTaskSeries.getCreated();
task.completionDate = rtmTask.getCompleted();
if(rtmTask.getHasDueTime() > 0)
task.definiteDueDate = rtmTask.getDue();
task.progressPercentage = (rtmTask.getCompleted() == null) ? null : 100;
task.importance = Importance.values()[rtmTask.getPriority().ordinal()];
return task;
}
}

@ -1,17 +1,281 @@
package com.timsu.astrid.sync;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.util.Log;
import com.timsu.astrid.R;
import com.timsu.astrid.data.sync.SyncMapping;
import com.timsu.astrid.data.task.TaskIdentifier;
import com.timsu.astrid.data.task.TaskModelForSync;
import com.timsu.astrid.utilities.DialogUtilities;
/** A service that synchronizes with Astrid
*
* @author timsu
*
*/
public interface SynchronizationService {
public abstract class SynchronizationService {
private int id;
public SynchronizationService(int id) {
this.id = id;
}
/** Synchronize with the service */
void synchronize(Activity activity);
abstract void synchronize(Activity activity);
/** Called when user requests a data clear */
abstract void clearPersonalData(Activity activity);
/** Get this service's id */
public int getId() {
return id;
}
/** Gets this service's name */
abstract String getName();
// --- utilities
/** Utility class for showing synchronization errors */
static void showError(Context context, Throwable e) {
Log.e("astrid", "Synchronization Error", e);
Resources r = context.getResources();
DialogUtilities.okDialog(context,
r.getString(R.string.sync_error) + " " +
e.getLocalizedMessage(), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// do nothing?
}
});
}
// --- synchronization logic
/** interface to assist with synchronization */
protected interface SynchronizeHelper {
/** Push the given task to the remote server.
*
* @param task task proxy to push
* @param mapping local/remote mapping.
*/
void pushTask(TaskProxy task, SyncMapping mapping) throws IOException;
/** Create a task on the remote server
*
* @return remote id
*/
String createTask() throws IOException;
/** Fetch remote task. Used to re-read merged tasks
*
* @param task TaskProxy of the original task
* @return new TaskProxy
*/
TaskProxy refetchTask(TaskProxy task) throws IOException;
/** Delete the task from the remote server
*
* @param mapping mapping to delete
*/
void deleteTask(SyncMapping mapping) throws IOException;
}
/** Helper to synchronize remote tasks with our local database.
*
* This initiates the following process:
* 1. local changes are read
* 2. remote changes are read
* 3. local tasks are merged with remote changes and pushed across
* 4. remote changes are then read in
*
* @param remoteTasks remote tasks that have been updated
* @return local tasks that need to be pushed across
*/
protected void synchronizeTasks(Activity activity, List<TaskProxy> remoteTasks,
SynchronizeHelper helper) throws IOException {
SyncStats stats = new SyncStats();
// get data out of the database
Set<SyncMapping> mappings = Synchronizer.getSyncController().getSyncMapping(getId());
Set<TaskIdentifier> localTasks = Synchronizer.getTaskController().getAllTaskIdentifiers();
// build local maps / lists
Map<String, SyncMapping> remoteIdToSyncMapping =
new HashMap<String, SyncMapping>();
Map<TaskIdentifier, SyncMapping> localIdToSyncMapping =
new HashMap<TaskIdentifier, SyncMapping>();
Set<SyncMapping> localChanges = new HashSet<SyncMapping>();
Set<TaskIdentifier> mappedTasks = new HashSet<TaskIdentifier>();
for(SyncMapping mapping : mappings) {
if(mapping.isUpdated())
localChanges.add(mapping);
remoteIdToSyncMapping.put(mapping.getRemoteId(), mapping);
localIdToSyncMapping.put(mapping.getTask(), mapping);
mappedTasks.add(mapping.getTask());
}
// build remote map
Map<TaskIdentifier, TaskProxy> remoteChangeMap =
new HashMap<TaskIdentifier, TaskProxy>();
for(TaskProxy remoteTask : remoteTasks) {
if(remoteIdToSyncMapping.containsKey(remoteTask.getRemoteId())) {
SyncMapping mapping = remoteIdToSyncMapping.get(remoteTask.getRemoteId());
remoteChangeMap.put(mapping.getTask(), remoteTask);
}
}
// grab tasks without a sync mapping and create them remotely
Set<TaskIdentifier> newlyCreatedTasks = new HashSet<TaskIdentifier>(localTasks);
newlyCreatedTasks.removeAll(mappedTasks);
for(TaskIdentifier taskId : newlyCreatedTasks) {
String remoteId = helper.createTask();
stats.remoteCreatedTasks++;
SyncMapping mapping = new SyncMapping(taskId, getId(), remoteId);
Synchronizer.getSyncController().saveSyncMapping(mapping);
// add it to data structures
localChanges.add(mapping);
}
// find deleted tasks and remove them from the list
Set<TaskIdentifier> deletedTasks = new HashSet<TaskIdentifier>(mappedTasks);
deletedTasks.removeAll(localTasks);
for(TaskIdentifier taskId : deletedTasks) {
stats.remoteDeletedTasks++;
SyncMapping mapping = localIdToSyncMapping.get(taskId);
Synchronizer.getSyncController().deleteSyncMapping(mapping);
helper.deleteTask(mapping);
// remove it from data structures
localChanges.remove(mapping);
remoteIdToSyncMapping.remove(mapping);
remoteChangeMap.remove(taskId);
}
// for each updated local task
for(SyncMapping mapping : localChanges) {
TaskProxy localTask = new TaskProxy(getId(), mapping.getRemoteId(), false);
TaskModelForSync task = Synchronizer.getTaskController().fetchTaskForSync(
mapping.getTask());
localTask.readFromTaskModel(task);
// if there is a conflict, merge
TaskProxy remoteConflict = null;
if(remoteChangeMap.containsKey(mapping.getTask())) {
remoteConflict = remoteChangeMap.get(mapping.getTask());
localTask.mergeWithOther(remoteConflict);
stats.mergedTasks++;
}
helper.pushTask(localTask, mapping);
// re-fetch remote task
if(remoteConflict != null) {
TaskProxy newTask = helper.refetchTask(remoteConflict);
remoteTasks.remove(remoteConflict);
remoteTasks.add(newTask);
}
stats.remoteUpdatedTasks++;
}
stats.remoteUpdatedTasks -= stats.remoteCreatedTasks;
// load remote information
for(TaskProxy remoteTask : remoteTasks) {
SyncMapping mapping = null;
TaskModelForSync task = null;
// if it's new, create a new task model
if(!remoteIdToSyncMapping.containsKey(remoteTask.getRemoteId())) {
// if it's new & deleted, forget about it
if(remoteTask.isDeleted()) {
continue;
}
task = new TaskModelForSync();
stats.localCreatedTasks++;
} else {
mapping = remoteIdToSyncMapping.get(remoteTask.getRemoteId());
if(remoteTask.isDeleted()) {
Synchronizer.getTaskController().deleteTask(mapping.getTask());
Synchronizer.getSyncController().deleteSyncMapping(mapping);
stats.localDeletedTasks++;
continue;
}
task = Synchronizer.getTaskController().fetchTaskForSync(
mapping.getTask());
}
// save the data
remoteTask.writeToTaskModel(task);
Synchronizer.getTaskController().saveTask(task);
stats.localUpdatedTasks++;
if(mapping == null) {
mapping = new SyncMapping(task.getTaskIdentifier(), remoteTask);
Synchronizer.getSyncController().saveSyncMapping(mapping);
}
}
stats.localUpdatedTasks -= stats.localCreatedTasks;
Synchronizer.getSyncController().clearUpdatedTaskList(getId());
stats.showDialog(activity);
}
// --- helper classes
private class SyncStats {
int localCreatedTasks = 0;
int localUpdatedTasks = 0;
int localDeletedTasks = 0;
int mergedTasks = 0;
int remoteCreatedTasks = 0;
int remoteUpdatedTasks = 0;
int remoteDeletedTasks = 0;
/** Display a dialog with statistics */
public void showDialog(Context context) {
if(equals(new SyncStats())) // i.e. no change
return;
StringBuilder sb = new StringBuilder();
sb.append(getName()).append(" Sync Results:"); // TODO i18n
sb.append("\n\nLocal ---");
if(localCreatedTasks > 0)
sb.append("\nCreated: " + localCreatedTasks);
if(localUpdatedTasks > 0)
sb.append("\nUpdated: " + localUpdatedTasks);
if(localDeletedTasks > 0)
sb.append("\nDeleted: " + localDeletedTasks);
if(mergedTasks > 0)
sb.append("\n\nMerged: " + localCreatedTasks);
sb.append("\n\nRemote ---");
if(remoteCreatedTasks > 0)
sb.append("\nCreated: " + remoteCreatedTasks);
if(remoteUpdatedTasks > 0)
sb.append("\nUpdated: " + remoteUpdatedTasks);
if(remoteDeletedTasks > 0)
sb.append("\nDeleted: " + remoteDeletedTasks);
/** Called when synchronization with this service is turned off */
void synchronizationDisabled(Activity activity);
DialogUtilities.okDialog(context, sb.toString(), null);
}
}
}

@ -4,20 +4,42 @@ import java.util.HashMap;
import java.util.Map;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.util.Log;
import com.timsu.astrid.R;
import com.timsu.astrid.utilities.DialogUtilities;
import com.timsu.astrid.data.sync.SyncDataController;
import com.timsu.astrid.data.task.TaskController;
import com.timsu.astrid.utilities.Preferences;
public class Synchronizer {
/* Synchronization Service ID's */
// Synchronization Service ID's
private static final int SYNC_ID_RTM = 1;
// --- public interface
/** Synchronize all activated sync services */
public static void synchronize(final Activity activity) {
// kick off a new thread
new Thread(new Runnable() {
@Override
public void run() {
// RTM sync
if(Preferences.shouldSyncRTM(activity)) {
openControllers(activity);
services.get(SYNC_ID_RTM).synchronize(activity);
}
closeControllers();
}
}, "sync").start();
}
/** Clears tokens if services are disabled */
public static void synchronizerStatusUpdated(Activity activity) {
// do nothing
}
// --- package helpers
/** Service map */
private static Map<Integer, SynchronizationService> services =
new HashMap<Integer, SynchronizationService>();
@ -25,38 +47,39 @@ public class Synchronizer {
services.put(SYNC_ID_RTM, new RTMSyncService(SYNC_ID_RTM));
}
// --- public interface
/** Synchronize all activated sync services */
public static void synchronize(Activity activity) {
// RTM sync
if(Preferences.shouldSyncRTM(activity)) {
services.get(SYNC_ID_RTM).synchronize(activity);
}
static SyncDataController getSyncController() {
return syncController;
}
/** Clears tokens if services are disabled */
public static void synchronizerStatusUpdated(Activity activity) {
if(!Preferences.shouldSyncRTM(activity)) {
services.get(SYNC_ID_RTM).synchronizationDisabled(activity);
}
static TaskController getTaskController() {
return taskController;
}
// --- package utilities
// --- controller stuff
private static SyncDataController syncController = null;
private static TaskController taskController = null;
/** Utility class for showing synchronization errors */
static void showError(Context context, Throwable e) {
Log.e("astrid", "Synchronization Error", e);
private static void openControllers(Activity activity) {
if(syncController == null) {
syncController = new SyncDataController(activity);
syncController.open();
}
Resources r = context.getResources();
DialogUtilities.okDialog(context,
r.getString(R.string.sync_error) + " " +
e.getLocalizedMessage(), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// do nothing?
}
});
if(taskController == null) {
taskController = new TaskController(activity);
taskController.open();
}
}
private static void closeControllers() {
if(syncController != null) {
syncController.close();
syncController = null;
}
if(taskController != null) {
taskController.close();
taskController = null;
}
}
}

@ -0,0 +1,168 @@
package com.timsu.astrid.sync;
import java.util.Date;
import com.timsu.astrid.data.enums.Importance;
import com.timsu.astrid.data.enums.RepeatInterval;
import com.timsu.astrid.data.task.TaskModelForSync;
import com.timsu.astrid.data.task.AbstractTaskModel.RepeatInfo;
/** Representation of a task on a remote server. Your synchronization
* service should instantiate these, filling out every field (use null
* where the field does not exist).
*
* @author timsu
*
*/
public class TaskProxy {
TaskProxy(int syncServiceId, String syncTaskId, boolean isDeleted) {
this.syncServiceId = syncServiceId;
this.syncTaskId = syncTaskId;
this.isDeleted = isDeleted;
}
// --- fill these out
String name = null;
String notes = null;
Importance importance = null;
Integer progressPercentage = null;
Date creationDate = null;
Date completionDate = null;
Date definiteDueDate = null;
Date preferredDueDate = null;
Date hiddenUntil = null;
String[] tags = null;
Integer estimatedSeconds = null;
Integer elapsedSeconds = null;
Integer repeatEveryNSeconds = null;
// --- internal state
/** id of the synchronization service */
private int syncServiceId;
/** id of this particular remote task */
private String syncTaskId;
/** was the task deleted on the remote server */
private boolean isDeleted = false;
public int getSyncServiceId() {
return syncServiceId;
}
public String getRemoteId() {
return syncTaskId;
}
public boolean isDeleted() {
return isDeleted;
}
// --- helper methods
/** Merge with another TaskProxy. Fields in this taskProxy will be overwritten! */
public void mergeWithOther(TaskProxy other) {
if(other == null)
return;
if(other.name != null)
name = other.name;
if(other.notes != null)
notes = other.notes;
if(other.importance != null)
importance = other.importance;
if(other.progressPercentage != null)
progressPercentage = other.progressPercentage;
if(other.creationDate != null)
creationDate = other.creationDate;
if(other.completionDate != null)
completionDate = other.completionDate;
if(other.definiteDueDate != null)
definiteDueDate = other.definiteDueDate;
if(other.preferredDueDate != null)
preferredDueDate = other.preferredDueDate;
if(other.hiddenUntil != null)
hiddenUntil = other.hiddenUntil;
if(other.tags != null)
tags = other.tags;
if(other.estimatedSeconds != null)
estimatedSeconds = other.estimatedSeconds;
if(other.elapsedSeconds != null)
elapsedSeconds = other.elapsedSeconds;
if(other.repeatEveryNSeconds != null)
repeatEveryNSeconds = other.repeatEveryNSeconds;
}
/** Read from the given task model */
public void readFromTaskModel(TaskModelForSync task) {
name = task.getName();
notes = task.getNotes();
importance = task.getImportance();
progressPercentage = task.getProgressPercentage();
creationDate = task.getCreationDate();
completionDate = task.getCompletionDate();
definiteDueDate = task.getDefiniteDueDate();
preferredDueDate = task.getPreferredDueDate();
hiddenUntil = task.getHiddenUntil();
estimatedSeconds = task.getEstimatedSeconds();
elapsedSeconds = task.getElapsedSeconds();
RepeatInfo repeatInfo = task.getRepeat();
if(repeatInfo != null) {
repeatEveryNSeconds = (int)(repeatInfo.shiftDate(new Date(0)).getTime()/1000);
}
}
/** Write to the given task model */
public void writeToTaskModel(TaskModelForSync task) {
if(name != null)
task.setName(name);
if(notes != null)
task.setNotes(notes);
if(importance != null)
task.setImportance(importance);
if(progressPercentage != null)
task.setProgressPercentage(progressPercentage);
if(creationDate != null)
task.setCreationDate(creationDate);
if(completionDate != null)
task.setCompletionDate(completionDate);
if(definiteDueDate != null)
task.setDefiniteDueDate(definiteDueDate);
if(preferredDueDate != null)
task.setPreferredDueDate(preferredDueDate);
if(hiddenUntil != null)
task.setHiddenUntil(hiddenUntil);
// TODO tags
if(estimatedSeconds != null)
task.setEstimatedSeconds(estimatedSeconds);
if(elapsedSeconds != null)
task.setElapsedSeconds(elapsedSeconds);
// this is inaccurate. =/
if(repeatEveryNSeconds != null) {
RepeatInterval repeatInterval;
int repeatValue;
if(repeatEveryNSeconds < 7 * 24 * 3600) {
repeatInterval = RepeatInterval.DAYS;
repeatValue = repeatEveryNSeconds / (24 * 3600);
} else if(repeatEveryNSeconds < 30 * 24 * 3600) {
repeatInterval = RepeatInterval.WEEKS;
repeatValue = repeatEveryNSeconds / (7 * 24 * 3600);
} else {
repeatInterval = RepeatInterval.MONTHS;
repeatValue = repeatEveryNSeconds / (30 * 24 * 3600);
}
task.setRepeat(new RepeatInfo(repeatInterval, repeatValue));
}
}
}

@ -1,5 +1,7 @@
package com.timsu.astrid.utilities;
import java.util.Date;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
@ -15,6 +17,7 @@ public class Preferences {
private static final String P_CURRENT_VERSION = "cv";
private static final String P_SHOW_REPEAT_HELP = "repeathelp";
private static final String P_SYNC_RTM_TOKEN = "rtmtoken";
private static final String P_SYNC_RTM_LAST_SYNC = "rtmlastsync";
// default values
private static final boolean DEFAULT_PERSISTENCE_MODE = true;
@ -132,6 +135,14 @@ public class Preferences {
editor.commit();
}
/** RTM Last Successful Sync Date, or null */
public static Date getSyncRTMLastSync(Context context) {
Long value = getPrefs(context).getLong(P_SYNC_RTM_LAST_SYNC, 0);
if(value == 0)
return null;
return new Date(value);
}
/** Should sync with RTM? */
public static boolean shouldSyncRTM(Context context) {
Resources r = context.getResources();

Loading…
Cancel
Save