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;
|
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.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
|
/** A service that synchronizes with Astrid
|
||||||
*
|
*
|
||||||
* @author timsu
|
* @author timsu
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public interface SynchronizationService {
|
public abstract class SynchronizationService {
|
||||||
|
|
||||||
|
private int id;
|
||||||
|
|
||||||
|
public SynchronizationService(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
/** Synchronize with the service */
|
/** 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;
|
||||||
|
}
|
||||||
|
|
||||||
/** Called when synchronization with this service is turned off */
|
task = Synchronizer.getTaskController().fetchTaskForSync(
|
||||||
void synchronizationDisabled(Activity activity);
|
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);
|
||||||
|
|
||||||
|
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