- Moved estimated time back to 1st page of edit box.

- Persisting sort status
  - Sync:
    - per-list sync happens only if one-list sync fails
    - adding some delay between grabbing the task list
    - tags
    - estimated time
    - reuse taskcontroller crash solved
    - sync dialog gets the name of the task
pull/14/head
Tim Su 17 years ago
parent b9aaa8e95f
commit 3edc4163b9

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.timsu.astrid" package="com.timsu.astrid"
android:versionCode="61" android:versionCode="62"
android:versionName="2.0.0-rc2"> android:versionName="2.0.0-rc3">
<meta-data android:name="com.a0soft.gphone.aTrackDog.webURL" <meta-data android:name="com.a0soft.gphone.aTrackDog.webURL"
android:value="http://www.weloveastrid.com" /> android:value="http://www.weloveastrid.com" />
<meta-data android:name="com.a0soft.gphone.aTrackDog.testVersion" <meta-data android:name="com.a0soft.gphone.aTrackDog.testVersion"
android:value="61" /> android:value="62" />
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>

@ -76,6 +76,16 @@
android:layout_height="1dip" android:layout_height="1dip"
android:background="@android:drawable/divider_horizontal_dark" android:background="@android:drawable/divider_horizontal_dark"
/> />
<TextView android:id="@+id/estimatedDuration_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/estimatedDuration_label"
style="@style/TextAppearance.EditEvent_Label"/>
<Button android:id="@+id/estimatedDuration"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<TextView android:id="@+id/notes_label" <TextView android:id="@+id/notes_label"
android:paddingTop="5dip" android:paddingTop="5dip"
@ -258,17 +268,7 @@
android:background="@android:drawable/divider_horizontal_dark" android:background="@android:drawable/divider_horizontal_dark"
/> />
<TextView android:id="@+id/estimatedDuration_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/estimatedDuration_label"
style="@style/TextAppearance.EditEvent_Label"/>
<Button android:id="@+id/estimatedDuration"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<TextView android:id="@+id/elapsedDuration_label" <TextView android:id="@+id/elapsedDuration_label"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

@ -75,7 +75,7 @@
<string name="taskList_titlePrefix">Astrid: </string> <string name="taskList_titlePrefix">Astrid: </string>
<string name="taskList_titleTagPrefix">Tagged \"%s\": </string> <string name="taskList_titleTagPrefix">Tagged \"%s\": </string>
<string name="taskList_hiddenSuffix"> hidden</string> <string name="taskList_hiddenSuffix"> hidden</string>
<string name="taskList_dueIn">Due in</string> <string name="taskList_dueIn">Due:</string>
<string name="taskList_goalPrefix">Goal</string> <string name="taskList_goalPrefix">Goal</string>
<string name="taskList_completedPrefix">Finished</string> <string name="taskList_completedPrefix">Finished</string>
<string name="taskList_overdueBy">Overdue by</string> <string name="taskList_overdueBy">Overdue by</string>
@ -220,8 +220,7 @@ When finished, restart Astrid and come back here.
<string name="sync_rtm_notes"> <string name="sync_rtm_notes">
Welcome to Astrid\'s RTM sync! Welcome to Astrid\'s RTM sync!
\n\n \n\n
- RTM tags are not read. Instead, RTM's lists are mapped to Astrid\'s tags.\n - Task notes are read from RTM, but edits are not sent back.\n
- Task notes are read from RTM, but edits are not written back.\n
- Notifications and repeats are not synchronized.\n - Notifications and repeats are not synchronized.\n
- Moving tasks in RTM to another list and then renaming them causes dupes.\n - Moving tasks in RTM to another list and then renaming them causes dupes.\n
- Deleting tasks in RTM is not detected.\n - Deleting tasks in RTM is not detected.\n

