mirror of https://github.com/tasks/tasks
First pass at synchronization. All the structural stuff is done, needs to be tested.
parent
db20f92769
commit
fd78e22cd8
@ -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);
|
||||
}
|
||||
}
|
@ -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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue