Merge remote-tracking branch 'origin/121105_sb_sync_subtasks'

pull/14/head
Sam Bosley 13 years ago
commit ded2f5d674

@ -50,6 +50,7 @@ import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.billing.BillingConstants;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.TagDataDao;
import com.todoroo.astrid.dao.TaskDao;
@ -75,6 +76,8 @@ import com.todoroo.astrid.service.StatisticsService;
import com.todoroo.astrid.service.TagDataService;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.service.abtesting.ABTestEventReportingService;
import com.todoroo.astrid.subtasks.SubtasksHelper;
import com.todoroo.astrid.subtasks.SubtasksUpdater;
import com.todoroo.astrid.sync.SyncV2Provider.SyncExceptionHandler;
import com.todoroo.astrid.tags.TagService;
import com.todoroo.astrid.tags.reusable.FeaturedListFilterExposer;
@ -130,11 +133,17 @@ public final class ActFmSyncService {
private final List<FailedPush> failedPushes = Collections.synchronizedList(new LinkedList<FailedPush>());
private Thread pushRetryThread = null;
private Runnable pushRetryRunnable;
private Thread pushTagOrder = null;
private Runnable pushTagOrderRunnable;
private final List<Long> tagOrderQueue = Collections.synchronizedList(new LinkedList<Long>());
private final AtomicInteger taskPushThreads = new AtomicInteger(0);
private final ConditionVariable waitUntilEmpty = new ConditionVariable(true);
public void initialize() {
initializeRetryRunnable();
initializeTagOrderRunnable();
taskDao.addListener(new ModelUpdateListener<Task>() {
@Override
@ -251,6 +260,31 @@ public final class ActFmSyncService {
};
}
private static final long WAIT_BEFORE_PUSH_ORDER = 15 * 1000;
private void initializeTagOrderRunnable() {
pushTagOrderRunnable = new Runnable() {
@Override
public void run() {
while (true) {
if(tagOrderQueue.isEmpty()) {
synchronized(ActFmSyncService.this) {
pushTagOrder = null;
return;
}
}
if (tagOrderQueue.size() > 0) {
AndroidUtilities.sleepDeep(WAIT_BEFORE_PUSH_ORDER);
Long tagDataId = tagOrderQueue.remove(0);
TagData td = tagDataService.fetchById(tagDataId, TagData.ID, TagData.REMOTE_ID, TagData.TAG_ORDERING);
if (td != null) {
pushTagOrdering(td);
}
}
}
}
};
}
private void addFailedPush(FailedPush fp) {
failedPushes.add(fp);
synchronized(this) {
@ -549,6 +583,40 @@ public final class ActFmSyncService {
pushUpdateOnSave(update, update.getMergedValues(), imageData);
}
public void pushTagOrderingOnSave(long tagDataId) {
if (!tagOrderQueue.contains(tagDataId)) {
tagOrderQueue.add(tagDataId);
synchronized(this) {
if(pushTagOrder == null) {
pushTagOrder = new Thread(pushTagOrderRunnable);
pushTagOrder.start();
}
}
}
}
private void pushTagOrdering(TagData tagData) {
if (!checkForToken())
return;
Long remoteId = tagData.getValue(TagData.REMOTE_ID);
if (remoteId == null || remoteId <= 0)
return;
ArrayList<Object> params = new ArrayList<Object>();
params.add("id"); params.add(remoteId);
params.add("order");
params.add(SubtasksHelper.convertTreeToRemoteIds(tagData.getValue(TagData.TAG_ORDERING)));
params.add("token"); params.add(token);
try {
actFmInvoker.invoke("tag_save", params.toArray(new Object[params.size()]));
} catch (IOException e) {
handleException("push-tag-order", e);
}
}
/**
* Send tagData changes to server
* @param setValues
@ -937,7 +1005,13 @@ public final class ActFmSyncService {
* @param done
*/
public void fetchActiveTasks(final boolean manual, SyncExceptionHandler handler, Runnable done) {
invokeFetchList("task", manual, handler, new TaskListItemProcessor(manual), done, "active_tasks");
invokeFetchList("task", manual, handler, new TaskListItemProcessor(manual) {
@Override
protected void saveOrdering(JSONArray ordering) {
String localOrdering = SubtasksHelper.convertTreeToLocalIds(ordering.toString());
Preferences.setString(SubtasksUpdater.ACTIVE_TASKS_ORDER, localOrdering);
}
}, done, "active_tasks");
}
/**
@ -956,6 +1030,13 @@ public final class ActFmSyncService {
Task.REMOTE_ID.isNotNull(),
Criterion.not(Task.ID.in(localIds))));
}
@Override
protected void saveOrdering(JSONArray ordering) {
String localOrdering = SubtasksHelper.convertTreeToLocalIds(ordering.toString());
tagData.setValue(TagData.TAG_ORDERING, localOrdering);
PluginServices.getTagDataService().save(tagData);
}
}, done, "tasks:" + tagData.getId(), "tag_id", tagData.getValue(TagData.REMOTE_ID));
}
@ -1195,6 +1276,10 @@ public final class ActFmSyncService {
}
}
public void processExtras(JSONObject fullResult) {
// Subclasses can override if they want to examine the full JSONObject for other information
}
protected void readRemoteIds(JSONArray list) throws JSONException {
remoteIds = new Long[list.length()];
for(int i = 0; i < list.length(); i++)
@ -1229,6 +1314,7 @@ public final class ActFmSyncService {
}
}
}
private class TaskListItemProcessor extends ListItemProcessor<Task> {
@ -1241,6 +1327,30 @@ public final class ActFmSyncService {
this.modificationDates = new HashMap<Long, Long>();
}
protected void saveOrdering(JSONArray ordering) {
// Subclasses should override
}
@Override
public void processExtras(JSONObject fullResult) {
if (!fullResult.has("order"))
return;
try {
JSONArray ordering = fullResult.getJSONArray("order");
if (ordering.optLong(0) != -1L) {
JSONArray newOrdering = new JSONArray();
newOrdering.put(-1L);
for (int i = 0; i < ordering.length(); i++)
newOrdering.put(ordering.get(i));
ordering = newOrdering;
}
saveOrdering(ordering);
} catch (JSONException e) {
Log.e("sync-ordering", "Error getting ordering from result " + fullResult , e);
}
}
@Override
protected void mergeAndSave(JSONArray list, HashMap<Long,Long> locals, long serverTime) throws JSONException {
Task remote = new Task();
@ -1421,6 +1531,7 @@ public final class ActFmSyncService {
long serverTime = result.optLong("time", 0);
JSONArray list = result.getJSONArray("list");
processor.process(list, serverTime);
processor.processExtras(result);
Preferences.setLong("actfm_time_" + lastSyncKey, serverTime);
Preferences.setLong("actfm_last_" + lastSyncKey, DateUtilities.now());

@ -31,7 +31,7 @@ public abstract class AstridOrderedListUpdater<LIST> {
}
public static class Node {
public final long taskId;
public long taskId;
public Node parent;
public int indent;
public final ArrayList<Node> children = new ArrayList<Node>();
@ -63,7 +63,12 @@ public abstract class AstridOrderedListUpdater<LIST> {
}
public void initializeFromSerializedTree(LIST list, Filter filter, String serializedTree) {
treeRoot = buildTreeModel(serializedTree);
treeRoot = buildTreeModel(serializedTree, new JSONTreeModelBuilder() {
@Override
public void afterAddNode(Node node) {
idToNode.put(node.taskId, node);
}
});
verifyTreeModel(list, filter);
}
@ -303,51 +308,60 @@ public abstract class AstridOrderedListUpdater<LIST> {
applyToFilter(filter);
}
private Node buildTreeModel(String serializedTree) {
private interface JSONTreeModelBuilder {
void afterAddNode(Node node);
}
public static Node buildTreeModel(String serializedTree, JSONTreeModelBuilder callback) {
Node root = new Node(-1, null, -1);
try {
JSONArray tree = new JSONArray(serializedTree);
recursivelyBuildChildren(root, tree);
recursivelyBuildChildren(root, tree, callback);
} catch (JSONException e) {
Log.e("OrderedListUpdater", "Error building tree model", e); //$NON-NLS-1$//$NON-NLS-2$
}
return root;
}
private void recursivelyBuildChildren(Node node, JSONArray children) throws JSONException {
private static void recursivelyBuildChildren(Node node, JSONArray children, JSONTreeModelBuilder callback) throws JSONException {
for (int i = 1; i < children.length(); i++) {
JSONArray subarray = children.optJSONArray(i);
if (subarray == null) {
Long id = children.getLong(i);
Node child = new Node(id, node, node.indent + 1);
node.children.add(child);
idToNode.put(id, child);
if (callback != null)
callback.afterAddNode(child);
} else {
Long id = subarray.getLong(0);
Node child = new Node(id, node, node.indent + 1);
recursivelyBuildChildren(child, subarray);
recursivelyBuildChildren(child, subarray, callback);
node.children.add(child);
idToNode.put(id, child);
if (callback != null)
callback.afterAddNode(child);
}
}
}
protected String serializeTree() {
return serializeTree(treeRoot);
}
public static String serializeTree(Node root) {
JSONArray tree = new JSONArray();
if (treeRoot == null) {
if (root == null) {
return tree.toString();
}
try {
tree.put(-1L);
recursivelySerialize(treeRoot, tree);
recursivelySerialize(root, tree);
} catch (JSONException e) {
Log.e("OrderedListUpdater", "Error serializing tree model", e); //$NON-NLS-1$//$NON-NLS-2$
}
return tree.toString();
}
public static void recursivelySerialize(Node node, JSONArray serializeTo) throws JSONException {
private static void recursivelySerialize(Node node, JSONArray serializeTo) throws JSONException {
ArrayList<Node> children = node.children;
serializeTo.put(node.taskId);
for (Node child : children) {

@ -1,13 +1,16 @@
package com.todoroo.astrid.subtasks;
import java.util.ArrayList;
import java.util.HashMap;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.TagViewFragment;
import com.todoroo.astrid.api.Filter;
@ -18,6 +21,7 @@ import com.todoroo.astrid.core.SortHelper;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.subtasks.AstridOrderedListUpdater.Node;
import com.todoroo.astrid.utility.AstridPreferences;
public class SubtasksHelper {
@ -75,14 +79,13 @@ public class SubtasksHelper {
else
serialized = Preferences.getStringValue(SubtasksUpdater.ACTIVE_TASKS_ORDER);
ArrayList<Long> ids = getIdArray(serialized);
return AstridOrderedListUpdater.buildOrderString(ids.toArray(new Long[ids.size()]));
return AstridOrderedListUpdater.buildOrderString(getIdArray(serialized));
}
@SuppressWarnings("nls")
private static ArrayList<Long> getIdArray(String serializedTree) {
public static Long[] getIdArray(String serializedTree) {
ArrayList<Long> ids = new ArrayList<Long>();
String[] digitsOnly = serializedTree.split("\\D+");
String[] digitsOnly = serializedTree.split("[\\[\\],\\s]"); // Split on [ ] , or whitespace chars
for (String idString : digitsOnly) {
try {
if (!TextUtils.isEmpty(idString))
@ -91,7 +94,76 @@ public class SubtasksHelper {
Log.e("widget-subtasks", "error parsing id " + idString, e);
}
}
return ids;
return ids.toArray(new Long[ids.size()]);
}
public static String convertTreeToRemoteIds(String localTree) {
return convertIdTree(localTree, true);
}
public static String convertTreeToLocalIds(String remoteTree) {
return convertIdTree(remoteTree, false);
}
private static String convertIdTree(String treeString, boolean localToRemote) {
Long[] ids = getIdArray(treeString);
HashMap<Long, Long> idMap = buildIdMap(ids, localToRemote);
idMap.put(-1L, -1L);
Node tree = AstridOrderedListUpdater.buildTreeModel(treeString, null);
remapTree(tree, idMap);
return AstridOrderedListUpdater.serializeTree(tree);
}
private static void remapTree(Node root, HashMap<Long, Long> idMap) {
ArrayList<Node> children = root.children;
for (int i = 0; i < children.size(); i++) {
Node child = children.get(i);
Long remoteId = idMap.get(child.taskId);
if (remoteId == null || remoteId <= 0) {
children.remove(i);
children.addAll(i, child.children);
i--;
} else {
child.taskId = remoteId;
remapTree(child, idMap);
}
}
}
private static HashMap<Long, Long> buildIdMap(Long[] localIds, boolean localToRemote) { // If localToRemote is true, keys are local ids. If false. keys are remtoe ids
HashMap<Long, Long> map = new HashMap<Long, Long>();
Criterion criterion;
if (localToRemote)
criterion = Task.ID.in(localIds);
else
criterion = Task.REMOTE_ID.in(localIds);
TodorooCursor<Task> tasks = PluginServices.getTaskService().query(Query.select(Task.ID, Task.REMOTE_ID).where(criterion));
try {
Task t = new Task();
for (tasks.moveToFirst(); !tasks.isAfterLast(); tasks.moveToNext()) {
t.clear();
t.readFromCursor(tasks);
if (t.containsNonNullValue(Task.REMOTE_ID)) {
Long key;
Long value;
if (localToRemote) {
key = t.getId();
value = t.getValue(Task.REMOTE_ID);
} else {
key = t.getValue(Task.REMOTE_ID);
value = t.getId();
}
map.put(key, value);
}
}
} finally {
tasks.close();
}
return map;
}
}

@ -2,9 +2,6 @@ package com.todoroo.astrid.subtasks;
import java.util.ArrayList;
import org.json.JSONArray;
import org.json.JSONException;
import android.util.Log;
import com.todoroo.andlib.data.TodorooCursor;
@ -99,14 +96,7 @@ public class SubtasksMetadataMigration {
Node newNode = new Node(item.getValue(Metadata.TASK), parent, parent.indent + 1);
parent.children.add(newNode);
}
try {
JSONArray array = new JSONArray();
AstridOrderedListUpdater.recursivelySerialize(root, array);
return array.toString();
} catch (JSONException e) {
return "[]"; //$NON-NLS-1$
}
return AstridOrderedListUpdater.serializeTree(root);
}
private Node findNextParentForIndent(Node root, int indent) {

@ -79,4 +79,9 @@ public class SubtasksTagListFragment extends TagViewFragment {
return helper.createTaskAdapter(cursor, sqlQueryTemplate);
}
@Override
protected void refresh() {
setUpTaskList();
}
}

@ -8,6 +8,7 @@ package com.todoroo.astrid.subtasks;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.sync.ActFmSyncService;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.TagData;
@ -19,6 +20,7 @@ public class SubtasksUpdater extends AstridOrderedListUpdater<TagData> {
@Autowired TagDataService tagDataService;
@Autowired TaskService taskService;
@Autowired ActFmSyncService actFmSyncService;
public static final String ACTIVE_TASKS_ORDER = "active_tasks_order"; //$NON-NLS-1$
@ -64,6 +66,7 @@ public class SubtasksUpdater extends AstridOrderedListUpdater<TagData> {
} else {
list.setValue(TagData.TAG_ORDERING, serialized);
tagDataService.save(list);
actFmSyncService.pushTagOrderingOnSave(list.getId());
}
}

@ -0,0 +1,59 @@
package com.todoroo.astrid.subtasks;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.data.Task;
public class SubtasksHelperTest extends SubtasksTestCase {
private Task A, B, C, D, E, F;
@Override
protected void setUp() throws Exception {
super.setUp();
createTasks();
updater.initializeFromSerializedTree(null, filter, DEFAULT_SERIALIZED_TREE);
}
private Task createTask(String title, long remoteId) {
Task t = new Task();
t.setValue(Task.TITLE, title);
t.setValue(Task.REMOTE_ID, remoteId);
PluginServices.getTaskService().save(t);
return t;
}
private void createTasks() {
A = createTask("A", 6); // Local id 1
B = createTask("B", 4); // Local id 2
C = createTask("C", 3); // Local id 3
D = createTask("D", 1); // Local id 4
E = createTask("E", 2); // Local id 5
F = createTask("F", 5); // Local id 6
}
private static final Long[] EXPECTED_ORDER = {-1L, 1L, 2L, 3L, 4L, 5L, 6L };
public void testOrderedIdArray() {
Long[] ids = SubtasksHelper.getIdArray(DEFAULT_SERIALIZED_TREE);
assertEquals(EXPECTED_ORDER.length, ids.length);
for (int i = 0; i < EXPECTED_ORDER.length; i++) {
assertEquals(EXPECTED_ORDER[i], ids[i]);
}
}
// Default order: "[-1, [1, 2, [3, 4]], 5, 6]"
private static String EXPECTED_REMOTE = "[-1, [6, 4, [3, 1]], 2, 5]".replaceAll("\\s", "");
public void testLocalToRemoteIdMapping() {
String mapped = SubtasksHelper.convertTreeToRemoteIds(DEFAULT_SERIALIZED_TREE).replaceAll("\\s", "");
assertEquals(EXPECTED_REMOTE, mapped);
}
private static String EXPECTED_LOCAL = "[-1, [4, 5, [3, 2]], 6, 1]".replaceAll("\\s", "");
public void testRemoteToLocalIdMapping() {
String mapped = SubtasksHelper.convertTreeToLocalIds(DEFAULT_SERIALIZED_TREE).replaceAll("\\s", "");
assertEquals(EXPECTED_LOCAL, mapped);
}
}

@ -12,6 +12,14 @@ public class SubtasksMovingTest extends SubtasksTestCase {
super.setUp();
createTasks();
updater.initializeFromSerializedTree(null, filter, DEFAULT_SERIALIZED_TREE);
// Assert initial state is correct
expectParentAndPosition(A, null, 0);
expectParentAndPosition(B, A, 0);
expectParentAndPosition(C, A, 1);
expectParentAndPosition(D, C, 0);
expectParentAndPosition(E, null, 1);
expectParentAndPosition(F, null, 2);
}
private void createTasks() {

@ -27,7 +27,6 @@ public class SubtasksTestCase extends DatabaseTestCase {
* F
*/
public static final String DEFAULT_SERIALIZED_TREE = "[-1, [1, 2, [3, 4]], 5, 6]".replaceAll("\\s", "");
//"[{\"1\":[{\"2\":[]}, {\"3\":[{\"4\":[]}]}]}, {\"5\":[]}, {\"6\":[]}]".replaceAll("\\s", "");
@Override
protected void setUp() throws Exception {

Loading…
Cancel
Save