@ -96,7 +96,7 @@ public class Invoker {
public static String API_SIG_PARAM = "api_sig"; public static String API_SIG_PARAM = "api_sig";
public static final long INVOCATION_INTERVAL = 500; public static final long INVOCATION_INTERVAL = 750;
private long lastInvocation; private long lastInvocation;

@ -35,7 +35,7 @@ import com.mdt.rtm.data.RtmTask.Priority;
/** /**
* Represents the Remember the Milk service API. * Represents the Remember the Milk service API.
* *
* @author Will Ross Jun 21, 2007 * @author Will Ross Jun 21, 2007
*/ */
public interface Service public interface Service
@ -44,7 +44,7 @@ public interface Service
/** /**
* Checks whether the service is authorized to communicate with the RTM server. Depends on the user's login info, and whether or not that user has * Checks whether the service is authorized to communicate with the RTM server. Depends on the user's login info, and whether or not that user has
* authorized the service wrapper to communicate with RTM. * authorized the service wrapper to communicate with RTM.
* *
* @return true if the service API has permission to interact with full permissions (including delete) with RTM * @return true if the service API has permission to interact with full permissions (including delete) with RTM
* @throws ServiceException * @throws ServiceException
* if there is a problem checking for authorization * if there is a problem checking for authorization
@ -54,7 +54,7 @@ public interface Service
/** /**
* Begins the process of obtaining authorization for the service API to communicate with RTM on behalf of a particular user. * Begins the process of obtaining authorization for the service API to communicate with RTM on behalf of a particular user.
* *
* @return the URL that the user should be prompted to log in to to complete authorization * @return the URL that the user should be prompted to log in to to complete authorization
* @throws ServiceException * @throws ServiceException
* if the authorization process cannot be started * if the authorization process cannot be started
@ -65,7 +65,7 @@ public interface Service
/** /**
* The same method as the previous {@link #beginAuthorization(com.mdt.rtm.data.RtmAuth.Perms)}, except that you need to invoke yourself the * The same method as the previous {@link #beginAuthorization(com.mdt.rtm.data.RtmAuth.Perms)}, except that you need to invoke yourself the
* {@link #auth_getFrob()} beforehand. * {@link #auth_getFrob()} beforehand.
* *
* This has been introduced, in order to provide better control over the API. * This has been introduced, in order to provide better control over the API.
*/ */
String beginAuthorization(RtmFrob frob, RtmAuth.Perms permissions) String beginAuthorization(RtmFrob frob, RtmAuth.Perms permissions)
@ -73,10 +73,10 @@ public interface Service
/** /**
* Completes the process of obtaining authorization for the service API to communicate with RTM on behalf of a particular user. * Completes the process of obtaining authorization for the service API to communicate with RTM on behalf of a particular user.
* *
* Once this is called successfully, <code>isServiceAuthorized()</code> should return true until the user goes to RTM and explicitly denies the * Once this is called successfully, <code>isServiceAuthorized()</code> should return true until the user goes to RTM and explicitly denies the
* service access. It also might be possible for authorization to time out, in which case this process would need to be started again. * service access. It also might be possible for authorization to time out, in which case this process would need to be started again.
* *
* @return the newly created authentication token * @return the newly created authentication token
* @throws ServiceException * @throws ServiceException
* if the authorization process cannot be completed * if the authorization process cannot be completed
@ -163,7 +163,7 @@ public interface Service
void tasks_addTags() void tasks_addTags()
throws ServiceException; throws ServiceException;
public RtmTaskSeries tasks_complete(String timelineId, String listId, String taskSeriesId, String taskId) public void tasks_complete(String timelineId, String listId, String taskSeriesId, String taskId)
throws ServiceException; throws ServiceException;
void tasks_delete(String timelineId, String listId, String taskSeriesId, String taskId) void tasks_delete(String timelineId, String listId, String taskSeriesId, String taskId)
@ -196,28 +196,28 @@ public interface Service
/** /**
* THINK: Would it not be better to have a {@link GregorianCalendar} parameter instead? * THINK: Would it not be better to have a {@link GregorianCalendar} parameter instead?
*/ */
RtmTaskSeries tasks_setDueDate(String timelineId, String listId, String taskSeriesId, String taskId, Date due, boolean hasDueTime) void tasks_setDueDate(String timelineId, String listId, String taskSeriesId, String taskId, Date due, boolean hasDueTime)
throws ServiceException; throws ServiceException;
void tasks_setEstimate() void tasks_setEstimate(String timelineId, String listId, String taskSeriesId, String taskId, String newEstimate)
throws ServiceException; throws ServiceException;
RtmTaskSeries tasks_setName(String timelineId, String listId, String taskSeriesId, String taskId, String newName) void tasks_setName(String timelineId, String listId, String taskSeriesId, String taskId, String newName)
throws ServiceException; throws ServiceException;
RtmTaskSeries tasks_setPriority(String timelineId, String listId, String taskSeriesId, String taskId, Priority priority) void tasks_setPriority(String timelineId, String listId, String taskSeriesId, String taskId, Priority priority)
throws ServiceException; throws ServiceException;
void tasks_setRecurrence() void tasks_setRecurrence()
throws ServiceException; throws ServiceException;
void tasks_setTags() void tasks_setTags(String timelineId, String listId, String taskSeriesId, String taskId, String[] tags)
throws ServiceException; throws ServiceException;
void tasks_setURL() void tasks_setURL()
throws ServiceException; throws ServiceException;
RtmTaskSeries tasks_uncomplete(String timelineId, String listId, String taskSeriesId, String taskId) void tasks_uncomplete(String timelineId, String listId, String taskSeriesId, String taskId)
throws ServiceException; throws ServiceException;
RtmTaskNote tasks_notes_add(String timelineId, String listId, String taskSeriesId, String taskId, String title, String text) RtmTaskNote tasks_notes_add(String timelineId, String listId, String taskSeriesId, String taskId, String title, String text)

@ -48,8 +48,7 @@ import com.mdt.rtm.data.RtmTask.Priority;
* @author Will Ross Jun 21, 2007 * @author Will Ross Jun 21, 2007
* @author Edouard Mercier, since 2008.04.15 * @author Edouard Mercier, since 2008.04.15
*/ */
public class ServiceImpl public class ServiceImpl implements Service
implements Service
{ {
public final static String SERVER_HOST_NAME = "api.rememberthemilk.com"; //"74.86.175.154"; // api.rememberthemilk.com public final static String SERVER_HOST_NAME = "api.rememberthemilk.com"; //"74.86.175.154"; // api.rememberthemilk.com
@ -326,14 +325,12 @@ public class ServiceImpl
throw new UnsupportedOperationException("Not supported yet."); throw new UnsupportedOperationException("Not supported yet.");
} }
public RtmTaskSeries tasks_complete(String timelineId, String listId, String taskSeriesId, String taskId) public void tasks_complete(String timelineId, String listId, String taskSeriesId, String taskId)
throws ServiceException throws ServiceException
{ {
Element response = invoker.invoke(new Param("method", "rtm.tasks.complete"), new Param("timeline", timelineId), new Param("list_id", listId), invoker.invoke(new Param("method", "rtm.tasks.complete"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken), new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey())); new Param("api_key", applicationInfo.getApiKey()));
RtmTaskList rtmTaskList = new RtmTaskList(response);
return findTask(taskSeriesId, taskId, rtmTaskList);
} }
public void tasks_delete(String timelineId, String listId, String taskSeriesId, String taskId) public void tasks_delete(String timelineId, String listId, String taskSeriesId, String taskId)
@ -372,7 +369,7 @@ public class ServiceImpl
return tasks_getTask(null, taskName); return tasks_getTask(null, taskName);
} }
public RtmTaskSeries tasks_getTask(String taskId, String taskName) public RtmTaskSeries tasks_getTask(String taskSeriesId, String taskName)
throws ServiceException throws ServiceException
{ {
Set<Param> params = new HashSet<Param>(); Set<Param> params = new HashSet<Param>();
@ -381,7 +378,7 @@ public class ServiceImpl
params.add(new Param("api_key", applicationInfo.getApiKey())); params.add(new Param("api_key", applicationInfo.getApiKey()));
params.add(new Param("filter", "name:" + taskName)); params.add(new Param("filter", "name:" + taskName));
RtmTasks rtmTasks = new RtmTasks(invoker.invoke(params.toArray(new Param[params.size()]))); RtmTasks rtmTasks = new RtmTasks(invoker.invoke(params.toArray(new Param[params.size()])));
return findTask(taskId, rtmTasks); return findTask(taskSeriesId, rtmTasks);
} }
private RtmTaskSeries findTask(String taskId, RtmTasks rtmTasks) private RtmTaskSeries findTask(String taskId, RtmTasks rtmTasks)
@ -431,40 +428,38 @@ public class ServiceImpl
throw new UnsupportedOperationException("Not supported yet."); throw new UnsupportedOperationException("Not supported yet.");
} }
public RtmTaskSeries tasks_setDueDate(String timelineId, String listId, String taskSeriesId, String taskId, Date due, boolean hasDueTime) public void tasks_setDueDate(String timelineId, String listId, String taskSeriesId, String taskId, Date due, boolean hasDueTime)
throws ServiceException throws ServiceException
{ {
final boolean setDueDate = (due != null); final boolean setDueDate = (due != null);
final Element elt;
if (setDueDate == true) if (setDueDate == true)
{ {
elt = invoker.invoke(new Param("method", "rtm.tasks.setDueDate"), new Param("timeline", timelineId), new Param("list_id", listId), invoker.invoke(new Param("method", "rtm.tasks.setDueDate"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("due", due), new Param("has_due_time", hasDueTime ? "1" : "0"), new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("due", due), new Param("has_due_time", hasDueTime ? "1" : "0"),
new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey())); new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey()));
} }
else else
{ {
elt = invoker.invoke(new Param("method", "rtm.tasks.setDueDate"), new Param("timeline", timelineId), new Param("list_id", listId), invoker.invoke(new Param("method", "rtm.tasks.setDueDate"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken), new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey())); new Param("api_key", applicationInfo.getApiKey()));
} }
final RtmTaskList rtmTaskList = new RtmTaskList(elt);
return findTask(taskSeriesId, taskId, rtmTaskList);
} }
public void tasks_setEstimate() public void tasks_setEstimate(String timelineId, String listId, String taskSeriesId, String taskId, String newEstimate)
throws ServiceException
{ {
throw new UnsupportedOperationException("Not supported yet."); invoker.invoke(new Param("method", "rtm.tasks.setEstimate"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("estimate", newEstimate), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()));
} }
public RtmTaskSeries tasks_setName(String timelineId, String listId, String taskSeriesId, String taskId, String newName) public void tasks_setName(String timelineId, String listId, String taskSeriesId, String taskId, String newName)
throws ServiceException throws ServiceException
{ {
Element elt = invoker.invoke(new Param("method", "rtm.tasks.setName"), new Param("timeline", timelineId), new Param("list_id", listId), invoker.invoke(new Param("method", "rtm.tasks.setName"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("name", newName), new Param("auth_token", currentAuthToken), new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("name", newName), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey())); new Param("api_key", applicationInfo.getApiKey()));
RtmTaskList rtmTaskList = new RtmTaskList(elt);
return findTask(taskSeriesId, taskId, rtmTaskList);
} }
private RtmTaskSeries findTask(String taskSeriesId, String taskId, RtmTaskList rtmTaskList) private RtmTaskSeries findTask(String taskSeriesId, String taskId, RtmTaskList rtmTaskList)
@ -479,14 +474,12 @@ public class ServiceImpl
return null; return null;
} }
public RtmTaskSeries tasks_setPriority(String timelineId, String listId, String taskSeriesId, String taskId, Priority priority) public void tasks_setPriority(String timelineId, String listId, String taskSeriesId, String taskId, Priority priority)
throws ServiceException throws ServiceException
{ {
Element elt = invoker.invoke(new Param("method", "rtm.tasks.setPriority"), new Param("timeline", timelineId), new Param("list_id", listId), invoker.invoke(new Param("method", "rtm.tasks.setPriority"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("priority", RtmTask.convertPriority(priority)), new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("priority", RtmTask.convertPriority(priority)),
new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey())); new Param("auth_token", currentAuthToken), new Param("api_key", applicationInfo.getApiKey()));
RtmTaskList rtmTaskList = new RtmTaskList(elt);
return findTask(taskSeriesId, taskId, rtmTaskList);
} }
public void tasks_setRecurrence() public void tasks_setRecurrence()
@ -494,9 +487,20 @@ public class ServiceImpl
throw new UnsupportedOperationException("Not supported yet."); throw new UnsupportedOperationException("Not supported yet.");
} }
public void tasks_setTags() public void tasks_setTags(String timelineId, String listId,
String taskSeriesId, String taskId, String[] tags) throws ServiceException
{ {
throw new UnsupportedOperationException("Not supported yet."); StringBuilder tagString = new StringBuilder();
if(tags != null) {
for(int i = 0; i < tags.length; i++) {
tagString.append(tags[i]);
if(i < tags.length - 1)
tagString.append(",");
}
}
invoker.invoke(new Param("method", "rtm.tasks.setTags"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("tags", tagString.toString()), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey()));
} }
public void tasks_setURL() public void tasks_setURL()
@ -504,14 +508,12 @@ public class ServiceImpl
throw new UnsupportedOperationException("Not supported yet."); throw new UnsupportedOperationException("Not supported yet.");
} }
public RtmTaskSeries tasks_uncomplete(String timelineId, String listId, String taskSeriesId, String taskId) public void tasks_uncomplete(String timelineId, String listId, String taskSeriesId, String taskId)
throws ServiceException throws ServiceException
{ {
Element response = invoker.invoke(new Param("method", "rtm.tasks.uncomplete"), new Param("timeline", timelineId), new Param("list_id", listId), invoker.invoke(new Param("method", "rtm.tasks.uncomplete"), new Param("timeline", timelineId), new Param("list_id", listId),
new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken), new Param("taskseries_id", taskSeriesId), new Param("task_id", taskId), new Param("auth_token", currentAuthToken),
new Param("api_key", applicationInfo.getApiKey())); new Param("api_key", applicationInfo.getApiKey()));
RtmTaskList rtmTaskList = new RtmTaskList(response);
return findTask(taskSeriesId, taskId, rtmTaskList);
} }
public RtmTaskNote tasks_notes_add(String timelineId, String listId, String taskSeriesId, String taskId, String title, String text) public RtmTaskNote tasks_notes_add(String timelineId, String listId, String taskSeriesId, String taskId, String title, String text)

@ -20,11 +20,14 @@
package com.mdt.rtm.data; package com.mdt.rtm.data;
import java.util.Date; import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.w3c.dom.Element; import org.w3c.dom.Element;
/** /**
* *
* @author Will Ross Jun 22, 2007 * @author Will Ross Jun 22, 2007
*/ */
public class RtmTaskSeries extends RtmData { public class RtmTaskSeries extends RtmData {
@ -43,6 +46,8 @@ public class RtmTaskSeries extends RtmData {
private final RtmTask task; private final RtmTask task;
private final LinkedList<String> tags;
private final RtmTaskNotes notes; private final RtmTaskNotes notes;
private final String locationId; private final String locationId;
@ -59,6 +64,7 @@ public class RtmTaskSeries extends RtmData {
this.locationId = null; this.locationId = null;
notes = null; notes = null;
url = null; url = null;
tags = null;
} }
public RtmTaskSeries(Element elt) { public RtmTaskSeries(Element elt) {
@ -75,6 +81,19 @@ public class RtmTaskSeries extends RtmData {
notes = new RtmTaskNotes(child(elt, "notes")); notes = new RtmTaskNotes(child(elt, "notes"));
locationId = elt.getAttribute("location_id"); locationId = elt.getAttribute("location_id");
url = elt.getAttribute("url"); url = elt.getAttribute("url");
Element elementTags = child(elt, "tags");
if(elementTags.getChildNodes().getLength() > 0) {
List<Element> elementTagList = children(elementTags, "tag");
tags = new LinkedList<String>();
for (Element elementTag : elementTagList) {
String tag = text(elementTag);
if(tag != null)
tags.add(tag);
}
} else {
tags = null;
}
} }
public String getId() { public String getId() {
@ -101,6 +120,10 @@ public class RtmTaskSeries extends RtmData {
return task; return task;
} }
public LinkedList<String> getTags() {
return tags;
}
public RtmTaskNotes getNotes() public RtmTaskNotes getNotes()
{ {
return notes; return notes;

@ -154,6 +154,8 @@ public class TaskList extends Activity {
Synchronizer.setTaskController(taskController); Synchronizer.setTaskController(taskController);
setupUIComponents(); setupUIComponents();
loadTaskListSort();
fillData();
// auto sync // auto sync
Integer autoSyncHours = Preferences.autoSyncFrequency(this); Integer autoSyncHours = Preferences.autoSyncFrequency(this);
@ -164,8 +166,7 @@ public class TaskList extends Activity {
1000L*3600*autoSyncHours < System.currentTimeMillis()) { 1000L*3600*autoSyncHours < System.currentTimeMillis()) {
Synchronizer.synchronize(this, true, null); Synchronizer.synchronize(this, true, null);
} }
} else }
fillData();
} }
public void setupUIComponents() { public void setupUIComponents() {
@ -555,6 +556,7 @@ public class TaskList extends Activity {
if(numServicesSynced == 0) if(numServicesSynced == 0)
DialogUtilities.okDialog(TaskList.this, getResources().getString( DialogUtilities.okDialog(TaskList.this, getResources().getString(
R.string.sync_no_synchronizers), null); R.string.sync_no_synchronizers), null);
fillData();
} }
}); });
} else if(requestCode == ACTIVITY_TAGS && resultCode == RESULT_CANCELED) } else if(requestCode == ACTIVITY_TAGS && resultCode == RESULT_CANCELED)
@ -614,6 +616,24 @@ public class TaskList extends Activity {
} }
} }
/** Save the sorting mode to the preferences */
private void saveTaskListSort() {
int sortId = sortMode.ordinal() + 1;
if(sortReverse)
sortId *= -1;
Preferences.setTaskListSort(this, sortId);
}
/** Save the sorting mode to the preferences */
private void loadTaskListSort() {
int sortId = Preferences.getTaskListSort(this);
if(sortId == 0)
return;
sortReverse = sortId < 0;
sortMode = SortMode.values()[Math.abs(sortId)];
}
@Override @Override
public boolean onMenuItemSelected(int featureId, MenuItem item) { public boolean onMenuItemSelected(int featureId, MenuItem item) {
Intent intent; Intent intent;
@ -684,6 +704,7 @@ public class TaskList extends Activity {
return true; return true;
sortReverse = false; sortReverse = false;
sortMode = SortMode.AUTO; sortMode = SortMode.AUTO;
saveTaskListSort();
fillData(); fillData();
return true; return true;
case CONTEXT_SORT_ALPHA: case CONTEXT_SORT_ALPHA:
@ -691,6 +712,7 @@ public class TaskList extends Activity {
return true; return true;
sortReverse = false; sortReverse = false;
sortMode = SortMode.ALPHA; sortMode = SortMode.ALPHA;
saveTaskListSort();
fillData(); fillData();
return true; return true;
case CONTEXT_SORT_DUEDATE: case CONTEXT_SORT_DUEDATE:
@ -698,10 +720,12 @@ public class TaskList extends Activity {
return true; return true;
sortReverse = false; sortReverse = false;
sortMode = SortMode.DUEDATE; sortMode = SortMode.DUEDATE;
saveTaskListSort();
fillData(); fillData();
return true; return true;
case CONTEXT_SORT_REVERSE: case CONTEXT_SORT_REVERSE:
sortReverse = !sortReverse; sortReverse = !sortReverse;
saveTaskListSort();
fillData(); fillData();
return true; return true;
} }
@ -715,5 +739,7 @@ public class TaskList extends Activity {
taskController.close(); taskController.close();
if(tagController != null) if(tagController != null)
tagController.close(); tagController.close();
Synchronizer.setTagController(null);
Synchronizer.setTaskController(null);
} }
} }

