Merge branch 'subtasks_performance'

pull/14/head
Sam Bosley 12 years ago
commit 360445034a

@ -114,6 +114,10 @@ public final class TagData extends RemoteModel {
public static final StringProperty TAG_DESCRIPTION = new StringProperty(
TABLE, "tagDescription");
/** Tag ordering */
public static final StringProperty TAG_ORDERING = new StringProperty(
TABLE, "tagOrdering");
/** List of all properties for this model */
public static final Property<?>[] PROPERTIES = generateProperties(TagData.class);
@ -154,6 +158,7 @@ public final class TagData extends RemoteModel {
defaultValues.put(IS_UNREAD.name, 0);
defaultValues.put(TASK_COUNT.name, 0);
defaultValues.put(TAG_DESCRIPTION.name, "");
defaultValues.put(TAG_ORDERING.name, "[]");
}
@Override

@ -6,8 +6,8 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.timsu.astrid"
android:versionName="4.4.1"
android:versionCode="286">
android:versionName="4.4.2-subtasks"
android:versionCode="287">
<!-- widgets, alarms, and services will break if Astrid is installed on SD card -->
<!-- android:installLocation="internalOnly"> -->
@ -174,6 +174,10 @@
<activity android:name="com.todoroo.astrid.service.UpdateMessagePreference"
android:theme="@android:style/Theme" />
<activity android:name="com.todoroo.astrid.service.UpgradeService$UpgradeActivity"
android:screenOrientation="portrait"
android:theme="@android:style/Theme" />
<!-- Start of Crittercism.com Code -->
<activity android:name="com.crittercism.NewFeedbackSpringboardActivity" android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"></activity>
<activity android:name="com.crittercism.NewFeedbackIssueListActivity" android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"></activity>

@ -27,7 +27,9 @@ import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.helper.ProgressBarSyncResultCallback;
import com.todoroo.astrid.service.SyncV2Service;
import com.todoroo.astrid.service.ThemeService;
import com.todoroo.astrid.subtasks.OrderedListFragmentHelper;
import com.todoroo.astrid.subtasks.AstridOrderedListFragmentHelper;
import com.todoroo.astrid.subtasks.OrderedListFragmentHelperInterface;
import com.todoroo.astrid.subtasks.OrderedMetadataListFragmentHelper;
import com.todoroo.astrid.subtasks.SubtasksListFragment;
public class GtasksListFragment extends SubtasksListFragment {
@ -60,8 +62,8 @@ public class GtasksListFragment extends SubtasksListFragment {
};
@Override
protected OrderedListFragmentHelper<?> createFragmentHelper() {
return new OrderedListFragmentHelper<StoreObject>(this, gtasksTaskListUpdater);
protected OrderedListFragmentHelperInterface<?> createFragmentHelper() {
return new OrderedMetadataListFragmentHelper<StoreObject>(this, gtasksTaskListUpdater);
}
@Override
@ -75,7 +77,7 @@ public class GtasksListFragment extends SubtasksListFragment {
long storeObjectId = extras.getLong(TOKEN_STORE_ID, 0);
list = storeObjectDao.fetch(storeObjectId, LIST_PROPERTIES);
((OrderedListFragmentHelper<StoreObject>)helper).setList(list);
((AstridOrderedListFragmentHelper<StoreObject>)helper).setList(list);
}
@Override

@ -29,7 +29,7 @@ import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.sync.GtasksTaskContainer;
import com.todoroo.astrid.subtasks.OrderedListUpdater.OrderedListIterator;
import com.todoroo.astrid.subtasks.OrderedMetadataListUpdater.OrderedListIterator;
import com.todoroo.astrid.sync.SyncMetadataService;
import com.todoroo.astrid.sync.SyncProviderUtilities;

@ -28,10 +28,10 @@ import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.sync.GtasksSyncService;
import com.todoroo.astrid.subtasks.OrderedListUpdater;
import com.todoroo.astrid.subtasks.OrderedListUpdater.OrderedListIterator;
import com.todoroo.astrid.subtasks.OrderedMetadataListUpdater;
import com.todoroo.astrid.subtasks.OrderedMetadataListUpdater.OrderedListIterator;
public class GtasksTaskListUpdater extends OrderedListUpdater<StoreObject> {
public class GtasksTaskListUpdater extends OrderedMetadataListUpdater<StoreObject> {
/** map of task -> parent task */
final HashMap<Long, Long> parents = new HashMap<Long, Long>();

@ -0,0 +1,321 @@
package com.todoroo.astrid.subtasks;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.ListView;
import com.commonsware.cwac.tlv.TouchListView.DropListener;
import com.commonsware.cwac.tlv.TouchListView.GrabberClickListener;
import com.commonsware.cwac.tlv.TouchListView.SwipeListener;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.adapter.TaskAdapter;
import com.todoroo.astrid.adapter.TaskAdapter.OnCompletedTaskListener;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.service.ThemeService;
import com.todoroo.astrid.ui.DraggableListView;
import com.todoroo.astrid.utility.AstridPreferences;
public class AstridOrderedListFragmentHelper<LIST> implements OrderedListFragmentHelperInterface<LIST> {
private final DisplayMetrics metrics = new DisplayMetrics();
private final AstridOrderedListUpdater<LIST> updater;
private final TaskListFragment fragment;
@Autowired TaskService taskService;
@Autowired MetadataService metadataService;
private DraggableTaskAdapter taskAdapter;
private LIST list;
public AstridOrderedListFragmentHelper(TaskListFragment fragment, AstridOrderedListUpdater<LIST> updater) {
DependencyInjectionService.getInstance().inject(this);
this.fragment = fragment;
this.updater = updater;
}
// --- ui component setup
private Activity getActivity() {
return fragment.getActivity();
}
private ListView getListView() {
return fragment.getListView();
}
private Filter getFilter() {
return fragment.getFilter();
}
public DraggableListView getTouchListView() {
DraggableListView tlv = (DraggableListView) fragment.getListView();
return tlv;
}
public void setUpUiComponents() {
TypedValue tv = new TypedValue();
getActivity().getTheme().resolveAttribute(R.attr.asThemeTextColor, tv, false);
getTouchListView().setDragndropBackgroundColor(tv.data);
getTouchListView().setDropListener(dropListener);
getTouchListView().setClickListener(rowClickListener);
getTouchListView().setSwipeListener(swipeListener);
getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
if(Preferences.getInt(AstridPreferences.P_SUBTASKS_HELP, 0) == 0)
showSubtasksHelp();
}
@SuppressWarnings("nls")
private void showSubtasksHelp() {
String body = String.format("<h3>%s</h3><img src='%s'>" +
"<br>%s<br><br><br><img src='%s'><br>%s",
getActivity().getString(R.string.subtasks_help_1),
"subtasks_vertical.png",
getActivity().getString(R.string.subtasks_help_2),
"subtasks_horizontal.png",
getActivity().getString(R.string.subtasks_help_3));
String color = ThemeService.getDialogTextColorString();
String html = String.format("<html><body style='text-align:center;color:%s'>%s</body></html>",
color, body);
DialogUtilities.htmlDialog(getActivity(), html, R.string.subtasks_help_title);
Preferences.setInt(AstridPreferences.P_SUBTASKS_HELP, 1);
}
public void beforeSetUpTaskList(Filter filter) {
updater.initialize(list, filter);
}
private final DropListener dropListener = new DropListener() {
@Override
public void drop(int from, int to) {
long targetTaskId = taskAdapter.getItemId(from);
if (targetTaskId <= 0) return; // This can happen with gestures on empty parts of the list (e.g. extra space below tasks)
long destinationTaskId = taskAdapter.getItemId(to);
try {
if(to >= getListView().getCount())
updater.moveTo(list, getFilter(), targetTaskId, -1);
else
updater.moveTo(list, getFilter(), targetTaskId, destinationTaskId);
} catch (Exception e) {
Log.e("drag", "Drag Error", e); //$NON-NLS-1$ //$NON-NLS-2$
}
fragment.reconstructCursor();
fragment.loadTaskListContent(true);
}
};
private final SwipeListener swipeListener = new SwipeListener() {
@Override
public void swipeRight(int which) {
indent(which, 1);
}
@Override
public void swipeLeft(int which) {
indent(which, -1);
}
protected void indent(int which, int delta) {
long targetTaskId = taskAdapter.getItemId(which);
if (targetTaskId <= 0) return; // This can happen with gestures on empty parts of the list (e.g. extra space below tasks)
try {
updater.indent(list, getFilter(), targetTaskId, delta);
} catch (Exception e) {
Log.e("drag", "Indent Error", e); //$NON-NLS-1$ //$NON-NLS-2$
}
fragment.reconstructCursor();
fragment.loadTaskListContent(true);
}
};
private final GrabberClickListener rowClickListener = new GrabberClickListener() {
@Override
public void onLongClick(final View v) {
if(v == null)
return;
fragment.registerForContextMenu(getListView());
getListView().showContextMenuForChild(v);
fragment.unregisterForContextMenu(getListView());
}
@Override
public void onClick(View v) {
if(v == null)
return;
((DraggableTaskAdapter) taskAdapter).getListener().onClick(v);
}
};
public TaskAdapter createTaskAdapter(TodorooCursor<Task> cursor,
AtomicReference<String> sqlQueryTemplate) {
int resource = Preferences.getBoolean(R.string.p_taskRowStyle, false) ?
R.layout.task_adapter_row_simple : R.layout.task_adapter_row;
taskAdapter = new DraggableTaskAdapter(fragment, resource,
cursor, sqlQueryTemplate, false, null);
taskAdapter.addOnCompletedTaskListener(new OnCompletedTaskListener() {
@Override
public void onCompletedTask(Task item, boolean newState) {
setCompletedForItemAndSubtasks(item, newState);
}
});
return taskAdapter;
}
private final class DraggableTaskAdapter extends TaskAdapter {
private DraggableTaskAdapter(TaskListFragment activity, int resource,
Cursor c, AtomicReference<String> query, boolean autoRequery,
OnCompletedTaskListener onCompletedTaskListener) {
super(activity, resource, c, query, autoRequery,
onCompletedTaskListener);
applyListeners = APPLY_LISTENERS_NONE;
}
@Override
protected ViewHolder getTagFromCheckBox(View v) {
return (ViewHolder)((View)v.getParent()).getTag();
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = super.newView(context, cursor, parent);
view.getLayoutParams().height = Math.round(45 * metrics.density);
ViewHolder vh = (ViewHolder) view.getTag();
MarginLayoutParams rowParams = (MarginLayoutParams) vh.rowBody.getLayoutParams();
rowParams.topMargin = rowParams.bottomMargin = 0;
ViewGroup.LayoutParams pictureParams = vh.picture.getLayoutParams();
pictureParams.width = pictureParams.height = Math.round(38 * metrics.density);
pictureParams = vh.pictureBorder.getLayoutParams();
pictureParams.width = pictureParams.height = Math.round(38 * metrics.density);
return view;
}
@Override
public synchronized void setFieldContentsAndVisibility(View view) {
super.setFieldContentsAndVisibility(view);
ViewHolder vh = (ViewHolder) view.getTag();
int indent = updater.getIndentForTask(vh.task.getId());
vh.rowBody.setPadding(Math.round(indent * 20 * metrics.density), 0, 0, 0);
}
@Override
protected void addListeners(View container) {
super.addListeners(container);
}
public TaskRowListener getListener() {
return listener;
}
}
private final Map<Long, ArrayList<Long>> chainedCompletions =
Collections.synchronizedMap(new HashMap<Long, ArrayList<Long>>());
private void setCompletedForItemAndSubtasks(final Task item, final boolean completedState) {
final long itemId = item.getId();
final Task model = new Task();
final long completionDate = completedState ? DateUtilities.now() : 0;
if(completedState == false) {
ArrayList<Long> chained = chainedCompletions.get(itemId);
if(chained != null) {
for(Long taskId : chained) {
model.setId(taskId);
model.setValue(Task.COMPLETION_DATE, completionDate);
taskService.save(model);
model.clear();
taskAdapter.getCompletedItems().put(taskId, false);
}
taskAdapter.notifyDataSetInvalidated();
}
return;
}
final ArrayList<Long> chained = new ArrayList<Long>();
updater.applyToDescendants(itemId, new AstridOrderedListUpdater.OrderedListNodeVisitor() {
@Override
public void visitNode(AstridOrderedListUpdater.Node node) {
// Task childTask = taskService.fetchById(node.taskId, Task.RECURRENCE);
//
// if(!TextUtils.isEmpty(childTask.getValue(Task.RECURRENCE))) {
// Metadata metadata = updater.getTaskMetadata(list, node.taskId);
// metadata.setValue(updater.indentProperty(), parentIndent);
// metadataService.save(metadata);
// }
model.setId(node.taskId);
model.setValue(Task.COMPLETION_DATE, completionDate);
taskService.save(model);
model.clear();
taskAdapter.getCompletedItems().put(node.taskId, true);
chained.add(node.taskId);
}
});
if(chained.size() > 0) {
chainedCompletions.put(itemId, chained);
taskAdapter.notifyDataSetInvalidated();
}
}
public void setList(LIST list) {
this.list = list;
}
public void onCreateTask(Task task) {
updater.onCreateTask(list, getFilter(), task.getId());
fragment.reconstructCursor();
fragment.loadTaskListContent(true);
}
public void onDeleteTask(Task task) {
updater.onDeleteTask(list, getFilter(), task.getId());
taskAdapter.notifyDataSetInvalidated();
}
}

@ -0,0 +1,361 @@
package com.todoroo.astrid.subtasks;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Log;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.TaskService;
public abstract class AstridOrderedListUpdater<LIST> {
@Autowired
private TaskService taskService;
public AstridOrderedListUpdater() {
DependencyInjectionService.getInstance().inject(this);
idToNode = new HashMap<Long, Node>();
}
public interface OrderedListNodeVisitor {
public void visitNode(Node node);
}
public static class Node {
public final long taskId;
public Node parent;
public int indent;
public final ArrayList<Node> children = new ArrayList<Node>();
public Node(long taskId, Node parent, int indent) {
this.taskId = taskId;
this.parent = parent;
this.indent = indent;
}
}
private Node treeRoot;
private final HashMap<Long, Node> idToNode;
protected abstract String getSerializedTree(LIST list, Filter filter);
protected abstract void writeSerialization(LIST list, String serialized);
protected abstract void applyToFilter(Filter filter);
public int getIndentForTask(long targetTaskId) {
Node n = idToNode.get(targetTaskId);
if (n == null)
return 0;
return n.indent;
}
public void initialize(LIST list, Filter filter) {
initializeFromSerializedTree(list, filter, getSerializedTree(list, filter));
}
public void initializeFromSerializedTree(LIST list, Filter filter, String serializedTree) {
treeRoot = buildTreeModel(serializedTree);
verifyTreeModel(list, filter);
}
private void verifyTreeModel(LIST list, Filter filter) {
boolean addedThings = false;
TodorooCursor<Task> tasks = taskService.fetchFiltered(filter.getSqlQuery(), null, Task.ID);
try {
for (tasks.moveToFirst(); !tasks.isAfterLast(); tasks.moveToNext()) {
Long id = tasks.getLong(0);
if (idToNode.containsKey(id))
continue;
addedThings = true;
Node newNode = new Node(id, treeRoot, 0);
treeRoot.children.add(newNode);
idToNode.put(id, newNode);
}
} finally {
tasks.close();
}
if (addedThings)
writeSerialization(list, serializeTree());
}
public Node findNodeForTask(long taskId) {
return idToNode.get(taskId);
}
public Long[] getOrderedIds() {
ArrayList<Long> ids = new ArrayList<Long>();
orderedIdHelper(treeRoot, ids);
return ids.toArray(new Long[ids.size()]);
}
public String getOrderString() {
Long[] ids = getOrderedIds();
return buildOrderString(ids);
}
public static String buildOrderString(Long[] ids) {
StringBuilder builder = new StringBuilder();
if (ids.length == 0)
return "(1)"; //$NON-NLS-1$
for (int i = ids.length - 1; i >= 0; i--) {
builder.append(Task.ID.eq(ids[i]).toString());
if (i > 0)
builder.append(", "); //$NON-NLS-1$
}
return builder.toString();
}
private void orderedIdHelper(Node node, List<Long> ids) {
if (node != treeRoot)
ids.add(node.taskId);
for (Node child : node.children) {
orderedIdHelper(child, ids);
}
}
public void applyToDescendants(long taskId, OrderedListNodeVisitor visitor) {
Node n = idToNode.get(taskId);
if (n == null)
return;
applyToDescendantsHelper(n, visitor);
}
private void applyToDescendantsHelper(Node n, OrderedListNodeVisitor visitor) {
ArrayList<Node> children = n.children;
for (Node child : children) {
visitor.visitNode(child);
applyToDescendantsHelper(child, visitor);
}
}
public void iterateOverList(OrderedListNodeVisitor visitor) {
applyToDescendantsHelper(treeRoot, visitor);
}
public void indent(LIST list, Filter filter, long targetTaskId, int delta) {
Node node = idToNode.get(targetTaskId);
indentHelper(list, filter, node, delta);
}
private void indentHelper(LIST list, Filter filter, Node node, int delta) {
if (node == null)
return;
if (delta == 0)
return;
Node parent = node.parent;
if (parent == null)
return;
if (delta > 0) {
ArrayList<Node> siblings = parent.children;
int index = siblings.indexOf(node);
if (index <= 0) // Can't indent first child
return;
Node newParent = siblings.get(index - 1);
siblings.remove(index);
node.parent = newParent;
newParent.children.add(node);
setNodeIndent(node, newParent.indent + 1);
} else if (delta < 0) {
if (parent == treeRoot) // Can't deindent a top level item
return;
ArrayList<Node> siblings = parent.children;
int index = siblings.indexOf(node);
if (index < 0)
return;
Node newParent = parent.parent;
ArrayList<Node> newSiblings = newParent.children;
int insertAfter = newSiblings.indexOf(parent);
siblings.remove(index);
node.parent = newParent;
setNodeIndent(node, newParent.indent + 1);
newSiblings.add(insertAfter + 1, node);
}
writeSerialization(list, serializeTree());
applyToFilter(filter);
}
private void setNodeIndent(Node node, int indent) {
node.indent = indent;
adjustDescendantsIndent(node, indent);
}
private void adjustDescendantsIndent(Node node, int baseIndent) {
for (Node child : node.children) {
child.indent = baseIndent + 1;
adjustDescendantsIndent(child, child.indent);
}
}
public void moveTo(LIST list, Filter filter, long targetTaskId, long beforeTaskId) {
Node target = idToNode.get(targetTaskId);
if (target == null)
return;
if (beforeTaskId == -1) {
moveToEndOfList(list, filter, target);
return;
}
Node before = idToNode.get(beforeTaskId);
if (before == null)
return;
if (isDescendantOf(before, target))
return;
moveHelper(list, filter, target, before);
}
private void moveHelper(LIST list, Filter filter, Node moveThis, Node beforeThis) {
Node oldParent = moveThis.parent;
ArrayList<Node> oldSiblings = oldParent.children;
Node newParent = beforeThis.parent;
ArrayList<Node> newSiblings = newParent.children;
int beforeIndex = newSiblings.indexOf(beforeThis);
if (beforeIndex < 0)
return;
int nodeIndex = oldSiblings.indexOf(moveThis);
if (nodeIndex < 0)
return;
moveThis.parent = newParent;
setNodeIndent(moveThis, newParent.indent + 1);
oldSiblings.remove(moveThis);
if (newSiblings == oldSiblings && beforeIndex > nodeIndex) {
beforeIndex--;
}
newSiblings.add(beforeIndex, moveThis);
writeSerialization(list, serializeTree());
applyToFilter(filter);
}
// Returns true if desc is a descendant of parent
private boolean isDescendantOf(Node desc, Node parent) {
Node curr = desc;
while (curr != treeRoot) {
if (curr == parent)
return true;
curr = curr.parent;
}
return false;
}
private void moveToEndOfList(LIST list, Filter filter, Node moveThis) {
Node parent = moveThis.parent;
parent.children.remove(moveThis);
treeRoot.children.add(moveThis);
moveThis.parent = treeRoot;
moveThis.indent = 0;
writeSerialization(list, serializeTree());
applyToFilter(filter);
}
public void onCreateTask(LIST list, Filter filter, long taskId) {
if (idToNode.containsKey(taskId) || taskId < 0)
return;
Node newNode = new Node(taskId, treeRoot, 0);
treeRoot.children.add(newNode);
idToNode.put(taskId, newNode);
writeSerialization(list, serializeTree());
applyToFilter(filter);
}
public void onDeleteTask(LIST list, Filter filter, long taskId) {
Node task = idToNode.get(taskId);
if (task == null)
return;
Node parent = task.parent;
ArrayList<Node> siblings = parent.children;
int index = siblings.indexOf(task);
siblings.remove(index);
for (Node child : task.children) {
child.parent = parent;
siblings.add(index, child);
setNodeIndent(child, parent.indent + 1);
index++;
}
idToNode.remove(taskId);
writeSerialization(list, serializeTree());
applyToFilter(filter);
}
private Node buildTreeModel(String serializedTree) {
Node root = new Node(-1, null, -1);
try {
JSONArray tree = new JSONArray(serializedTree);
recursivelyBuildChildren(root, tree);
} 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 {
for (int i = 0; i < children.length(); i++) {
JSONObject childObj = children.getJSONObject(i);
JSONArray keys = childObj.names();
if (keys == null)
continue;
Long id = keys.getLong(0);
if (id <= 0)
continue;
JSONArray childsChildren = childObj.getJSONArray(Long.toString(id));
Node child = new Node(id, node, node.indent + 1);
recursivelyBuildChildren(child, childsChildren);
node.children.add(child);
idToNode.put(id, child);
}
}
protected String serializeTree() {
JSONArray tree = new JSONArray();
if (treeRoot == null) {
return tree.toString();
}
try {
recursivelySerializeChildren(treeRoot, tree);
} catch (JSONException e) {
Log.e("OrderedListUpdater", "Error serializing tree model", e); //$NON-NLS-1$//$NON-NLS-2$
}
return tree.toString();
}
public static void recursivelySerializeChildren(Node node, JSONArray serializeTo) throws JSONException {
ArrayList<Node> children = node.children;
for (Node child : children) {
JSONObject childObj = new JSONObject();
JSONArray childsChildren = new JSONArray();
recursivelySerializeChildren(child, childsChildren);
childObj.put(Long.toString(child.taskId), childsChildren);
serializeTo.put(childObj);
}
}
}

@ -0,0 +1,18 @@
package com.todoroo.astrid.subtasks;
import java.util.concurrent.atomic.AtomicReference;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.astrid.adapter.TaskAdapter;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.data.Task;
public interface OrderedListFragmentHelperInterface<T> {
void setUpUiComponents();
void beforeSetUpTaskList(Filter filter);
void onCreateTask(Task task);
void onDeleteTask(Task task);
TaskAdapter createTaskAdapter(TodorooCursor<Task> cursor, AtomicReference<String> queryTemplate);
}

@ -44,15 +44,15 @@ import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.service.ThemeService;
import com.todoroo.astrid.subtasks.OrderedListUpdater.Node;
import com.todoroo.astrid.subtasks.OrderedListUpdater.OrderedListNodeVisitor;
import com.todoroo.astrid.subtasks.OrderedMetadataListUpdater.Node;
import com.todoroo.astrid.subtasks.OrderedMetadataListUpdater.OrderedListNodeVisitor;
import com.todoroo.astrid.ui.DraggableListView;
import com.todoroo.astrid.utility.AstridPreferences;
public class OrderedListFragmentHelper<LIST> {
public class OrderedMetadataListFragmentHelper<LIST> implements OrderedListFragmentHelperInterface<LIST> {
private final DisplayMetrics metrics = new DisplayMetrics();
private final OrderedListUpdater<LIST> updater;
private final OrderedMetadataListUpdater<LIST> updater;
private final TaskListFragment fragment;
@Autowired TaskService taskService;
@ -62,7 +62,7 @@ public class OrderedListFragmentHelper<LIST> {
private LIST list;
public OrderedListFragmentHelper(TaskListFragment fragment, OrderedListUpdater<LIST> updater) {
public OrderedMetadataListFragmentHelper(TaskListFragment fragment, OrderedMetadataListUpdater<LIST> updater) {
DependencyInjectionService.getInstance().inject(this);
this.fragment = fragment;
this.updater = updater;
@ -321,6 +321,11 @@ public class OrderedListFragmentHelper<LIST> {
this.list = list;
}
@Override
public void onCreateTask(Task task) {
//
}
public void onDeleteTask(Task task) {
updater.onDeleteTask(getFilter(), list, task.getId());
taskAdapter.notifyDataSetInvalidated();

@ -18,11 +18,11 @@ import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.subtasks.OrderedListUpdater.OrderedListIterator;
import com.todoroo.astrid.subtasks.OrderedMetadataListUpdater.OrderedListIterator;
abstract public class OrderedListUpdater<LIST> {
abstract public class OrderedMetadataListUpdater<LIST> {
public OrderedListUpdater() {
public OrderedMetadataListUpdater() {
DependencyInjectionService.getInstance().inject(this);
}

@ -1,9 +1,14 @@
package com.todoroo.astrid.subtasks;
import java.util.ArrayList;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.TagViewFragment;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterWithCustomIntent;
@ -11,7 +16,6 @@ import com.todoroo.astrid.core.CoreFilterExposer;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.core.SortHelper;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.utility.AstridPreferences;
@ -48,35 +52,46 @@ public class SubtasksHelper {
public static String applySubtasksToWidgetFilter(Filter filter, String query, String tagName, int limit) {
if (SubtasksHelper.shouldUseSubtasksFragmentForFilter(filter)) {
// care for manual ordering
if(tagName == null)
tagName = SubtasksMetadata.LIST_ACTIVE_TASKS;
else {
TagData tag = PluginServices.getTagDataService().getTag(tagName, TagData.PROPERTIES);
if (tag != null)
tagName = "td:"+tag.getId();
else
tagName = SubtasksMetadata.LIST_ACTIVE_TASKS;
}
String subtaskJoin = String.format("LEFT JOIN %s ON (%s = %s AND %s = '%s' AND %s = '%s') ",
Metadata.TABLE, Task.ID, Metadata.TASK,
Metadata.KEY, SubtasksMetadata.METADATA_KEY,
SubtasksMetadata.TAG, tagName);
TagData tagData = PluginServices.getTagDataService().getTag(tagName, TagData.TAG_ORDERING);
if(!query.contains(subtaskJoin)) {
query = subtaskJoin + query;
query = query.replaceAll("ORDER BY .*", "");
query = query + String.format(" ORDER BY %s, %s, IFNULL(CAST(%s AS LONG), %s)",
Task.DELETION_DATE, Task.COMPLETION_DATE,
SubtasksMetadata.ORDER, Task.CREATION_DATE);
if (limit > 0)
query = query + " LIMIT " + limit;
query = query.replace(TaskCriteria.isVisible().toString(),
Criterion.all.toString());
query = query.replaceAll("ORDER BY .*", "");
query = query + String.format(" ORDER BY %s, %s, %s, %s",
Task.DELETION_DATE, Task.COMPLETION_DATE,
getOrderString(tagData), Task.CREATION_DATE);
if (limit > 0)
query = query + " LIMIT " + limit;
query = query.replace(TaskCriteria.isVisible().toString(),
Criterion.all.toString());
filter.setFilterQueryOverride(query);
}
filter.setFilterQueryOverride(query);
}
return query;
}
private static String getOrderString(TagData tagData) {
String serialized;
if (tagData != null)
serialized = tagData.getValue(TagData.TAG_ORDERING);
else
serialized = Preferences.getStringValue(SubtasksUpdater.ACTIVE_TASKS_ORDER);
ArrayList<Long> ids = getIdArray(serialized);
return AstridOrderedListUpdater.buildOrderString(ids.toArray(new Long[ids.size()]));
}
@SuppressWarnings("nls")
private static ArrayList<Long> getIdArray(String serializedTree) {
ArrayList<Long> ids = new ArrayList<Long>();
String[] digitsOnly = serializedTree.split("\\D+");
for (String idString : digitsOnly) {
try {
if (!TextUtils.isEmpty(idString))
ids.add(Long.parseLong(idString));
} catch (NumberFormatException e) {
Log.e("widget-subtasks", "error parsing id " + idString, e);
}
}
return ids;
}
}

@ -10,10 +10,10 @@ import android.view.View;
import android.view.ViewGroup;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.adapter.TaskAdapter;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.Task;
/**
@ -24,7 +24,7 @@ import com.todoroo.astrid.data.Task;
*/
public class SubtasksListFragment extends TaskListFragment {
protected OrderedListFragmentHelper<?> helper;
protected OrderedListFragmentHelperInterface<?> helper;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
@ -32,10 +32,10 @@ public class SubtasksListFragment extends TaskListFragment {
super.onActivityCreated(savedInstanceState);
}
protected OrderedListFragmentHelper<?> createFragmentHelper() {
OrderedListFragmentHelper<String> olfh =
new OrderedListFragmentHelper<String>(this, new SubtasksUpdater());
olfh.setList(SubtasksMetadata.LIST_ACTIVE_TASKS);
protected OrderedListFragmentHelperInterface<?> createFragmentHelper() {
AstridOrderedListFragmentHelper<TagData> olfh =
new AstridOrderedListFragmentHelper<TagData>(this, new SubtasksUpdater());
olfh.setList(getActiveTagData());
return olfh;
}
@ -52,7 +52,7 @@ public class SubtasksListFragment extends TaskListFragment {
}
@Override
protected void setUpTaskList() {
public void setUpTaskList() {
helper.beforeSetUpTaskList(filter);
super.setUpTaskList();
@ -61,14 +61,14 @@ public class SubtasksListFragment extends TaskListFragment {
}
@Override
public Property<?>[] taskProperties() {
return helper.taskProperties();
protected boolean isDraggable() {
return true;
}
@Override
protected boolean isDraggable() {
return true;
public void onTaskCreated(Task task) {
super.onTaskCreated(task);
helper.onCreateTask(task);
}
@Override

@ -16,6 +16,7 @@ import com.todoroo.astrid.data.Metadata;
* @author Tim Su <tim@todoroo.com>
*
*/
@Deprecated
public class SubtasksMetadata {
public static final String LIST_ACTIVE_TASKS = "[AT]"; //$NON-NLS-1$

@ -0,0 +1,122 @@
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;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Functions;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.TagDataService;
import com.todoroo.astrid.subtasks.AstridOrderedListUpdater.Node;
@SuppressWarnings("deprecation") // Subtasks metadata is deprecated
public class SubtasksMetadataMigration {
@Autowired
private TagDataService tagDataService;
@Autowired
private MetadataService metadataService;
public SubtasksMetadataMigration() {
DependencyInjectionService.getInstance().inject(this);
}
public void performMigration() {
TodorooCursor<Metadata> subtasksMetadata = metadataService.query(Query.select(Metadata.PROPERTIES)
.where(MetadataCriteria.withKey(SubtasksMetadata.METADATA_KEY))
.orderBy(Order.asc(SubtasksMetadata.TAG), Order.asc(Functions.cast(SubtasksMetadata.ORDER, "LONG")))); //$NON-NLS-1$
try {
Metadata m = new Metadata();
for (subtasksMetadata.moveToFirst(); !subtasksMetadata.isAfterLast(); subtasksMetadata.moveToNext()) {
m.readFromCursor(subtasksMetadata);
String tag = m.getValue(SubtasksMetadata.TAG);
processTag(tag, subtasksMetadata);
}
} finally {
subtasksMetadata.close();
}
}
@SuppressWarnings("nls")
private void processTag(String tag, TodorooCursor<Metadata> subtasksMetadata) {
Metadata item = new Metadata();
TagData td = null;
try {
if (!SubtasksMetadata.LIST_ACTIVE_TASKS.equals(tag)) {
String idString = tag.replace("td:", "");
long id = Long.parseLong(idString);
td = tagDataService.fetchById(id, TagData.ID);
}
} catch (NumberFormatException e) {
Log.e("subtasks-migration", "Could not parse tag id from " + tag, e);
}
if (td == null && !SubtasksMetadata.LIST_ACTIVE_TASKS.equals(tag)) {
for (; !subtasksMetadata.isAfterLast(); subtasksMetadata.moveToNext()) {
item.readFromCursor(subtasksMetadata);
if (!item.getValue(SubtasksMetadata.TAG).equals(tag))
break;
}
} else {
String newTree = buildTreeModelFromMetadata(tag, subtasksMetadata);
if (td != null) {
td.setValue(TagData.TAG_ORDERING, newTree);
tagDataService.save(td);
} else {
Preferences.setString(SubtasksUpdater.ACTIVE_TASKS_ORDER, newTree);
}
}
subtasksMetadata.moveToPrevious(); // Move back one to undo the last iteration of the for loop
}
private String buildTreeModelFromMetadata(String tag, TodorooCursor<Metadata> cursor) {
Metadata item = new Metadata();
Node root = new Node(-1, null, -1);
for (; !cursor.isAfterLast(); cursor.moveToNext()) {
item.clear();
item.readFromCursor(cursor);
if (!item.getValue(SubtasksMetadata.TAG).equals(tag))
break;
int indent = 0;
if (item.containsNonNullValue(SubtasksMetadata.INDENT))
indent = item.getValue(SubtasksMetadata.INDENT);
Node parent = findNextParentForIndent(root, indent);
Node newNode = new Node(item.getValue(Metadata.TASK), parent, parent.indent + 1);
parent.children.add(newNode);
}
try {
JSONArray array = new JSONArray();
AstridOrderedListUpdater.recursivelySerializeChildren(root, array);
return array.toString();
} catch (JSONException e) {
return "[]"; //$NON-NLS-1$
}
}
private Node findNextParentForIndent(Node root, int indent) {
if (indent <= 0)
return root;
ArrayList<Node> children = root.children;
if (children.size() == 0)
return root;
return findNextParentForIndent(children.get(children.size() - 1), indent - 1);
}
}

@ -9,25 +9,24 @@ import android.view.View;
import android.view.ViewGroup;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.astrid.actfm.TagViewFragment;
import com.todoroo.astrid.adapter.TaskAdapter;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.Task;
public class SubtasksTagListFragment extends TagViewFragment {
private final OrderedListFragmentHelper<String> helper;
private final AstridOrderedListFragmentHelper<TagData> helper;
public SubtasksTagListFragment() {
super();
helper = new OrderedListFragmentHelper<String>(this, new SubtasksUpdater());
helper = new AstridOrderedListFragmentHelper<TagData>(this, new SubtasksUpdater());
}
@Override
protected void postLoadTagData() {
String list = "td:" + tagData.getId(); //$NON-NLS-1$
helper.setList(list);
helper.setList(tagData);
}
@Override
@ -50,7 +49,7 @@ public class SubtasksTagListFragment extends TagViewFragment {
}
@Override
protected void setUpTaskList() {
public void setUpTaskList() {
helper.beforeSetUpTaskList(filter);
super.setUpTaskList();
@ -59,18 +58,19 @@ public class SubtasksTagListFragment extends TagViewFragment {
}
@Override
public Property<?>[] taskProperties() {
return helper.taskProperties();
protected boolean isDraggable() {
return true;
}
@Override
protected boolean isDraggable() {
return true;
public void onTaskCreated(Task task) {
super.onTaskCreated(task);
helper.onCreateTask(task);
}
@Override
protected void onTaskDelete(Task task) {
super.onTaskDelete(task);
helper.onDeleteTask(task);
}

@ -5,152 +5,66 @@
*/
package com.todoroo.astrid.subtasks;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import com.todoroo.andlib.data.Property.IntegerProperty;
import com.todoroo.andlib.data.Property.LongProperty;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.TagDataService;
import com.todoroo.astrid.service.TaskService;
public class SubtasksUpdater extends OrderedListUpdater<String> {
private static final String METADATA_ID = "mdi"; //$NON-NLS-1$
public class SubtasksUpdater extends AstridOrderedListUpdater<TagData> {
@Autowired MetadataService metadataService;
@Autowired TagDataService tagDataService;
@Autowired TaskService taskService;
@Override
protected IntegerProperty indentProperty() {
return SubtasksMetadata.INDENT;
}
public static final String ACTIVE_TASKS_ORDER = "active_tasks_order"; //$NON-NLS-1$
@Override
protected LongProperty orderProperty() {
return SubtasksMetadata.ORDER;
}
@Override
protected LongProperty parentProperty() {
return null;
public void initialize(TagData list, Filter filter) {
super.initialize(list, filter);
applyToFilter(filter);
}
@Override
protected void initialize(String list, Filter filter) {
applySubtasksToFilter(filter, list);
sanitizeTaskList(filter, list);
}
@SuppressWarnings("nls")
public void applyToFilter(Filter filter) {
String query = filter.getSqlQuery();
@Override
protected Metadata getTaskMetadata(String list, long taskId) {
TodorooCursor<Metadata> cursor = metadataService.query(Query.select(Metadata.PROPERTIES).where(
Criterion.and(
Metadata.TASK.eq(taskId),
Metadata.KEY.eq(SubtasksMetadata.METADATA_KEY),
SubtasksMetadata.TAG.eq(list))));
try {
cursor.moveToFirst();
if(cursor.isAfterLast())
return null;
return new Metadata(cursor);
} finally {
cursor.close();
}
}
query = query.replaceAll("ORDER BY .*", "");
query = query + String.format(" ORDER BY %s, %s, %s, %s",
Task.DELETION_DATE, Task.COMPLETION_DATE,
getOrderString(), Task.CREATION_DATE);
query = query.replace(TaskCriteria.isVisible().toString(),
Criterion.all.toString());
@Override
protected Metadata createEmptyMetadata(String list, long taskId) {
Metadata m = new Metadata();
m.setValue(Metadata.TASK, taskId);
m.setValue(Metadata.KEY, SubtasksMetadata.METADATA_KEY);
m.setValue(SubtasksMetadata.TAG, list);
return m;
filter.setFilterQueryOverride(query);
}
@Override
protected void iterateThroughList(Filter filter, String list, OrderedListIterator iterator) {
TodorooCursor<Task> cursor = taskService.query(Query.select(Task.ID,
Metadata.ID.as(METADATA_ID), Metadata.TASK, Metadata.KEY, SubtasksMetadata.INDENT,
SubtasksMetadata.ORDER).withQueryTemplate(filter.getSqlQuery()));
TodorooCursor<Metadata> metadataCursor = new TodorooCursor<Metadata>(cursor.getCursor(),
cursor.getProperties());
Metadata metadata = new Metadata();
try {
for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
metadata.readFromCursor(metadataCursor);
metadata.setId(cursor.getLong(cursor.getColumnIndex(METADATA_ID)));
iterator.processTask(cursor.get(Task.ID), metadata);
}
} finally {
cursor.close();
protected String getSerializedTree(TagData list, Filter filter) {
String order;
if (list == null) {
order = Preferences.getStringValue(ACTIVE_TASKS_ORDER);
} else {
order = list.getValue(TagData.TAG_ORDERING);
}
}
if (order == null || "null".equals(order)) //$NON-NLS-1$
order = "[]"; //$NON-NLS-1$
@SuppressWarnings("nls")
public void applySubtasksToFilter(Filter filter, String tagName) {
String query = filter.getSqlQuery();
if(tagName == null)
tagName = SubtasksMetadata.LIST_ACTIVE_TASKS;
String subtaskJoin = String.format("LEFT JOIN %s ON (%s = %s AND %s = '%s' AND %s = '%s') ",
Metadata.TABLE, Task.ID, Metadata.TASK,
Metadata.KEY, SubtasksMetadata.METADATA_KEY,
SubtasksMetadata.TAG, tagName);
if(!query.contains(subtaskJoin)) {
query = subtaskJoin + query;
query = query.replaceAll("ORDER BY .*", "");
query = query + String.format(" ORDER BY %s, %s, IFNULL(CAST(%s AS LONG), %s)",
Task.DELETION_DATE, Task.COMPLETION_DATE,
SubtasksMetadata.ORDER, Task.CREATION_DATE);
query = query.replace(TaskCriteria.isVisible().toString(),
Criterion.all.toString());
filter.setFilterQueryOverride(query);
}
return order;
}
public void sanitizeTaskList(Filter filter, String list) {
final AtomicInteger previousIndent = new AtomicInteger(-1);
final AtomicLong previousOrder = new AtomicLong(-1);
final HashSet<Long> taskIds = new HashSet<Long>();
iterateThroughList(filter, list, new OrderedListIterator() {
@Override
public void processTask(long taskId, Metadata metadata) {
if(!metadata.isSaved())
return;
if(taskIds.contains(taskId)) {
metadataService.delete(metadata);
return;
}
long order = metadata.getValue(SubtasksMetadata.ORDER);
if(order <= previousOrder.get()) // bad
order = previousOrder.get() + 1;
int indent = metadata.getValue(SubtasksMetadata.INDENT);
if(indent < 0 || indent > previousIndent.get() + 1) // bad
indent = Math.max(0, previousIndent.get() + 1);
metadata.setValue(SubtasksMetadata.ORDER, order);
metadata.setValue(SubtasksMetadata.INDENT, indent);
saveAndUpdateModifiedDate(metadata);
previousIndent.set(indent);
previousOrder.set(order);
taskIds.add(taskId);
}
});
@Override
protected void writeSerialization(TagData list, String serialized) {
if (list == null) {
Preferences.setString(ACTIVE_TASKS_ORDER, serialized);
} else {
list.setValue(TagData.TAG_ORDERING, serialized);
tagDataService.save(list);
}
}
}

@ -95,6 +95,8 @@ public class TaskListActivity extends AstridActivity implements MainMenuListener
public static final int FILTER_MODE_PEOPLE = 1;
public static final int FILTER_MODE_FEATURED = 2;
public static final int REQUEST_CODE_RESTART = 10;
@Autowired private ABTestEventReportingService abTestEventReportingService;
private View listsNav;

@ -895,11 +895,34 @@ public class TaskListFragment extends ListFragment implements OnScrollListener,
* @param withCustomId
* force task with given custom id to be part of list
*/
@SuppressWarnings("nls")
protected void setUpTaskList() {
public void setUpTaskList() {
if (filter == null)
return;
TodorooCursor<Task> currentCursor = constructCursor();
if (currentCursor == null)
return;
// set up list adapters
taskAdapter = createTaskAdapter(currentCursor);
setListAdapter(taskAdapter);
getListView().setOnScrollListener(this);
registerForContextMenu(getListView());
loadTaskListContent(true);
}
public Property<?>[] taskProperties() {
return TaskAdapter.PROPERTIES;
}
public Filter getFilter() {
return filter;
}
@SuppressWarnings("nls")
private TodorooCursor<Task> constructCursor() {
String tagName = null;
if (getActiveTagData() != null)
tagName = getActiveTagData().getValue(TagData.NAME);
@ -936,35 +959,24 @@ public class TaskListFragment extends ListFragment implements OnScrollListener,
groupedQuery = sqlQueryTemplate.get() + " GROUP BY " + Task.ID;
sqlQueryTemplate.set(groupedQuery);
// perform query
TodorooCursor<Task> currentCursor;
// Peform query
try {
currentCursor = taskService.fetchFiltered(
return taskService.fetchFiltered(
sqlQueryTemplate.get(), null, taskProperties());
} catch (SQLiteException e) {
// We don't show this error anymore--seems like this can get triggered
// by a strange bug, but there seems to not be any negative side effect.
// For now, we'll suppress the error
// See http://astrid.com/home#tags-7tsoi/task-1119pk
return;
return null;
}
// set up list adapters
taskAdapter = createTaskAdapter(currentCursor);
setListAdapter(taskAdapter);
getListView().setOnScrollListener(this);
registerForContextMenu(getListView());
loadTaskListContent(true);
}
public Property<?>[] taskProperties() {
return TaskAdapter.PROPERTIES;
}
public Filter getFilter() {
return filter;
public void reconstructCursor() {
TodorooCursor<Task> cursor = constructCursor();
if (cursor == null)
return;
taskAdapter.changeCursor(cursor);
}
/**
@ -1145,6 +1157,10 @@ public class TaskListFragment extends ListFragment implements OnScrollListener,
}).setNegativeButton(android.R.string.cancel, null).show();
}
public void onTaskCreated(Task task) {
incrementFilterCount();
}
protected void onTaskDelete(Task task) {
decrementFilterCount();

@ -40,7 +40,7 @@ public class Database extends AbstractDatabase {
* Database version number. This variable must be updated when database
* tables are updated, as it determines whether a database needs updating.
*/
public static final int VERSION = 26;
public static final int VERSION = 27;
/**
* Database name (must be unique)
@ -339,6 +339,12 @@ public class Database extends AbstractDatabase {
} catch (SQLiteException e) {
Log.e("astrid", "db-upgrade-" + oldVersion + "-" + newVersion, e);
}
case 26: try {
database.execSQL("ALTER TABLE " + TagData.TABLE.name + " ADD " +
TagData.TAG_ORDERING.accept(visitor, null));
} catch (SQLiteException e) {
Log.e("astrid", "db-upgrade-" + oldVersion + "-" + newVersion, e);
}
return true;
}

@ -15,6 +15,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.Property.LongProperty;
@ -29,6 +30,7 @@ import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.activity.AstridActivity;
import com.todoroo.astrid.activity.Eula;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.core.SortHelper;
@ -40,12 +42,14 @@ import com.todoroo.astrid.helper.DueDateTimeMigrator;
import com.todoroo.astrid.notes.NoteMetadata;
import com.todoroo.astrid.producteev.sync.ProducteevDataService;
import com.todoroo.astrid.service.abtesting.ABChooser;
import com.todoroo.astrid.subtasks.SubtasksMetadataMigration;
import com.todoroo.astrid.tags.TagCaseMigrator;
import com.todoroo.astrid.utility.AstridPreferences;
public final class UpgradeService {
public static final int V4_4_2 = 287;
public static final int V4_4_1 = 286;
public static final int V4_4 = 285;
public static final int V4_3_4_2 = 284;
@ -149,7 +153,7 @@ public final class UpgradeService {
* @param from
* @param to
*/
public void performUpgrade(final Context context, final int from) {
public void performUpgrade(final Activity context, final int from) {
if(from == 135)
AddOnService.recordOem();
@ -160,51 +164,76 @@ public final class UpgradeService {
Preferences.setString(R.string.p_theme, "black"); //$NON-NLS-1$
}
if( from<= V3_9_1_1) {
if(from <= V3_9_1_1) {
actFmPreferenceService.clearLastSyncDate();
}
// long running tasks: pop up a progress dialog
final ProgressDialog dialog;
if(from < V4_0_6 && context instanceof Activity)
dialog = DialogUtilities.progressDialog(context,
context.getString(R.string.DLG_upgrading));
else
dialog = null;
int maxWithUpgrade = V4_4_2; // The last version that required a migration
final String lastSetVersionName = AstridPreferences.getCurrentVersionName();
Preferences.setInt(AstridPreferences.P_UPGRADE_FROM, from);
new Thread(new Runnable() {
@Override
public void run() {
try {
// NOTE: This line should be uncommented whenever any new version requires a data migration
// TasksXmlExporter.exportTasks(context, TasksXmlExporter.ExportType.EXPORT_TYPE_ON_UPGRADE, null, null, lastSetVersionName);
if(from < V3_0_0)
new Astrid2To3UpgradeHelper().upgrade2To3(context, from);
if(from < V3_1_0)
new Astrid2To3UpgradeHelper().upgrade3To3_1(context, from);
if(from < V3_8_3_1)
new TagCaseMigrator().performTagCaseMigration(context);
if(from < V3_8_4 && Preferences.getBoolean(R.string.p_showNotes, false))
taskService.clearDetails(Task.NOTES.neq("")); //$NON-NLS-1$
if (from < V4_0_6)
new DueDateTimeMigrator().migrateDueTimes();
if(from < maxWithUpgrade) {
Intent upgrade = new Intent(context, UpgradeActivity.class);
upgrade.putExtra(UpgradeActivity.TOKEN_FROM_VERSION, from);
context.startActivityForResult(upgrade, 0);
}
}
} finally {
DialogUtilities.dismissDialog((Activity)context, dialog);
context.sendBroadcast(new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH));
}
public static class UpgradeActivity extends Activity {
@Autowired
private TaskService taskService;
private ProgressDialog dialog;
public static final String TOKEN_FROM_VERSION = "from_version"; //$NON-NLS-1$
private int from;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DependencyInjectionService.getInstance().inject(this);
from = getIntent().getIntExtra(TOKEN_FROM_VERSION, -1);
if (from > 0) {
dialog = DialogUtilities.progressDialog(this,
getString(R.string.DLG_upgrading));
new Thread() {
@Override
public void run() {
try {
if(from < V3_0_0)
new Astrid2To3UpgradeHelper().upgrade2To3(UpgradeActivity.this, from);
if(from < V3_1_0)
new Astrid2To3UpgradeHelper().upgrade3To3_1(UpgradeActivity.this, from);
if(from < V3_8_3_1)
new TagCaseMigrator().performTagCaseMigration(UpgradeActivity.this);
if(from < V3_8_4 && Preferences.getBoolean(R.string.p_showNotes, false))
taskService.clearDetails(Task.NOTES.neq("")); //$NON-NLS-1$
if (from < V4_0_6)
new DueDateTimeMigrator().migrateDueTimes();
if (from < V4_4_2)
new SubtasksMetadataMigration().performMigration();
} finally {
DialogUtilities.dismissDialog(UpgradeActivity.this, dialog);
sendBroadcast(new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH));
setResult(AstridActivity.RESULT_RESTART_ACTIVITY);
finish();
}
};
}.start();
} else {
finish();
}
}).start();
}
}
/**

@ -357,7 +357,7 @@ public class QuickAddBar extends LinearLayout {
metadataService.save(fileMetadata);
}
fragment.incrementFilterCount();
fragment.onTaskCreated(task);
StatisticsService.reportEvent(StatisticsConstants.TASK_CREATED_TASKLIST);
return task;

@ -0,0 +1,130 @@
package com.todoroo.astrid.subtasks;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.TagData;
@SuppressWarnings("deprecation")
public class SubtasksMigrationTest extends SubtasksTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
Preferences.clear(SubtasksUpdater.ACTIVE_TASKS_ORDER);
}
/* Starting basic state (see SubtasksTestCase):
*
* A
* B
* C
* D
* E
* F
*/
private void createBasicMetadata(TagData tagData) {
createSubtasksMetadata(tagData, 1, 1, 0);
createSubtasksMetadata(tagData, 2, 2, 1);
createSubtasksMetadata(tagData, 3, 3, 1);
createSubtasksMetadata(tagData, 4, 4, 2);
createSubtasksMetadata(tagData, 5, 5, 0);
createSubtasksMetadata(tagData, 6, 6, 0);
}
private void createSubtasksMetadata(TagData tagData, long taskId, long order, int indent) {
Metadata m = new Metadata();
m.setValue(Metadata.KEY, SubtasksMetadata.METADATA_KEY);
m.setValue(Metadata.TASK, taskId);
String tagString = (tagData == null ? SubtasksMetadata.LIST_ACTIVE_TASKS : "td:"+tagData.getId());
m.setValue(SubtasksMetadata.TAG, tagString);
m.setValue(SubtasksMetadata.ORDER, order);
m.setValue(SubtasksMetadata.INDENT, indent);
PluginServices.getMetadataService().save(m);
}
private void basicTest(TagData tagData) {
createBasicMetadata(tagData);
SubtasksMetadataMigration migrator = new SubtasksMetadataMigration();
migrator.performMigration();
String newSerializedTree = getSerializedTree(tagData);
String expectedSerializedTree = DEFAULT_SERIALIZED_TREE;
assertEquals(expectedSerializedTree, newSerializedTree);
}
private String getSerializedTree(TagData tagData) {
if (tagData == null)
return Preferences.getStringValue(SubtasksUpdater.ACTIVE_TASKS_ORDER).replaceAll("\\s", "");
tagData = PluginServices.getTagDataService().fetchById(tagData.getId(), TagData.ID, TagData.TAG_ORDERING);
return tagData.getValue(TagData.TAG_ORDERING).replaceAll("\\s", "");
}
public void testMigrationForActiveTasks() {
basicTest(null);
}
public void testMigrationForTagData() {
TagData td = new TagData();
td.setValue(TagData.NAME, "tag");
PluginServices.getTagDataService().save(td);
basicTest(td);
}
/* Starting advanced state
*
* For active tasks
*
* A
* B
* C
* D
* E
* F
*
* For tag data
*
* F
* E
* B
* D
* C
* A
*/
private static final String ACTIVE_TASKS_TREE = "[{\"1\":[{\"2\":[]}]}, {\"3\":[{\"4\":[{\"5\":[]}]}, {\"6\":[]}]}]".replaceAll("\\s", "");
private static final String TAG_DATA_TREE = "[{\"6\":[]}, {\"5\":[{\"2\":[]}, {\"4\":[{\"3\":[]}]}]}, {\"1\":[]}]".replaceAll("\\s", "");
private void createAdvancedMetadata(TagData tagData) {
createSubtasksMetadata(tagData, 6, 1, 0);
createSubtasksMetadata(tagData, 5, 2, 0);
createSubtasksMetadata(tagData, 2, 3, 1);
createSubtasksMetadata(tagData, 4, 4, 1);
createSubtasksMetadata(tagData, 3, 5, 2);
createSubtasksMetadata(tagData, 1, 6, 0);
createSubtasksMetadata(null, 1, 1, 0);
createSubtasksMetadata(null, 2, 2, 1);
createSubtasksMetadata(null, 3, 3, 0);
createSubtasksMetadata(null, 4, 4, 1);
createSubtasksMetadata(null, 5, 5, 2);
createSubtasksMetadata(null, 6, 6, 1);
}
public void testMigrationWithBothPresent() {
TagData td = new TagData();
td.setValue(TagData.NAME, "tag");
PluginServices.getTagDataService().save(td);
createAdvancedMetadata(td);
SubtasksMetadataMigration migrator = new SubtasksMetadataMigration();
migrator.performMigration();
assertEquals(TAG_DATA_TREE, getSerializedTree(td));
assertEquals(ACTIVE_TASKS_TREE, getSerializedTree(null));
}
}

@ -1,160 +1,102 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.subtasks;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.core.CoreFilterExposer;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.test.DatabaseTestCase;
@SuppressWarnings("nls")
public class SubtasksMovingTest extends DatabaseTestCase {
public class SubtasksMovingTest extends SubtasksTestCase {
private SubtasksUpdater updater;
private Filter filter;
private Task A, B, C, D, E, F;
private final String list = SubtasksMetadata.LIST_ACTIVE_TASKS;
/* Starting State:
*
* A
* B
* C
* D
* E
* F
*/
public void testMoveBeforeIntoSelf() {
givenTasksABCDEF();
whenTriggerMoveBefore(A, B);
/*
* A
* B
* C
* D
* E
*/
thenExpectMetadataOrderAndIndent(A, 0, 0);
thenExpectMetadataOrderAndIndent(B, 1, 1);
thenExpectMetadataOrderAndIndent(C, 2, 1);
thenExpectMetadataOrderAndIndent(D, 3, 2);
thenExpectMetadataOrderAndIndent(E, 4, 0);
@Override
protected void setUp() throws Exception {
super.setUp();
createTasks();
updater.initializeFromSerializedTree(null, filter, DEFAULT_SERIALIZED_TREE);
}
public void testMoveIntoChild() {
givenTasksABCDEF();
whenTriggerMoveBefore(A, C);
/*
* A
* B
* C
* D
* E
*/
thenExpectMetadataOrderAndIndent(A, 0, 0);
thenExpectMetadataOrderAndIndent(B, 1, 1);
thenExpectMetadataOrderAndIndent(C, 2, 1);
thenExpectMetadataOrderAndIndent(D, 3, 2);
thenExpectMetadataOrderAndIndent(E, 4, 0);
private void createTasks() {
A = createTask("A");
B = createTask("B");
C = createTask("C");
D = createTask("D");
E = createTask("E");
F = createTask("F");
}
public void testMoveEndOfChildren() {
givenTasksABCDEF();
whenTriggerMoveBefore(A, E);
/*
* A
* B
* C
* D
* E
*/
thenExpectMetadataOrderAndIndent(A, 0, 0);
thenExpectMetadataOrderAndIndent(B, 1, 1);
thenExpectMetadataOrderAndIndent(C, 2, 1);
thenExpectMetadataOrderAndIndent(D, 3, 2);
thenExpectMetadataOrderAndIndent(E, 4, 0);
private Task createTask(String title) {
Task task = new Task();
task.setValue(Task.TITLE, title);
PluginServices.getTaskService().save(task);
return task;
}
public void testMoveAfterChildren() {
givenTasksABCDEF();
private void whenTriggerMoveBefore(Task target, Task before) {
long beforeId = (before == null ? -1 : before.getId());
updater.moveTo(null, filter, target.getId(), beforeId);
}
whenTriggerMoveBefore(A, F);
/* Starting State (see SubtasksTestCase):
*
* A
* B
* C
* D
* E
* F
*/
public void testMoveBeforeIntoSelf() { // Should have no effect
whenTriggerMoveBefore(A, B);
/*
* E
* A
* B
* C
* D
*/
thenExpectMetadataOrderAndIndent(E, 0, 0);
thenExpectMetadataOrderAndIndent(A, 1, 0);
thenExpectMetadataOrderAndIndent(B, 2, 1);
thenExpectMetadataOrderAndIndent(C, 3, 1);
thenExpectMetadataOrderAndIndent(D, 4, 2);
expectParentAndPosition(A, null, 0);
expectParentAndPosition(B, A, 0);
expectParentAndPosition(C, A, 1);
expectParentAndPosition(D, C, 0);
expectParentAndPosition(E, null, 1);
expectParentAndPosition(F, null, 2);
}
// --- helpers
public void testMoveIntoDescendant() { // Should have no effect
whenTriggerMoveBefore(A, C);
/** moveTo = null => move to end */
private void whenTriggerMoveBefore(Task target, Task moveTo) {
System.err.println("CAN I GET A WITNESS?");
updater.debugPrint(filter, list);
updater.moveTo(filter, list, target.getId(), moveTo == null ? -1 : moveTo.getId());
updater.debugPrint(filter, list);
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 thenExpectMetadataOrderAndIndent(Task task, long order, int indent) {
Metadata metadata = updater.getTaskMetadata(list, task.getId());
assertNotNull("metadata was found", metadata);
assertEquals("order", order, metadata.getValue(SubtasksMetadata.ORDER).longValue());
assertEquals("indentation", indent, (int)metadata.getValue(SubtasksMetadata.INDENT));
public void testMoveToEndOfChildren() { // Should have no effect
whenTriggerMoveBefore(A, E);
expectParentAndPosition(A, null, 0);
expectParentAndPosition(B, A, 0);
expectParentAndPosition(C, A, 1);
expectParentAndPosition(D, C, 0);
expectParentAndPosition(E, null, 1);
expectParentAndPosition(F, null, 2);
}
@Override
protected void setUp() throws Exception {
super.setUp();
public void testStandardMove() {
whenTriggerMoveBefore(A, F);
updater = new SubtasksUpdater();
filter = CoreFilterExposer.buildInboxFilter(getContext().getResources());
updater.applySubtasksToFilter(filter, list);
expectParentAndPosition(A, null, 1);
expectParentAndPosition(B, A, 0);
expectParentAndPosition(C, A, 1);
expectParentAndPosition(D, C, 0);
expectParentAndPosition(E, null, 0);
expectParentAndPosition(F, null, 2);
}
private Task[] givenTasksABCDEF() {
Task[] tasks = new Task[] {
A = createTask("A", 0, 0),
B = createTask("B", 1, 1),
C = createTask("C", 2, 1),
D = createTask("D", 3, 2),
E = createTask("E", 4, 0),
F = createTask("F", 5, 0),
};
return tasks;
}
public void testMoveToEndOfList() {
whenTriggerMoveBefore(A, null);
private Task createTask(String title, long order, int indent) {
Task task = new Task();
task.setValue(Task.TITLE, title);
PluginServices.getTaskService().save(task);
Metadata metadata = updater.createEmptyMetadata(list, task.getId());
metadata.setValue(SubtasksMetadata.ORDER, order);
metadata.setValue(SubtasksMetadata.INDENT, indent);
PluginServices.getMetadataService().save(metadata);
return task;
expectParentAndPosition(A, null, 2);
expectParentAndPosition(B, A, 0);
expectParentAndPosition(C, A, 1);
expectParentAndPosition(D, C, 0);
expectParentAndPosition(E, null, 0);
expectParentAndPosition(F, null, 1);
}
}

@ -0,0 +1,48 @@
package com.todoroo.astrid.subtasks;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.core.CoreFilterExposer;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.subtasks.AstridOrderedListUpdater.Node;
import com.todoroo.astrid.test.DatabaseTestCase;
/**
* Contains useful methods common to all subtasks tests
* @author Sam
*
*/
public class SubtasksTestCase extends DatabaseTestCase {
protected SubtasksUpdater updater;
protected Filter filter;
/* Starting State:
*
* A
* B
* C
* D
* E
* F
*/
public static final String DEFAULT_SERIALIZED_TREE =
"[{\"1\":[{\"2\":[]}, {\"3\":[{\"4\":[]}]}]}, {\"5\":[]}, {\"6\":[]}]".replaceAll("\\s", "");
@Override
protected void setUp() throws Exception {
super.setUp();
filter = CoreFilterExposer.buildInboxFilter(getContext().getResources());
Preferences.clear(SubtasksUpdater.ACTIVE_TASKS_ORDER);
updater = new SubtasksUpdater();
}
protected void expectParentAndPosition(Task task, Task parent, int positionInParent) {
long parentId = (parent == null ? -1 : parent.getId());
Node n = updater.findNodeForTask(task.getId());
assertNotNull("No node found for task " + task.getValue(Task.TITLE), n);
assertEquals("Parent mismatch", parentId, n.parent.taskId);
assertEquals("Position mismatch", positionInParent, n.parent.children.indexOf(n));
}
}
Loading…
Cancel
Save