@ -4,9 +4,9 @@ import java.io.IOException;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import java.util.Map.Entry;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
@ -31,6 +31,7 @@ import com.mdt.rtm.data.RtmTask.Priority;
import com.timsu.astrid.R; import com.timsu.astrid.R;
import com.timsu.astrid.data.enums.Importance; import com.timsu.astrid.data.enums.Importance;
import com.timsu.astrid.data.sync.SyncMapping; import com.timsu.astrid.data.sync.SyncMapping;
import com.timsu.astrid.data.task.TaskModelForSync;
import com.timsu.astrid.utilities.DialogUtilities; import com.timsu.astrid.utilities.DialogUtilities;
import com.timsu.astrid.utilities.Preferences; import com.timsu.astrid.utilities.Preferences;
@ -45,6 +46,8 @@ public class RTMSyncService extends SynchronizationService {
super(id); super(id);
} }
// --- abstract methods
@Override @Override
String getName() { String getName() {
return "RTM"; return "RTM";
@ -52,7 +55,7 @@ public class RTMSyncService extends SynchronizationService {
@Override @Override
protected void synchronize(final Activity activity) { protected void synchronize(final Activity activity) {
if(Preferences.shouldSyncRTM(activity) && if(Preferences.shouldSyncRTM(activity) && rtmService == null &&
Preferences.getSyncRTMToken(activity) == null) { Preferences.getSyncRTMToken(activity) == null) {
DialogUtilities.okCancelDialog(activity, DialogUtilities.okCancelDialog(activity,
activity.getResources().getString(R.string.sync_rtm_notes), activity.getResources().getString(R.string.sync_rtm_notes),
@ -73,6 +76,8 @@ public class RTMSyncService extends SynchronizationService {
Synchronizer.getSyncController(activity).deleteAllMappings(getId()); Synchronizer.getSyncController(activity).deleteAllMappings(getId());
} }
// --- authentication
/** Perform authentication with RTM. Will open the SyncBrowser if necessary */ /** Perform authentication with RTM. Will open the SyncBrowser if necessary */
private void authenticate(final Activity activity) { private void authenticate(final Activity activity) {
try { try {
@ -129,6 +134,8 @@ public class RTMSyncService extends SynchronizationService {
} }
} }
// --- synchronization!
private void performSync(final Activity activity) { private void performSync(final Activity activity) {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
@ -140,14 +147,8 @@ public class RTMSyncService extends SynchronizationService {
private void performSyncInNewThread(final Activity activity) { private void performSyncInNewThread(final Activity activity) {
try { try {
syncHandler.post(new Runnable() { syncHandler.post(new ProgressLabelUpdater("Reading remote data"));
@Override syncHandler.post(new ProgressUpdater(0, 1));
public void run() {
progressDialog.show();
progressDialog.setMessage("Reading Remote Information");
progressDialog.setProgress(0);
}
});
// get RTM timeline // get RTM timeline
final String timeline = rtmService.timelines_create(); final String timeline = rtmService.timelines_create();
@ -164,77 +165,52 @@ public class RTMSyncService extends SynchronizationService {
} }
// read all tasks // read all tasks
List<TaskProxy> remoteChanges = new LinkedList<TaskProxy>(); LinkedList<TaskProxy> remoteChanges = new LinkedList<TaskProxy>();
Date lastSyncDate = Preferences.getSyncRTMLastSync(activity); Date lastSyncDate = Preferences.getSyncRTMLastSync(activity);
String filter = ""; boolean shouldSyncIndividualLists = false;
if(lastSyncDate == null) // 1st time sync, just uncompleted tasks String filter = null;
filter = "status:incomplete"; if(lastSyncDate == null)
int progress = 0; filter = "status:incomplete"; // 1st time sync: get unfinished tasks
for(final String listId : listIdToNameMap.keySet()) {
RtmTasks tasks; // try the quick synchronization
try { try {
tasks = rtmService.tasks_getList(listId, filter, lastSyncDate); Thread.sleep(1500); // throttle
} catch (Exception e) { RtmTasks tasks = rtmService.tasks_getList(null, filter, lastSyncDate);
syncHandler.post(new Runnable() { syncHandler.post(new ProgressUpdater(1, 1));
@Override addTasksToList(tasks, remoteChanges);
public void run() { } catch (Exception e) {
DialogUtilities.okDialog(activity, remoteChanges.clear();
"List " + listIdToNameMap.get(listId) + shouldSyncIndividualLists = true;
" import failed (too big?)", null); }
}
});
continue;
}
for(RtmTaskList taskList : tasks.getLists()) { if(shouldSyncIndividualLists) {
for(RtmTaskSeries taskSeries : taskList.getSeries()) { int progress = 0;
TaskProxy remoteTask = parseRemoteTask(taskList.getId(), taskSeries); for(final Entry<String, String> entry : listIdToNameMap.entrySet()) {
remoteChanges.add(remoteTask); syncHandler.post(new ProgressLabelUpdater("Reading " +
" list: " + entry.getValue()));
syncHandler.post(new ProgressUpdater(progress++,
listIdToNameMap.size()));
try {
Thread.sleep(1500);
RtmTasks tasks = rtmService.tasks_getList(entry.getKey(),
filter, lastSyncDate);
addTasksToList(tasks, remoteChanges);
} catch (Exception e) {
syncHandler.post(new Runnable() {
@Override
public void run() {
DialogUtilities.okDialog(activity,
"List '" + entry.getValue() +
"' import failed (too big?)", null);
}
});
continue;
} }
} }
syncHandler.post(new ProgressUpdater(++progress, listIdToNameMap.size())); syncHandler.post(new ProgressUpdater(1, 1));
} }
synchronizeTasks(activity, remoteChanges, new RtmSyncHelper(timeline));
synchronizeTasks(activity, remoteChanges, new SynchronizeHelper() {
@Override
public String createTask(String listName) throws IOException {
if(listName == null)
listName = INBOX_LIST_NAME;
if(!listNameToIdMap.containsKey(listName.toLowerCase())) {
try {
String listId =
rtmService.lists_add(timeline, listName).getId();
listNameToIdMap.put(listName.toLowerCase(), listId);
} catch (Exception e) {
listName = INBOX_LIST_NAME;
}
}
String listId = listNameToIdMap.get(listName.toLowerCase());
RtmTaskSeries s = rtmService.tasks_add(timeline,
listId, "tmp");
return new RtmId(listId, 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(id.taskSeriesId,
task.name);
if(rtmTask != null)
return task; // can't fetch
return parseRemoteTask(id.listId, rtmTask);
}
});
// add a bit of fudge time so we don't load tasks we just edited // add a bit of fudge time so we don't load tasks we just edited
Date syncTime = new Date(System.currentTimeMillis() + 1000); Date syncTime = new Date(System.currentTimeMillis() + 1000);
@ -245,48 +221,52 @@ public class RTMSyncService extends SynchronizationService {
} }
} }
/** Helper class for processing RTM id's into one field */ // --- helper methods
private static class RtmId {
String taskId;
String taskSeriesId;
String listId;
public RtmId(String listId, RtmTaskSeries taskSeries) { /** Add the tasks read from RTM to the given list */
this.taskId = taskSeries.getTask().getId(); private void addTasksToList(RtmTasks tasks, LinkedList<TaskProxy> list) {
this.taskSeriesId = taskSeries.getId(); for(RtmTaskList taskList : tasks.getLists()) {
this.listId = listId; for(RtmTaskSeries taskSeries : taskList.getSeries()) {
} TaskProxy remoteTask =
parseRemoteTask(taskList.getId(), taskSeries);
public RtmId(String id) { list.add(remoteTask);
StringTokenizer strtok = new StringTokenizer(id, "|"); }
taskId = strtok.nextToken();
taskSeriesId = strtok.nextToken();
listId = strtok.nextToken();
}
@Override
public String toString() {
return taskId + "|" + taskSeriesId + "|" + listId;
} }
} }
/** Send changes for the given TaskProxy across the wire */ /** Send changes for the given TaskProxy across the wire */
private void pushLocalTask(String timeline, TaskProxy task, SyncMapping mapping) private void pushLocalTask(String timeline, TaskProxy task, TaskProxy remoteTask,
throws ServiceException { SyncMapping mapping) throws ServiceException {
RtmId id = new RtmId(mapping.getRemoteId()); RtmId id = new RtmId(mapping.getRemoteId());
if(task.name != null) // fetch remote task for comparison (won't work if you renamed it)
if(remoteTask == null) {
RtmTaskSeries rtmTask = rtmService.tasks_getTask(id.taskSeriesId, task.name);
if(rtmTask != null)
remoteTask = parseRemoteTask(id.listId, rtmTask);
}
if(remoteTask == null)
remoteTask = new TaskProxy(0, "", false);
if(task.name != null && !task.name.equals(remoteTask.name))
rtmService.tasks_setName(timeline, id.listId, id.taskSeriesId, rtmService.tasks_setName(timeline, id.listId, id.taskSeriesId,
id.taskId, task.name); id.taskId, task.name);
if(task.importance != null) if(task.importance != null && !task.importance.equals(remoteTask.importance))
rtmService.tasks_setPriority(timeline, id.listId, id.taskSeriesId, rtmService.tasks_setPriority(timeline, id.listId, id.taskSeriesId,
id.taskId, Priority.values()[task.importance.ordinal()]); id.taskId, Priority.values()[task.importance.ordinal()]);
// due date
Date dueDate = task.definiteDueDate; Date dueDate = task.definiteDueDate;
if(dueDate == null) if(dueDate == null)
dueDate = task.preferredDueDate; dueDate = task.preferredDueDate;
rtmService.tasks_setDueDate(timeline, id.listId, id.taskSeriesId, if(dueDate != remoteTask.definiteDueDate &&
(dueDate == null || !dueDate.equals(remoteTask.definiteDueDate)))
rtmService.tasks_setDueDate(timeline, id.listId, id.taskSeriesId,
id.taskId, dueDate, dueDate != null); id.taskId, dueDate, dueDate != null);
if(task.progressPercentage != null) {
// progress
if(task.progressPercentage != null && !task.progressPercentage.equals(
remoteTask.progressPercentage)) {
if(task.progressPercentage == 100) if(task.progressPercentage == 100)
rtmService.tasks_complete(timeline, id.listId, id.taskSeriesId, rtmService.tasks_complete(timeline, id.listId, id.taskSeriesId,
id.taskId); id.taskId);
@ -294,9 +274,38 @@ public class RTMSyncService extends SynchronizationService {
rtmService.tasks_uncomplete(timeline, id.listId, id.taskSeriesId, rtmService.tasks_uncomplete(timeline, id.listId, id.taskSeriesId,
id.taskId); id.taskId);
} }
if(task.notes != null && !task.notes.endsWith("\n")) {
// notes
if(task.notes != null && task.notes.length() > 0 && !task.equals(remoteTask.notes))
rtmService.tasks_notes_add(timeline, id.listId, id.taskSeriesId, rtmService.tasks_notes_add(timeline, id.listId, id.taskSeriesId,
id.taskId, "From Astrid", task.notes); id.taskId, "From Astrid", task.notes);
// tags
if(task.tags != null && !task.tags.equals(remoteTask.tags)) {
String listName = listIdToNameMap.get(id.listId);
if(task.tags.size() > 0 && listName.equals(task.tags.getFirst()))
task.tags.remove(0);
rtmService.tasks_setTags(timeline, id.listId, id.taskSeriesId,
id.taskId, task.tags.toArray(new String[task.tags.size()]));
}
// estimated time
if(task.estimatedSeconds != null && !task.estimatedSeconds.equals(remoteTask.estimatedSeconds)) {
String estimation;
int estimatedSeconds = task.estimatedSeconds;
if(estimatedSeconds == 0)
estimation = "";
else if(estimatedSeconds < 3600)
estimation = estimatedSeconds/60 + " minutes";
else if(estimatedSeconds < 24*3600) {
int hours = (estimatedSeconds/3600);
estimation = hours+ " hours ";
if(hours*3600 != estimatedSeconds)
estimation += estimatedSeconds - hours*3600 + " minutes";
} else
estimation = estimatedSeconds/3600/24 + " days";
rtmService.tasks_setEstimate(timeline, id.listId, id.taskSeriesId,
id.taskId, estimation);
} }
} }
@ -307,26 +316,145 @@ public class RTMSyncService extends SynchronizationService {
rtmTaskSeries.getTask().getDeleted() != null); rtmTaskSeries.getTask().getDeleted() != null);
task.name = rtmTaskSeries.getName(); task.name = rtmTaskSeries.getName();
// notes
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for(RtmTaskNote note: rtmTaskSeries.getNotes().getNotes()) { for(RtmTaskNote note: rtmTaskSeries.getNotes().getNotes()) {
sb.append(note.getText() + "\n"); sb.append(note.getText() + "\n");
} }
if(sb.length() > 0) if(sb.length() > 0)
task.notes = sb.toString(); task.notes = sb.toString();
// list / tags
LinkedList<String> tagsList = rtmTaskSeries.getTags();
String listName = listIdToNameMap.get(listId); String listName = listIdToNameMap.get(listId);
if(listName != null && !listName.equals(INBOX_LIST_NAME)) if(listName != null && !listName.equals(INBOX_LIST_NAME)) {
task.tags = new String[] { listName }; if(tagsList == null)
tagsList = new LinkedList<String>();
tagsList.addFirst(listName);
}
if(tagsList != null)
task.tags = tagsList;
RtmTask rtmTask = rtmTaskSeries.getTask(); RtmTask rtmTask = rtmTaskSeries.getTask();
String estimate = rtmTask.getEstimate();
if(estimate != null && estimate.length() > 0) {
task.estimatedSeconds = parseEstimate(estimate);
}
task.creationDate = rtmTaskSeries.getCreated(); task.creationDate = rtmTaskSeries.getCreated();
task.completionDate = rtmTask.getCompleted(); task.completionDate = rtmTask.getCompleted();
if(rtmTask.getDue() != null) if(rtmTask.getDue() != null)
task.definiteDueDate = rtmTask.getDue(); task.definiteDueDate = rtmTask.getDue();
task.progressPercentage = (rtmTask.getCompleted() == null) ? null : 100; task.progressPercentage = (rtmTask.getCompleted() == null) ? 0 : 100;
task.importance = Importance.values()[rtmTask.getPriority().ordinal()]; task.importance = Importance.values()[rtmTask.getPriority().ordinal()];
return task; return task;
} }
/** Parse an estimated time of the format ## {s,m,h,d,w,mo} and return
* the duration in seconds. Returns null on failure. */
private Integer parseEstimate(String estimate) {
try {
float total = 0;
int position = 0;
while(position != -1) {
for(; position < estimate.length(); position++) {
char c = estimate.charAt(position);
if(c != '.' && (c < '0' || c > '9'))
break;
}
float numberPortion = Float.parseFloat(estimate.substring(0, position));
String stringPortion = estimate.substring(position).trim();
position = stringPortion.indexOf(" ");
if(position != -1)
estimate = stringPortion.substring(position+1).trim();
if(stringPortion.startsWith("mo"))
total += numberPortion * 31 * 24 * 3600;
else if(stringPortion.startsWith("w"))
total += numberPortion * 7 * 24 * 3600;
else if(stringPortion.startsWith("d"))
total += numberPortion * 24 * 3600;
else if(stringPortion.startsWith("h"))
total += numberPortion * 3600;
else if(stringPortion.startsWith("m"))
total += numberPortion * 60;
else if(stringPortion.startsWith("s"))
total += numberPortion;
}
return (int)total;
} catch (Exception e) { /* */ }
return null;
}
// --- helper classes
/** SynchronizeHelper for remember the milk */
private class RtmSyncHelper implements SynchronizeHelper {
private String timeline;
private String lastCreatedTask = null;
public RtmSyncHelper(String timeline) {
this.timeline = timeline;
}
@Override
public String createTask(TaskModelForSync task) throws IOException {
String listId = listNameToIdMap.get(INBOX_LIST_NAME.toLowerCase());
RtmTaskSeries s = rtmService.tasks_add(timeline, listId,
task.getName());
lastCreatedTask = new RtmId(listId, s).toString();
return lastCreatedTask;
}
@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, TaskProxy remoteTask,
SyncMapping mapping) throws IOException {
// don't save the stuff that we already saved by creating the task
if(task.getRemoteId().equals(lastCreatedTask))
task.name = null;
pushLocalTask(timeline, task, remoteTask, mapping);
}
@Override
public TaskProxy refetchTask(TaskProxy task) throws IOException {
RtmId id = new RtmId(task.getRemoteId());
RtmTaskSeries rtmTask = rtmService.tasks_getTask(id.taskSeriesId,
task.name);
if(rtmTask == null)
return task; // can't fetch
return parseRemoteTask(id.listId, rtmTask);
}
}
/** 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;
}
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;
}
}
} }

@ -4,7 +4,6 @@ import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
@ -99,16 +98,19 @@ public abstract class SynchronizationService {
/** Push the given task to the remote server. /** Push the given task to the remote server.
* *
* @param task task proxy to push * @param task task proxy to push
* @param remoteTask remote task that we merged with, or null
* @param mapping local/remote mapping. * @param mapping local/remote mapping.
*/ */
void pushTask(TaskProxy task, SyncMapping mapping) throws IOException; void pushTask(TaskProxy task, TaskProxy remoteTask,
SyncMapping mapping) throws IOException;
/** Create a task on the remote server /** Create a task on the remote server. This is followed by a call of
* pushTask on the id in question.
* *
* @return primaryTag primary tag of this task. null if no tags exist. * @return task to create
* @return remote id * @return remote id
*/ */
String createTask(String primaryTag) throws IOException; String createTask(TaskModelForSync task) throws IOException;
/** Fetch remote task. Used to re-read merged tasks /** Fetch remote task. Used to re-read merged tasks
* *
@ -135,153 +137,105 @@ public abstract class SynchronizationService {
* @param remoteTasks remote tasks that have been updated * @param remoteTasks remote tasks that have been updated
* @return local tasks that need to be pushed across * @return local tasks that need to be pushed across
*/ */
protected void synchronizeTasks(final Activity activity, List<TaskProxy> remoteTasks, protected void synchronizeTasks(final Activity activity, LinkedList<TaskProxy>
SynchronizeHelper helper) throws IOException { remoteTasks, SynchronizeHelper helper) throws IOException {
final SyncStats stats = new SyncStats(); final SyncStats stats = new SyncStats();
final StringBuilder log = new StringBuilder(); final StringBuilder log = new StringBuilder();
syncHandler.post(new Runnable() {
@Override
public void run() {
if(!progressDialog.isShowing())
progressDialog.show();
}
});
SyncDataController syncController = Synchronizer.getSyncController(activity); SyncDataController syncController = Synchronizer.getSyncController(activity);
TaskController taskController = Synchronizer.getTaskController(activity); TaskController taskController = Synchronizer.getTaskController(activity);
TagController tagController = Synchronizer.getTagController(activity); TagController tagController = Synchronizer.getTagController(activity);
AlertController alertController = Synchronizer.getAlertController(activity); AlertController alertController = Synchronizer.getAlertController(activity);
SyncData data = new SyncData(activity, remoteTasks);
// 1. get data out of the database // 1. CREATE: grab tasks without a sync mapping and create them remotely
HashSet<SyncMapping> mappings = syncController.getSyncMapping(getId());
HashSet<TaskIdentifier> activeTasks = taskController.
getActiveTaskIdentifiers();
HashSet<TaskIdentifier> allTasks = taskController.
getAllTaskIdentifiers();
HashMap<TagIdentifier, TagModelForView> tags =
tagController.getAllTagsAsMap(activity);
// 2. build helper data structures
HashMap<String, SyncMapping> remoteIdToSyncMapping =
new HashMap<String, SyncMapping>();
HashMap<TaskIdentifier, SyncMapping> localIdToSyncMapping =
new HashMap<TaskIdentifier, SyncMapping>();
HashSet<SyncMapping> localChanges = new HashSet<SyncMapping>();
HashSet<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());
}
// 3. build map of remote tasks
HashMap<TaskIdentifier, TaskProxy> remoteChangeMap =
new HashMap<TaskIdentifier, TaskProxy>();
HashMap<String, TaskProxy> newRemoteTasks = new HashMap<String, TaskProxy>();
for(TaskProxy remoteTask : remoteTasks) {
if(remoteIdToSyncMapping.containsKey(remoteTask.getRemoteId())) {
SyncMapping mapping = remoteIdToSyncMapping.get(remoteTask.getRemoteId());
remoteChangeMap.put(mapping.getTask(), remoteTask);
} else if(remoteTask.name != null){
newRemoteTasks.put(remoteTask.name, remoteTask);
}
}
// 4. CREATE: grab tasks without a sync mapping and create them remotely
log.append(">> on remote server:\n"); log.append(">> on remote server:\n");
syncHandler.post(new ProgressLabelUpdater("Sending locally created tasks")); for(TaskIdentifier taskId : data.newlyCreatedTasks) {
HashSet<TaskIdentifier> newlyCreatedTasks = new HashSet<TaskIdentifier>(activeTasks);
newlyCreatedTasks.removeAll(mappedTasks);
for(TaskIdentifier taskId : newlyCreatedTasks) {
TaskModelForSync task = taskController.fetchTaskForSync(taskId); TaskModelForSync task = taskController.fetchTaskForSync(taskId);
syncHandler.post(new ProgressLabelUpdater("Sending local task: " +
task.getName()));
syncHandler.post(new ProgressUpdater(stats.remoteCreatedTasks,
data.newlyCreatedTasks.size()));
/* If there exists an incoming remote task with the same name and /* If there exists an incoming remote task with the same name and
* no mapping, we don't want to create this on the remote server. * no mapping, we don't want to create this on the remote server.
* Instead, we create a mapping and do an update. */ * Instead, we create a mapping and do an update. */
if(newRemoteTasks.containsKey(task.getName())) { if(data.newRemoteTasks.containsKey(task.getName())) {
TaskProxy remoteTask = newRemoteTasks.get(task.getName()); TaskProxy remoteTask = data.newRemoteTasks.get(task.getName());
SyncMapping mapping = new SyncMapping(taskId, getId(), SyncMapping mapping = new SyncMapping(taskId, getId(),
remoteTask.getRemoteId()); remoteTask.getRemoteId());
syncController.saveSyncMapping(mapping); syncController.saveSyncMapping(mapping);
localChanges.add(mapping); data.localChanges.add(mapping);
remoteChangeMap.put(taskId, remoteTask); data.remoteChangeMap.put(taskId, remoteTask);
localIdToSyncMapping.put(taskId, mapping); data.localIdToSyncMapping.put(taskId, mapping);
continue; continue;
} }
// grab the primary tag for this task String remoteId = helper.createTask(task);
LinkedList<TagIdentifier> taskTags =
tagController.getTaskTags(activity, taskId);
String listName = null;
if(taskTags.size() > 0) {
listName = tags.get(taskTags.get(0)).getName();
if(listName.startsWith(TagModelForView.HIDDEN_FROM_MAIN_LIST_PREFIX))
listName = listName.substring(1);
}
String remoteId = helper.createTask(listName);
SyncMapping mapping = new SyncMapping(taskId, getId(), remoteId); SyncMapping mapping = new SyncMapping(taskId, getId(), remoteId);
syncController.saveSyncMapping(mapping); syncController.saveSyncMapping(mapping);
TaskProxy localTask = new TaskProxy(getId(), remoteId, false); TaskProxy localTask = new TaskProxy(getId(), remoteId, false);
localTask.readFromTaskModel(task); localTask.readFromTaskModel(task);
helper.pushTask(localTask, mapping); localTask.readTagsFromController(activity, taskId, tagController, data.tags);
helper.pushTask(localTask, null, mapping);
// update stats // update stats
log.append("added " + task.getName() + "\n"); log.append("added '" + task.getName() + "'\n");
stats.remoteCreatedTasks++; stats.remoteCreatedTasks++;
syncHandler.post(new ProgressUpdater(stats.remoteCreatedTasks,
newlyCreatedTasks.size()));
} }
// 5. DELETE: find deleted tasks and remove them from the list // 2. DELETE: find deleted tasks and remove them from the list
syncHandler.post(new ProgressLabelUpdater("Sending locally deleted tasks")); syncHandler.post(new ProgressLabelUpdater("Sending locally deleted tasks"));
HashSet<TaskIdentifier> deletedTasks = new HashSet<TaskIdentifier>( for(TaskIdentifier taskId : data.deletedTasks) {
mappedTasks); SyncMapping mapping = data.localIdToSyncMapping.get(taskId);
deletedTasks.removeAll(allTasks);
for(TaskIdentifier taskId : deletedTasks) {
SyncMapping mapping = localIdToSyncMapping.get(taskId);
syncController.deleteSyncMapping(mapping); syncController.deleteSyncMapping(mapping);
helper.deleteTask(mapping); helper.deleteTask(mapping);
// remove it from data structures // remove it from data structures
localChanges.remove(mapping); data.localChanges.remove(mapping);
remoteIdToSyncMapping.remove(mapping); data.remoteIdToSyncMapping.remove(mapping);
remoteChangeMap.remove(taskId); data.remoteChangeMap.remove(taskId);
// update stats // update stats
log.append("deleted id #" + taskId.getId() + "\n"); log.append("deleted id #" + taskId.getId() + "\n");
stats.remoteDeletedTasks++; stats.remoteDeletedTasks++;
syncHandler.post(new ProgressUpdater(stats.remoteDeletedTasks, syncHandler.post(new ProgressUpdater(stats.remoteDeletedTasks,
deletedTasks.size())); data.deletedTasks.size()));
} }
// 6. UPDATE: for each updated local task // 3. UPDATE: for each updated local task
syncHandler.post(new ProgressLabelUpdater("Sending locally edited tasks")); for(SyncMapping mapping : data.localChanges) {
for(SyncMapping mapping : localChanges) {
TaskProxy localTask = new TaskProxy(getId(), mapping.getRemoteId(), TaskProxy localTask = new TaskProxy(getId(), mapping.getRemoteId(),
false); false);
TaskModelForSync task = taskController.fetchTaskForSync( TaskModelForSync task = taskController.fetchTaskForSync(
mapping.getTask()); mapping.getTask());
localTask.readFromTaskModel(task); localTask.readFromTaskModel(task);
localTask.readTagsFromController(activity, task.getTaskIdentifier(),
tagController, data.tags);
syncHandler.post(new ProgressLabelUpdater("Sending local task: " +
task.getName()));
syncHandler.post(new ProgressUpdater(stats.remoteUpdatedTasks,
data.localChanges.size()));
// if there is a conflict, merge // if there is a conflict, merge
TaskProxy remoteConflict = null; TaskProxy remoteConflict = null;
if(remoteChangeMap.containsKey(mapping.getTask())) { if(data.remoteChangeMap.containsKey(mapping.getTask())) {
remoteConflict = remoteChangeMap.get(mapping.getTask()); remoteConflict = data.remoteChangeMap.get(mapping.getTask());
localTask.mergeWithOther(remoteConflict); localTask.mergeWithOther(remoteConflict);
stats.mergedTasks++; stats.mergedTasks++;
log.append("merged " + task.getName() + "\n");
} else {
log.append("updated " + task.getName() + "\n");
} }
try { try {
helper.pushTask(localTask, mapping); helper.pushTask(localTask, remoteConflict, mapping);
if(remoteConflict != null)
log.append("merged '" + task.getName() + "'\n");
else
log.append("updated '" + task.getName() + "'\n");
} catch (Exception e) { } catch (Exception e) {
Log.e("astrid", "Exception pushing task", e); Log.e("astrid", "Exception pushing task", e);
log.append("error sending '" + task.getName() + "'\n");
continue; continue;
} }
@ -292,19 +246,22 @@ public abstract class SynchronizationService {
remoteTasks.add(newTask); remoteTasks.add(newTask);
} else } else
stats.remoteUpdatedTasks++; stats.remoteUpdatedTasks++;
syncHandler.post(new ProgressUpdater(stats.remoteUpdatedTasks,
localChanges.size()));
} }
// 7. REMOTE SYNC load remote information // 4. REMOTE SYNC load remote information
log.append(">> on astrid:\n"); log.append(">> on astrid:\n");
syncHandler.post(new ProgressLabelUpdater("Updating local tasks")); syncHandler.post(new ProgressUpdater(0, 1));
for(TaskProxy remoteTask : remoteTasks) { for(TaskProxy remoteTask : remoteTasks) {
if(remoteTask.name != null)
syncHandler.post(new ProgressLabelUpdater("Updating local " +
"tasks: " + remoteTask.name));
else
syncHandler.post(new ProgressLabelUpdater("Updating local tasks"));
SyncMapping mapping = null; SyncMapping mapping = null;
TaskModelForSync task = null; TaskModelForSync task = null;
// if it's new, create a new task model // if it's new, create a new task model
if(!remoteIdToSyncMapping.containsKey(remoteTask.getRemoteId())) { if(!data.remoteIdToSyncMapping.containsKey(remoteTask.getRemoteId())) {
// if it's new & deleted, forget about it // if it's new & deleted, forget about it
if(remoteTask.isDeleted()) { if(remoteTask.isDeleted()) {
continue; continue;
@ -316,11 +273,11 @@ public abstract class SynchronizationService {
setupTaskDefaults(activity, task); setupTaskDefaults(activity, task);
log.append("added " + remoteTask.name + "\n"); log.append("added " + remoteTask.name + "\n");
} else { } else {
mapping = localIdToSyncMapping.get(task.getTaskIdentifier()); mapping = data.localIdToSyncMapping.get(task.getTaskIdentifier());
log.append("merged " + remoteTask.name + "\n"); log.append("merged " + remoteTask.name + "\n");
} }
} else { } else {
mapping = remoteIdToSyncMapping.get(remoteTask.getRemoteId()); mapping = data.remoteIdToSyncMapping.get(remoteTask.getRemoteId());
if(remoteTask.isDeleted()) { if(remoteTask.isDeleted()) {
taskController.deleteTask(mapping.getTask()); taskController.deleteTask(mapping.getTask());
syncController.deleteSyncMapping(mapping); syncController.deleteSyncMapping(mapping);
@ -329,7 +286,7 @@ public abstract class SynchronizationService {
continue; continue;
} }
log.append("updated " + remoteTask.name + "\n"); log.append("updated '" + remoteTask.name + "'\n");
task = taskController.fetchTaskForSync( task = taskController.fetchTaskForSync(
mapping.getTask()); mapping.getTask());
} }
@ -338,39 +295,40 @@ public abstract class SynchronizationService {
remoteTask.writeToTaskModel(task); remoteTask.writeToTaskModel(task);
taskController.saveTask(task); taskController.saveTask(task);
// save tag // save tags
if(remoteTask.tags != null && remoteTask.tags.length > 0) { if(remoteTask.tags != null) {
String tag = remoteTask.tags[0]; LinkedList<TagIdentifier> taskTags = tagController.getTaskTags(activity, task.getTaskIdentifier());
TagIdentifier tagIdentifier = null; HashSet<TagIdentifier> tagsToAdd = new HashSet<TagIdentifier>();
for(TagModelForView tagModel : tags.values()) { for(String tag : remoteTask.tags) {
String tagName = tagModel.getName(); String tagLower = tag.toLowerCase();
if(tagName.startsWith(TagModelForView.HIDDEN_FROM_MAIN_LIST_PREFIX)) if(!data.tagsByLCName.containsKey(tagLower)) {
tagName = tagName.substring(1); TagIdentifier tagId = tagController.createTag(tag);
if(tagName.equalsIgnoreCase(tag)) { data.tagsByLCName.put(tagLower, tagId);
tagIdentifier = tagModel.getTagIdentifier(); tagsToAdd.add(tagId);
break; } else
} tagsToAdd.add(data.tagsByLCName.get(tagLower));
}
try {
if(tagIdentifier == null)
tagIdentifier = tagController.createTag(tag);
tagController.addTag(task.getTaskIdentifier(),
tagIdentifier);
} catch (Exception e) {
// tag already exists or something
} }
HashSet<TagIdentifier> tagsToDelete = new HashSet<TagIdentifier>(taskTags);
tagsToDelete.removeAll(tagsToAdd);
tagsToAdd.removeAll(taskTags);
for(TagIdentifier tagId : tagsToDelete)
tagController.removeTag(task.getTaskIdentifier(), tagId);
for(TagIdentifier tagId : tagsToAdd)
tagController.addTag(task.getTaskIdentifier(), tagId);
} }
stats.localUpdatedTasks++; stats.localUpdatedTasks++;
// try looking for this task if it doesn't already have a mapping
if(mapping == null) { if(mapping == null) {
// try looking for this task mapping = data.localIdToSyncMapping.get(task.getTaskIdentifier());
mapping = localIdToSyncMapping.get(task.getTaskIdentifier());
if(mapping == null) { if(mapping == null) {
try { try {
mapping = new SyncMapping(task.getTaskIdentifier(), remoteTask); mapping = new SyncMapping(task.getTaskIdentifier(), remoteTask);
syncController.saveSyncMapping(mapping); syncController.saveSyncMapping(mapping);
} catch (Exception e) { } catch (Exception e) {
// ignore - it'll get merged later // unique violation: ignore - it'll get merged later
} }
} }
stats.localCreatedTasks++; stats.localCreatedTasks++;
@ -401,6 +359,71 @@ public abstract class SynchronizationService {
// --- helper classes // --- helper classes
/** data structure builder */
private class SyncData {
HashSet<SyncMapping> mappings;
HashSet<TaskIdentifier> activeTasks;
HashSet<TaskIdentifier> allTasks;
HashMap<String, SyncMapping> remoteIdToSyncMapping;
HashMap<TaskIdentifier, SyncMapping> localIdToSyncMapping;
HashSet<SyncMapping> localChanges;
HashSet<TaskIdentifier> mappedTasks;
HashMap<TaskIdentifier, TaskProxy> remoteChangeMap;
HashMap<String, TaskProxy> newRemoteTasks;
HashMap<TagIdentifier, TagModelForView> tags;
HashMap<String, TagIdentifier> tagsByLCName;
HashSet<TaskIdentifier> newlyCreatedTasks;
HashSet<TaskIdentifier> deletedTasks;
public SyncData(Activity activity, LinkedList<TaskProxy> remoteTasks) {
// 1. get data out of the database
mappings = Synchronizer.getSyncController(activity).getSyncMapping(getId());
activeTasks = Synchronizer.getTaskController(activity).getActiveTaskIdentifiers();
allTasks = Synchronizer.getTaskController(activity).getAllTaskIdentifiers();
tags = Synchronizer.getTagController(activity).getAllTagsAsMap(activity);
// 2. build helper data structures
remoteIdToSyncMapping = new HashMap<String, SyncMapping>();
localIdToSyncMapping = new HashMap<TaskIdentifier, SyncMapping>();
localChanges = new HashSet<SyncMapping>();
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());
}
tagsByLCName = new HashMap<String, TagIdentifier>();
for(TagModelForView tag : tags.values())
tagsByLCName.put(tag.getName().toLowerCase(), tag.getTagIdentifier());
// 3. build map of remote tasks
remoteChangeMap = new HashMap<TaskIdentifier, TaskProxy>();
newRemoteTasks = new HashMap<String, TaskProxy>();
for(TaskProxy remoteTask : remoteTasks) {
if(remoteIdToSyncMapping.containsKey(remoteTask.getRemoteId())) {
SyncMapping mapping = remoteIdToSyncMapping.get(remoteTask.getRemoteId());
remoteChangeMap.put(mapping.getTask(), remoteTask);
} else if(remoteTask.name != null){
newRemoteTasks.put(remoteTask.name, remoteTask);
}
}
// 4. build data structures of things to do
newlyCreatedTasks = new HashSet<TaskIdentifier>(activeTasks);
newlyCreatedTasks.removeAll(mappedTasks);
deletedTasks = new HashSet<TaskIdentifier>(mappedTasks);
deletedTasks.removeAll(allTasks);
}
}
/** statistics tracking and displaying */
protected class SyncStats { protected class SyncStats {
int localCreatedTasks = 0; int localCreatedTasks = 0;
int localUpdatedTasks = 0; int localUpdatedTasks = 0;
@ -433,10 +456,10 @@ public abstract class SynchronizationService {
} }
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(getName()).append(" Sync Results:"); // TODO i18n sb.append(getName()).append("Results:"); // TODO i18n
sb.append("\n\n"); sb.append("\n\n");
sb.append(log); sb.append(log);
sb.append("\n--- Summary: Astrid Tasks ---"); sb.append("\nSummary - Astrid Tasks:");
if(localCreatedTasks > 0) if(localCreatedTasks > 0)
sb.append("\nCreated: " + localCreatedTasks); sb.append("\nCreated: " + localCreatedTasks);
if(localUpdatedTasks > 0) if(localUpdatedTasks > 0)
@ -447,7 +470,7 @@ public abstract class SynchronizationService {
if(mergedTasks > 0) if(mergedTasks > 0)
sb.append("\n\nMerged: " + localCreatedTasks); sb.append("\n\nMerged: " + localCreatedTasks);
sb.append("\n\n--- Summary: Remote Server ---"); sb.append("\n\nSummary - Remote Server:");
if(remoteCreatedTasks > 0) if(remoteCreatedTasks > 0)
sb.append("\nCreated: " + remoteCreatedTasks); sb.append("\nCreated: " + remoteCreatedTasks);
if(remoteUpdatedTasks > 0) if(remoteUpdatedTasks > 0)
@ -478,8 +501,9 @@ public abstract class SynchronizationService {
this.label = label; this.label = label;
} }
public void run() { public void run() {
if(!progressDialog.isShowing())
progressDialog.show();
progressDialog.setMessage(label); progressDialog.setMessage(label);
progressDialog.setProgress(0);
} }
} }
} }

@ -167,6 +167,8 @@ public class Synchronizer {
} }
public void set(TYPE newController) { public void set(TYPE newController) {
close();
override = newController != null; override = newController != null;
controller = newController; controller = newController;
} }

@ -1,9 +1,17 @@
package com.timsu.astrid.sync; package com.timsu.astrid.sync;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import android.app.Activity;
import com.timsu.astrid.data.enums.Importance; import com.timsu.astrid.data.enums.Importance;
import com.timsu.astrid.data.enums.RepeatInterval; import com.timsu.astrid.data.enums.RepeatInterval;
import com.timsu.astrid.data.tag.TagController;
import com.timsu.astrid.data.tag.TagIdentifier;
import com.timsu.astrid.data.tag.TagModelForView;
import com.timsu.astrid.data.task.TaskIdentifier;
import com.timsu.astrid.data.task.TaskModelForSync; import com.timsu.astrid.data.task.TaskModelForSync;
import com.timsu.astrid.data.task.AbstractTaskModel.RepeatInfo; import com.timsu.astrid.data.task.AbstractTaskModel.RepeatInfo;
@ -24,35 +32,35 @@ public class TaskProxy {
// --- fill these out // --- fill these out
String name = null; String name = null;
String notes = null; String notes = null;
Importance importance = null; Importance importance = null;
Integer progressPercentage = null; Integer progressPercentage = null;
Date creationDate = null; Date creationDate = null;
Date completionDate = null; Date completionDate = null;
Date definiteDueDate = null; Date definiteDueDate = null;
Date preferredDueDate = null; Date preferredDueDate = null;
Date hiddenUntil = null; Date hiddenUntil = null;
String[] tags = null; LinkedList<String> tags = null;
Integer estimatedSeconds = null; Integer estimatedSeconds = null;
Integer elapsedSeconds = null; Integer elapsedSeconds = null;
Integer repeatEveryNSeconds = null; Integer repeatEveryNSeconds = null;
// --- internal state // --- internal state
/** id of the synchronization service */ /** id of the synchronization service */
private int syncServiceId; private int syncServiceId;
/** id of this particular remote task */ /** id of this particular remote task */
private String syncTaskId; private String syncTaskId;
/** was the task deleted on the remote server */ /** was the task deleted on the remote server */
private boolean isDeleted = false; private boolean isDeleted = false;
public int getSyncServiceId() { public int getSyncServiceId() {
return syncServiceId; return syncServiceId;
@ -120,6 +128,18 @@ public class TaskProxy {
} }
} }
/** Read tags from the given tag controller */
public void readTagsFromController(Activity activity, TaskIdentifier taskId,
TagController tagController, HashMap<TagIdentifier, TagModelForView>
tagList) {
LinkedList<TagIdentifier> tagIds = tagController.getTaskTags(activity,
taskId);
tags = new LinkedList<String>();
for(TagIdentifier tagId : tagIds) {
tags.add(tagList.get(tagId).getName());
}
}
/** Write to the given task model */ /** Write to the given task model */
public void writeToTaskModel(TaskModelForSync task) { public void writeToTaskModel(TaskModelForSync task) {
if(name != null) if(name != null)
@ -141,8 +161,6 @@ public class TaskProxy {
if(hiddenUntil != null) if(hiddenUntil != null)
task.setHiddenUntil(hiddenUntil); task.setHiddenUntil(hiddenUntil);
// TODO tags
if(estimatedSeconds != null) if(estimatedSeconds != null)
task.setEstimatedSeconds(estimatedSeconds); task.setEstimatedSeconds(estimatedSeconds);
if(elapsedSeconds != null) if(elapsedSeconds != null)

@ -113,7 +113,7 @@ public class DateUtilities {
unitsDisplayed++; unitsDisplayed++;
} }
if(unitsDisplayed < unitsToShow && seconds > 0) { if(unitsDisplayed < unitsToShow && seconds > 0) {
result.append(hours).append(" s "); result.append(seconds).append(" s ");
} }
return result.toString(); return result.toString();

@ -16,6 +16,7 @@ public class Preferences {
// pref keys // pref keys
private static final String P_CURRENT_VERSION = "cv"; private static final String P_CURRENT_VERSION = "cv";
private static final String P_SHOW_REPEAT_HELP = "repeathelp"; private static final String P_SHOW_REPEAT_HELP = "repeathelp";
private static final String P_TASK_LIST_SORT = "tlsort";
private static final String P_SYNC_RTM_TOKEN = "rtmtoken"; private static final String P_SYNC_RTM_TOKEN = "rtmtoken";
private static final String P_SYNC_RTM_LAST_SYNC = "rtmlastsync"; private static final String P_SYNC_RTM_LAST_SYNC = "rtmlastsync";
private static final String P_SYNC_LAST_SYNC = "lastsync"; private static final String P_SYNC_LAST_SYNC = "lastsync";
@ -122,6 +123,18 @@ public class Preferences {
return getIntegerValue(context, R.string.p_notif_defaultRemind); return getIntegerValue(context, R.string.p_notif_defaultRemind);
} }
/** TaskListSort: the sorting method for the task list */
public static int getTaskListSort(Context context) {
return getPrefs(context).getInt(P_TASK_LIST_SORT, 0);
}
/** TaskListSort: the sorting method for the task list */
public static void setTaskListSort(Context context, int value) {
Editor editor = getPrefs(context).edit();
editor.putInt(P_TASK_LIST_SORT, value);
editor.commit();
}
// --- synchronization preferences // --- synchronization preferences
/** RTM authentication token, or null if doesn't exist */ /** RTM authentication token, or null if doesn't exist */

Loading…
Cancel
Save