SyncbaseTodoList persistence

Change-Id: I89e3780fda046d45781d5a0ad0f33d23db664d8c
diff --git a/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java b/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
index b2a5497..d3ebf20 100644
--- a/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
+++ b/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
@@ -29,7 +29,9 @@
 import static android.support.test.espresso.matcher.ViewMatchers.withText;
 import static org.hamcrest.Matchers.not;
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -125,7 +127,7 @@
 
         verify(mocked).addTodoList(any(ListSpec.class));
         verify(mocked, never()).deleteTodoList(anyString());
-        verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+        verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
     }
 
     // Press the fab but don't actually add the item.
@@ -141,7 +143,7 @@
 
         verify(mocked, never()).addTodoList(any(ListSpec.class));
         verify(mocked, never()).deleteTodoList(anyString());
-        verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+        verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
     }
 
     // Press the fab but don't actually add the item.
@@ -157,7 +159,7 @@
 
         verify(mocked, never()).addTodoList(any(ListSpec.class));
         verify(mocked, never()).deleteTodoList(anyString());
-        verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+        verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
     }
 
     // Add some default items so that we can interact with them with swipes.
@@ -265,7 +267,7 @@
 
         verify(mocked, never()).addTodoList(any(ListSpec.class));
         verify(mocked, never()).deleteTodoList(anyString());
-        verify(mocked).completeAllTasks(any(ListMetadata.class));
+        verify(mocked).setCompletion(any(ListMetadata.class), eq(true));
     }
 
     // Swipe a todo list item to the left to attempt to delete it.
@@ -282,7 +284,7 @@
 
         verify(mocked, never()).addTodoList(any(ListSpec.class));
         verify(mocked).deleteTodoList(anyString());
-        verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+        verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
     }
 
     // Tap a todo list item to launch its corresponding TodoListActivity
diff --git a/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java b/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
index 2df421e..a8f7f1b 100644
--- a/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
+++ b/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
@@ -84,7 +84,7 @@
     }
 
     @Override
-    public void completeAllTasks(final ListMetadata listMetadata) {
+    public void setCompletion(final ListMetadata listMetadata, final boolean done) {
         // Update all child tasks for this key to have done = true.
         Firebase tasksRef = getFirebase().child(FirebaseTodoList.TASKS).child(listMetadata.key);
         tasksRef.runTransaction(new Transaction.Handler() {
@@ -94,7 +94,7 @@
                 // this in a batch or to split up the Task into components.
                 for (MutableData taskData : mutableData.getChildren()) {
                     TaskSpec spec = taskData.getValue(TaskSpec.class);
-                    spec.setDone(true);
+                    spec.setDone(done);
                     taskData.setValue(spec);
                 }
                 return Transaction.success(mutableData);
diff --git a/app/src/main/java/io/v/todos/MainActivity.java b/app/src/main/java/io/v/todos/MainActivity.java
index 8c1e5e3..878f28f 100644
--- a/app/src/main/java/io/v/todos/MainActivity.java
+++ b/app/src/main/java/io/v/todos/MainActivity.java
@@ -33,19 +33,18 @@
  * @author alexfandrianto
  */
 public class MainActivity extends Activity {
-    private static final String TAG = "MainActivity";
-
     private MainPersistence mPersistence;
 
     // Snackoos are the code name for the list of todos.
     // These todos are backed up at the SNACKOOS child of the Firebase URL.
-    // We use the snackoosList to track a custom sorted list of the stored values.
+    // We use mMainList to track a custom sorted list of the stored values.
     static final String INTENT_SNACKOO_KEY = "snackoo key";
-    private DataList<ListMetadata> snackoosList = new DataList<>();
+    private DataList<ListMetadata> mMainList = new DataList<>();
 
     // This adapter handle mirrors the firebase list values and generates the corresponding todo
     // item View children for a list view.
-    private TodoListRecyclerAdapter adapter;
+    private TodoListRecyclerAdapter mAdapter;
+    private RecyclerView mRecyclerView;
 
     @Override
     protected void onDestroy() {
@@ -65,7 +64,7 @@
         getActionBar().setTitle(R.string.app_name);
 
         // Set up the todo list adapter
-        adapter = new TodoListRecyclerAdapter(snackoosList, new View.OnClickListener() {
+        mAdapter = new TodoListRecyclerAdapter(mMainList, new View.OnClickListener() {
             @Override
             public void onClick(View view) {
                 String fbKey = (String)view.getTag();
@@ -76,39 +75,36 @@
             }
         });
 
-        RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler);
-        recyclerView.setAdapter(adapter);
+        mRecyclerView = (RecyclerView)findViewById(R.id.recycler);
+        mRecyclerView.setAdapter(mAdapter);
 
         new ItemTouchHelper(new SwipeableTouchHelperCallback() {
             @Override
             public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int direction) {
                 String todoListKey = (String)viewHolder.itemView.getTag();
                 if (direction == ItemTouchHelper.RIGHT) {
-                    int position = snackoosList.findIndexByKey(todoListKey);
+                    int position = mMainList.findIndexByKey(todoListKey);
                     if (position == -1) {
                         return;
                     }
-                    ListMetadata l = snackoosList.get(position);
+                    ListMetadata l = mMainList.get(position);
                     if (l.canCompleteAll()) {
-                        mPersistence.completeAllTasks(l);
+                        mPersistence.setCompletion(l, true);
+                    } else if (l.numTasks > 0) {
+                        mPersistence.setCompletion(l, false);
+                    } else {
+                        mAdapter.notifyItemChanged(position);
                     }
-                    // TODO(alexfandrianto): Can we remove this? The bug when we don't have it
-                    // here is that the swiped card doesn't return from being swiped to the right
-                    // whether or not is it marking tasks as done or not.
-                    // Doing this causes a little bit of flicker when marking all tasks as done and
-                    // appears natural in the no-op case.
-                    adapter.notifyItemChanged(position);
                 } else if (direction == ItemTouchHelper.LEFT) {
                     mPersistence.deleteTodoList(todoListKey);
                 }
             }
-        }).attachToRecyclerView(recyclerView);
+        }).attachToRecyclerView(mRecyclerView);
 
         new PersistenceInitializer<MainPersistence>(this) {
             @Override
             protected MainPersistence initPersistence() throws Exception {
-                return PersistenceFactory.getMainPersistence(mActivity,
-                        createMainListener());
+                return PersistenceFactory.getMainPersistence(mActivity, createMainListener());
             }
 
             @Override
@@ -123,31 +119,49 @@
     @VisibleForTesting
     ListEventListener<ListMetadata> createMainListener() {
         return new ListEventListener<ListMetadata>() {
-            @Override
-            public void onItemAdd(ListMetadata item) {
-                int position = snackoosList.insertInOrder(item);
+                    @Override
+                    public void onItemAdd(ListMetadata item) {
+                        int position = mMainList.insertInOrder(item);
 
-                adapter.notifyItemInserted(position);
-                setEmptyVisiblity();
-            }
+                        mAdapter.notifyItemInserted(position);
+                        setEmptyVisiblity();
+                    }
 
-            @Override
-            public void onItemUpdate(ListMetadata item) {
-                int start = snackoosList.findIndexByKey(item.key);
-                int end = snackoosList.updateInOrder(item);
+                    @Override
+                    public void onItemUpdate(final ListMetadata item) {
+                        int start = mMainList.findIndexByKey(item.key);
+                        int end = mMainList.updateInOrder(item);
 
-                adapter.notifyItemMoved(start, end);
-                adapter.notifyItemChanged(end);
-            }
+                        if (start != end) {
+                            mAdapter.notifyItemMoved(start, end);
+                        }
 
-            @Override
-            public void onItemDelete(String key) {
-                int position = snackoosList.removeByKey(key);
+                        // The change animation involves a cross-fade that, if interrupted
+                        // while another for the same item is already in progress, interacts
+                        // badly with ItemTouchHelper's swipe animator. The effect would be
+                        // a flicker of the intermediate ListMetadata view, then it fading
+                        // out to the latest view but X-translated off the screen due to the
+                        // swipe animator.
+                        //
+                        // We could queue up the next change after the current one, but it's
+                        // probably better just to rebind.
+                        View view = mRecyclerView.getChildAt(end);
+                        if (view.getAlpha() < 1) {
+                            mAdapter.bindViewHolder((TodoListViewHolder) mRecyclerView
+                                    .getChildViewHolder(view), end);
+                        } else {
+                            mAdapter.notifyItemChanged(end);
+                        }
+                    }
 
-                adapter.notifyItemRemoved(position);
-                setEmptyVisiblity();
-            }
-        };
+                    @Override
+                    public void onItemDelete(String key) {
+                        int position = mMainList.removeByKey(key);
+
+                        mAdapter.notifyItemRemoved(position);
+                        setEmptyVisiblity();
+                    }
+                };
     }
 
     // Allow the tests to mock out the main persistence.
@@ -159,7 +173,7 @@
     // Set the visibility based on what the adapter thinks is the visible item count.
     private void setEmptyVisiblity() {
         View v = findViewById(R.id.empty);
-        v.setVisibility(adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
+        v.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
     }
 
     public void initiateItemAdd(View view) {
diff --git a/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java b/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java
index 58856c3..717461e 100644
--- a/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java
+++ b/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java
@@ -44,6 +44,7 @@
         }
     }
 
+    @Override
     public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
                             float dX, float dY, int actionState, boolean isCurrentlyActive) {
         SwipeableCardViewHolder holder = (SwipeableCardViewHolder)viewHolder;
@@ -52,6 +53,7 @@
                 isCurrentlyActive);
     }
 
+    @Override
     public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
                                 RecyclerView.ViewHolder viewHolder, float dX, float dY,
                                 int actionState, boolean isCurrentlyActive) {
diff --git a/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java b/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java
index 73533dc..02ab30b 100644
--- a/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java
+++ b/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java
@@ -17,16 +17,16 @@
  * @author alexfandrianto
  */
 public class TaskRecyclerAdapter extends RecyclerView.Adapter<TaskViewHolder> {
-    private ArrayList<Task> backup;
-    private View.OnClickListener itemListener;
-    private boolean showDone = true;
+    private ArrayList<Task> mBackup;
+    private View.OnClickListener mItemListener;
+    private boolean mShowDone = true;
 
     private static final int RESOURCE_ID = R.layout.task_row;
 
     public TaskRecyclerAdapter(ArrayList<Task> backup, View.OnClickListener itemListener) {
         super();
-        this.backup = backup;
-        this.itemListener = itemListener;
+        mBackup = backup;
+        mItemListener = itemListener;
     }
 
     @Override
@@ -38,26 +38,39 @@
 
     @Override
     public void onBindViewHolder(TaskViewHolder holder, int position) {
-        Task task = backup.get(position);
-        holder.bindTask(task, itemListener);
+        Task task = mBackup.get(position);
+        holder.bindTask(task, mItemListener);
     }
 
     @Override
     public int getItemCount() {
-        return showDone ? backup.size() : nonDoneSize();
+        return mShowDone ? mBackup.size() : nonDoneSize();
     }
 
     private int nonDoneSize() {
-        for (int i = 0; i < backup.size(); i++) {
-            if (backup.get(i).done) {
+        for (int i = 0; i < mBackup.size(); i++) {
+            if (mBackup.get(i).done) {
                 return i;
             }
         }
-        return backup.size();
+        return mBackup.size();
     }
 
     public void setShowDone(boolean showDone) {
-        this.showDone = showDone;
-        this.notifyDataSetChanged();
+        if (mShowDone != showDone) {
+            mShowDone = showDone;
+            int nonDoneSize = nonDoneSize(),
+                doneSize = mBackup.size() - nonDoneSize;
+
+            if (showDone) {
+                notifyItemRangeInserted(nonDoneSize, doneSize);
+            } else {
+                notifyItemRangeRemoved(nonDoneSize, doneSize);
+            }
+        }
+    }
+
+    public boolean getShowDone() {
+        return mShowDone;
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/io/v/todos/TaskViewHolder.java b/app/src/main/java/io/v/todos/TaskViewHolder.java
index 2aed003..89e8262 100644
--- a/app/src/main/java/io/v/todos/TaskViewHolder.java
+++ b/app/src/main/java/io/v/todos/TaskViewHolder.java
@@ -14,8 +14,6 @@
  * @author alexfandrianto
  */
 public class TaskViewHolder extends SwipeableCardViewHolder {
-    private boolean showDone = true;
-
     public TaskViewHolder(View itemView) {
         super(itemView);
     }
@@ -34,15 +32,9 @@
 
         itemView.setTag(task.key);
         itemView.setOnClickListener(listener);
-
-        itemView.setVisibility(!showDone && task.done ? View.GONE : View.VISIBLE);
     }
 
     private String computeCreated(Task task) {
         return UIUtil.computeTimeAgo("Created", task.addedAt);
     }
-
-    public void setShowDone(boolean showDone) {
-        this.showDone = showDone;
-    }
 }
diff --git a/app/src/main/java/io/v/todos/TodoListActivity.java b/app/src/main/java/io/v/todos/TodoListActivity.java
index ebe2e68..a5c603b 100644
--- a/app/src/main/java/io/v/todos/TodoListActivity.java
+++ b/app/src/main/java/io/v/todos/TodoListActivity.java
@@ -46,7 +46,6 @@
 
     // The menu item that toggles whether done items are shown or not.
     private MenuItem mShowDoneMenuItem;
-    private boolean mShowDone; // mirrors the checked status of mShowDoneMenuItem
 
     @Override
     protected void onDestroy() {
@@ -110,7 +109,6 @@
 
                             @Override
                             public void onUpdateShowDone(boolean showDone) {
-                                mShowDone = showDone;
                                 if (mShowDoneMenuItem != null) {
                                     // Only interact with mShowDoneMenu if it has been inflated.
                                     mShowDoneMenuItem.setChecked(showDone);
@@ -212,8 +210,8 @@
         // Also, obtain the reference to the show done menu item.
         mShowDoneMenuItem = menu.findItem(R.id.show_done);
 
-        // Since the menu item may be inflated too late, set checked to mShowDone.
-        mShowDoneMenuItem.setChecked(mShowDone);
+        // Since the menu item may be inflated too late, set checked to the adapter's value.
+        mShowDoneMenuItem.setChecked(adapter.getShowDone());
 
         return true;
     }
diff --git a/app/src/main/java/io/v/todos/TodoListRecyclerAdapter.java b/app/src/main/java/io/v/todos/TodoListRecyclerAdapter.java
index 46f6cd3..079a0c9 100644
--- a/app/src/main/java/io/v/todos/TodoListRecyclerAdapter.java
+++ b/app/src/main/java/io/v/todos/TodoListRecyclerAdapter.java
@@ -17,15 +17,15 @@
  * @author alexfandrianto
  */
 public class TodoListRecyclerAdapter extends RecyclerView.Adapter<TodoListViewHolder> {
-    private ArrayList<ListMetadata> backup;
-    private View.OnClickListener itemListener;
+    private ArrayList<ListMetadata> mBackup;
+    private View.OnClickListener mItemListener;
 
     private static final int RESOURCE_ID = R.layout.todo_list_row;
 
     public TodoListRecyclerAdapter(ArrayList<ListMetadata> backup, View.OnClickListener itemListener) {
         super();
-        this.backup = backup;
-        this.itemListener = itemListener;
+        this.mBackup = backup;
+        this.mItemListener = itemListener;
     }
 
     @Override
@@ -37,12 +37,12 @@
 
     @Override
     public void onBindViewHolder(TodoListViewHolder holder, int position) {
-        ListMetadata listMetadata = backup.get(position);
-        holder.bindTodoList(listMetadata, itemListener);
+        ListMetadata listMetadata = mBackup.get(position);
+        holder.bindTodoList(listMetadata, mItemListener);
     }
 
     @Override
     public int getItemCount() {
-        return backup.size();
+        return mBackup.size();
     }
 }
diff --git a/app/src/main/java/io/v/todos/TodoListViewHolder.java b/app/src/main/java/io/v/todos/TodoListViewHolder.java
index 5ddfd8c..0394188 100644
--- a/app/src/main/java/io/v/todos/TodoListViewHolder.java
+++ b/app/src/main/java/io/v/todos/TodoListViewHolder.java
@@ -13,19 +13,19 @@
  * @author alexfandrianto
  */
 public class TodoListViewHolder extends SwipeableCardViewHolder {
+    private final TextView mName, mCompletedStatus, mTimeAgo;
+
     public TodoListViewHolder(View itemView) {
         super(itemView);
+        mName = (TextView) itemView.findViewById(R.id.todo_list_name);
+        mCompletedStatus = (TextView) itemView.findViewById(R.id.todo_list_completed);
+        mTimeAgo = (TextView) itemView.findViewById(R.id.todo_list_time);
     }
 
     public void bindTodoList(ListMetadata listMetadata, View.OnClickListener listener) {
-        final TextView name=(TextView) itemView.findViewById(R.id.todo_list_name);
-        name.setText(listMetadata.name);
-
-        final TextView completedStatus=(TextView) itemView.findViewById(R.id.todo_list_completed);
-        completedStatus.setText(computeCompleted(listMetadata));
-
-        final TextView timeAgo=(TextView) itemView.findViewById(R.id.todo_list_time);
-        timeAgo.setText(computeTimeAgo(listMetadata));
+        mName.setText(listMetadata.name);
+        mCompletedStatus.setText(computeCompleted(listMetadata));
+        mTimeAgo.setText(computeTimeAgo(listMetadata));
 
         getCardView().setCardBackgroundColor(listMetadata.isDone() ? 0xFFCCCCCC : 0xFFFFFFFF);
 
@@ -48,4 +48,9 @@
             return listMetadata.numCompleted + " of " + listMetadata.numTasks;
         }
     }
+
+    @Override
+    public String toString() {
+        return mName.getText() + " (" + mCompletedStatus.getText() + "), " + mTimeAgo.getText();
+    }
 }
diff --git a/app/src/main/java/io/v/todos/persistence/MainPersistence.java b/app/src/main/java/io/v/todos/persistence/MainPersistence.java
index 0d3a5a3..d5ac712 100644
--- a/app/src/main/java/io/v/todos/persistence/MainPersistence.java
+++ b/app/src/main/java/io/v/todos/persistence/MainPersistence.java
@@ -10,5 +10,5 @@
 public interface MainPersistence extends Persistence {
     void addTodoList(ListSpec listSpec);
     void deleteTodoList(String key);
-    void completeAllTasks(ListMetadata listMetadata);
+    void setCompletion(ListMetadata listMetadata, boolean done);
 }
diff --git a/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java b/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
index d6fe975..5ceaba7 100644
--- a/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
+++ b/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
@@ -5,7 +5,6 @@
 package io.v.todos.persistence;
 
 import android.app.Activity;
-import android.content.Context;
 
 import io.v.todos.model.ListMetadata;
 import io.v.todos.model.ListSpec;
@@ -56,7 +55,7 @@
         public void deleteTodoList(String key) {}
 
         @Override
-        public void completeAllTasks(ListMetadata listMetadata) {}
+        public void setCompletion(ListMetadata listMetadata, boolean done) {}
 
         @Override
         public void close() {}
diff --git a/app/src/syncbase/java/io/v/todos/persistence/PersistenceFactory.java b/app/src/syncbase/java/io/v/todos/persistence/PersistenceFactory.java
index 7960ed7..2134fd4 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/PersistenceFactory.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/PersistenceFactory.java
@@ -9,6 +9,7 @@
 import io.v.impl.google.services.syncbase.SyncbaseServer;
 import io.v.todos.model.ListMetadata;
 import io.v.todos.persistence.syncbase.SyncbaseMain;
+import io.v.todos.persistence.syncbase.SyncbaseTodoList;
 import io.v.v23.verror.VException;
 
 public final class PersistenceFactory {
@@ -37,14 +38,15 @@
      * used.
      */
     public static boolean mightGetTodoListPersistenceBlock() {
-        return false;
+        return !SyncbaseTodoList.isInitialized();
     }
 
     /**
      * Instantiates a persistence object that can be used to manipulate a todo list.
      */
     public static TodoListPersistence getTodoListPersistence(
-            Activity activity, String key, TodoListListener listener) throws VException {
-        throw new RuntimeException("Unsupported product flavor.");
+            Activity activity, String key, TodoListListener listener)
+            throws VException, SyncbaseServer.StartException {
+        return new SyncbaseTodoList(activity, key, listener);
     }
 }
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/MainListTracker.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/MainListTracker.java
index 744ee81..be1f568 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/MainListTracker.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/MainListTracker.java
@@ -41,7 +41,7 @@
 
     private final Map<String, Boolean> mIsTaskCompleted = new HashMap<>();
     private int mNumCompletedTasks;
-    private boolean mFireUpdates;
+    private boolean mListExistsLocally;
 
     public final ListenableFuture<Void> watchFuture;
 
@@ -108,10 +108,10 @@
             ListMetadata listMetadata = getListMetadata();
             Log.d(TAG, listMetadata.toString());
 
-            if (mFireUpdates) {
+            if (mListExistsLocally) {
                 mListener.onItemUpdate(listMetadata);
             } else {
-                mFireUpdates = true;
+                mListExistsLocally = true;
                 mListener.onItemAdd(listMetadata);
             }
         }
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
index 14c1ddf..8d6b190 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
@@ -7,14 +7,14 @@
 import android.app.Activity;
 import android.util.Log;
 
-import com.google.common.base.Function;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.UUID;
+import java.util.concurrent.Callable;
 
 import javax.annotation.Nullable;
 
@@ -37,23 +37,14 @@
 import io.v.v23.syncbase.RowRange;
 import io.v.v23.syncbase.WatchChange;
 import io.v.v23.vdl.VdlAny;
-import io.v.v23.verror.ExistException;
 import io.v.v23.verror.VException;
 import io.v.v23.vom.VomUtil;
 
 public class SyncbaseMain extends SyncbasePersistence implements MainPersistence {
     private static final String
             TAG = SyncbaseMain.class.getSimpleName(),
-            MAIN_COLLECTION_NAME = "userdata",
             LISTS_PREFIX = "lists_";
 
-    private static final Object sMainCollectionMutex = new Object();
-    private static volatile Collection sMainCollection;
-
-    public static boolean isInitialized() {
-        return sMainCollection != null;
-    }
-
     private final Map<String, MainListTracker> mTaskTrackers = new HashMap<>();
 
     /**
@@ -63,21 +54,8 @@
             throws VException, SyncbaseServer.StartException {
         super(activity);
 
-        synchronized (sMainCollectionMutex) {
-            if (sMainCollection == null) {
-                Collection mainCollection = getDatabase()
-                        .getCollection(mVContext, MAIN_COLLECTION_NAME);
-                try {
-                    VFutures.sync(mainCollection.create(mVContext, null));
-                } catch (ExistException e) {
-                    // This is fine.
-                }
-                sMainCollection = mainCollection;
-            }
-        }
-
         InputChannel<WatchChange> watch = getDatabase().watch(
-                mVContext, sMainCollection.id(), LISTS_PREFIX);
+                mVContext, getUserCollection().id(), LISTS_PREFIX);
         trap(InputChannels.withCallback(watch, new InputChannelCallback<WatchChange>() {
             @Override
             public ListenableFuture<Void> onNext(WatchChange change) {
@@ -91,7 +69,7 @@
                     if (mTaskTrackers.put(listId, listTracker) != null) {
                         // List entries in the main collection are just ( list ID => nil ), so we
                         // never expect updates other than an initial add...
-                        Log.w(TAG, "Unexpected update to " + MAIN_COLLECTION_NAME + " collection " +
+                        Log.w(TAG, "Unexpected update to " + USER_COLLECTION_NAME + " collection " +
                                 "for list " + listId);
                     }
 
@@ -104,14 +82,14 @@
 
     @Override
     public void addTodoList(final ListSpec listSpec) {
-        final String listName = LISTS_PREFIX + UUID.randomUUID().toString().replace('-', '_');
+        final String listName = LISTS_PREFIX + randomName();
         final Collection listCollection = getDatabase().getCollection(mVContext, listName);
         Futures.addCallback(listCollection.create(mVContext, null),
                 new TrappingCallback<Void>(mActivity) {
                     @Override
                     public void onSuccess(@Nullable Void result) {
                         // These can happen in either order
-                        trap(sMainCollection.put(mVContext, listName, null, VdlAny.class));
+                        trap(getUserCollection().put(mVContext, listName, null, VdlAny.class));
                         trap(listCollection.put(mVContext, SyncbaseTodoList.LIST_ROW_NAME, listSpec,
                                 ListSpec.class));
                     }
@@ -120,42 +98,41 @@
 
     @Override
     public void deleteTodoList(String key) {
-        trap(sMainCollection.delete(mVContext, key));
+        trap(getUserCollection().delete(mVContext, key));
     }
 
     @Override
-    public void completeAllTasks(ListMetadata listMetadata) {
+    public void setCompletion(ListMetadata listMetadata, final boolean done) {
         final String listId = listMetadata.key;
         trap(Batch.runInBatch(mVContext, getDatabase(), new BatchOptions(),
                 new Batch.BatchOperation() {
                     @Override
-                    public ListenableFuture<Void> run(BatchDatabase db) {
-                        final Collection list = db.getCollection(mVContext, listId);
+                    public ListenableFuture<Void> run(final BatchDatabase db) {
+                        return sExecutor.submit(new Callable<Void>() {
+                            @Override
+                            public Void call() throws Exception {
+                                final Collection list = db.getCollection(mVContext, listId);
 
-                        InputChannel<KeyValue> scan = list.scan(mVContext,
-                                RowRange.prefix(SyncbaseTodoList.TASKS_PREFIX));
-                        InputChannel<ListenableFuture<Void>> puts = InputChannels.transform(
-                                mVContext, scan, new InputChannels.TransformFunction<KeyValue,
-                                        ListenableFuture<Void>>() {
-                                    @Override
-                                    public ListenableFuture<Void> apply(KeyValue kv)
-                                            throws VException {
-                                        TaskSpec taskSpec =
-                                                (TaskSpec) VomUtil.decode(kv.getValue());
-                                        taskSpec.setDone(true);
-                                        return list.put(mVContext, kv.getKey(), taskSpec,
-                                                TaskSpec.class);
-                                    }
-                                });
+                                InputChannel<KeyValue> scan = list.scan(mVContext,
+                                        RowRange.prefix(SyncbaseTodoList.TASKS_PREFIX));
 
-                        return Futures.transform(Futures.allAsList(InputChannels.asIterable(puts)),
-                                new Function<List<Void>, Void>() {
-                                    @Nullable
-                                    @Override
-                                    public Void apply(@Nullable List<Void> input) {
-                                        return null;
+                                List<ListenableFuture<Void>> puts = new ArrayList<>();
+                                for (KeyValue kv : InputChannels.asIterable(scan)) {
+                                    TaskSpec taskSpec = (TaskSpec) VomUtil.decode(kv.getValue());
+                                    if (taskSpec.getDone() != done) {
+                                        taskSpec.setDone(done);
+                                        puts.add(list.put(mVContext, kv.getKey(), taskSpec,
+                                                TaskSpec.class));
                                     }
-                                });
+                                }
+
+                                if (!puts.isEmpty()) {
+                                    puts.add(SyncbaseTodoList.updateListTimestamp(mVContext, list));
+                                }
+                                VFutures.sync(Futures.allAsList(puts));
+                                return null;
+                            }
+                        });
                     }
                 }));
     }
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
index 75b9342..b2b0db9 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
@@ -24,6 +24,7 @@
 import com.google.common.util.concurrent.SettableFuture;
 
 import java.io.File;
+import java.util.UUID;
 import java.util.concurrent.Executors;
 
 import javax.annotation.Nullable;
@@ -41,6 +42,7 @@
 import io.v.v23.security.access.AccessList;
 import io.v.v23.security.access.Constants;
 import io.v.v23.security.access.Permissions;
+import io.v.v23.syncbase.Collection;
 import io.v.v23.syncbase.Database;
 import io.v.v23.syncbase.Syncbase;
 import io.v.v23.syncbase.SyncbaseService;
@@ -60,6 +62,8 @@
             PROXY = "proxy",
             DATABASE = "db",
             BLESSINGS_KEY = "blessings";
+    public static final String
+            USER_COLLECTION_NAME = "userdata";
     // BlessingPattern initialization has to be deferred until after V23 init due to native binding.
     private static final Supplier<AccessList> OPEN_ACL = Suppliers.memoize(
             new Supplier<AccessList>() {
@@ -107,7 +111,7 @@
      * @throws io.v.impl.google.services.syncbase.SyncbaseServer.StartException if there was an
      * error starting the syncbase service
      */
-    private static SyncbaseService ensureSyncbaseStarted(Context androidContext)
+    private static void ensureSyncbaseStarted(Context androidContext)
             throws SyncbaseServer.StartException {
         synchronized (sSyncbaseMutex) {
             if (sSyncbase == null) {
@@ -140,13 +144,12 @@
                 sVContext = singletonContext;
             }
         }
-        return sSyncbase;
     }
 
     private static final Object sDatabaseMutex = new Object();
     private static Database sDatabase;
 
-    private static Database ensureDatabaseExists(Context androidContext) throws VException {
+    private static void ensureDatabaseExists() throws VException {
         synchronized (sDatabaseMutex) {
             if (sDatabase == null) {
                 final Database db = sSyncbase.getDatabase(sVContext, DATABASE, null);
@@ -159,7 +162,32 @@
                 sDatabase = db;
             }
         }
-        return sDatabase;
+    }
+
+    private static final Object sUserCollectionMutex = new Object();
+    private static volatile Collection sUserCollection;
+
+    private static void ensureUserCollectionExists() throws VException {
+        synchronized (sUserCollectionMutex) {
+            if (sUserCollection == null) {
+                Collection userCollection = sDatabase.getCollection(sVContext,
+                        USER_COLLECTION_NAME);
+                try {
+                    VFutures.sync(userCollection.create(sVContext, null));
+                } catch (ExistException e) {
+                    // This is fine.
+                }
+                sUserCollection = userCollection;
+            }
+        }
+    }
+
+    public static boolean isInitialized() {
+        return sUserCollection != null;
+    }
+
+    protected static String randomName() {
+        return UUID.randomUUID().toString().replace('-', '_');
     }
 
     /**
@@ -224,6 +252,10 @@
         return sDatabase;
     }
 
+    protected Collection getUserCollection() {
+        return sUserCollection;
+    }
+
     /**
      * @see TrappingCallback
      */
@@ -256,7 +288,8 @@
         }
         VFutures.sync(Futures.dereference(blessings));
         ensureSyncbaseStarted(activity);
-        ensureDatabaseExists(activity);
+        ensureDatabaseExists();
+        ensureUserCollectionExists();
     }
 
     @Override
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
index 56f56e9..49a4a14 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
@@ -4,8 +4,158 @@
 
 package io.v.todos.persistence.syncbase;
 
-public class SyncbaseTodoList {
+import android.app.Activity;
+import android.support.annotation.NonNull;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import io.v.impl.google.services.syncbase.SyncbaseServer;
+import io.v.todos.model.ListSpec;
+import io.v.todos.model.Task;
+import io.v.todos.model.TaskSpec;
+import io.v.todos.persistence.TodoListListener;
+import io.v.todos.persistence.TodoListPersistence;
+import io.v.v23.InputChannel;
+import io.v.v23.InputChannelCallback;
+import io.v.v23.InputChannels;
+import io.v.v23.context.VContext;
+import io.v.v23.syncbase.ChangeType;
+import io.v.v23.syncbase.Collection;
+import io.v.v23.syncbase.WatchChange;
+import io.v.v23.verror.NoExistException;
+import io.v.v23.verror.VException;
+
+public class SyncbaseTodoList extends SyncbasePersistence implements TodoListPersistence {
     public static final String
             LIST_ROW_NAME = "list",
             TASKS_PREFIX = "tasks_";
+
+    private static final String
+            SHOW_DONE_ROW_NAME = "ShowDone";
+
+    private final Collection mList;
+    private final TodoListListener mListener;
+    private final Set<String> mTaskIds = new HashSet<>();
+
+    /**
+     * This assumes that the collection for this list already exists.
+     */
+    public SyncbaseTodoList(Activity activity, String listId, TodoListListener listener)
+            throws VException, SyncbaseServer.StartException {
+        super(activity);
+        mListener = listener;
+
+        mList = getDatabase().getCollection(mVContext, listId);
+        InputChannel<WatchChange> listWatch = getDatabase().watch(mVContext, mList.id(), "");
+        ListenableFuture<Void> listWatchFuture = InputChannels.withCallback(listWatch,
+                new InputChannelCallback<WatchChange>() {
+                    @Override
+                    public ListenableFuture<Void> onNext(WatchChange change) {
+                        processWatchChange(change);
+                        return null;
+                    }
+                });
+        Futures.addCallback(listWatchFuture, new TrappingCallback<Void>(activity) {
+            @Override
+            public void onFailure(@NonNull Throwable t) {
+                if (t instanceof NoExistException) {
+                    // The collection has been deleted.
+                    mListener.onDelete();
+                } else {
+                    super.onFailure(t);
+                }
+            }
+        });
+
+        // Watch the "showDone" boolean in the userdata collection and forward changes to the
+        // listener.
+        InputChannel<WatchChange> showDoneWatch = getDatabase()
+                .watch(mVContext, getUserCollection().id(), SHOW_DONE_ROW_NAME);
+        trap(InputChannels.withCallback(showDoneWatch, new InputChannelCallback<WatchChange>() {
+            @Override
+            public ListenableFuture<Void> onNext(WatchChange result) {
+                mListener.onUpdateShowDone((boolean)result.getValue());
+                return null;
+            }
+        }));
+    }
+
+    private void processWatchChange(WatchChange change) {
+        String rowName = change.getRowName();
+
+        if (rowName.equals(SyncbaseTodoList.LIST_ROW_NAME)) {
+            ListSpec listSpec = SyncbasePersistence.castWatchValue(change.getValue(),
+                    ListSpec.class);
+            mListener.onUpdate(listSpec);
+        } else if (change.getChangeType() == ChangeType.DELETE_CHANGE) {
+            mTaskIds.remove(rowName);
+            mListener.onItemDelete(rowName);
+        } else {
+            TaskSpec taskSpec = SyncbasePersistence.castWatchValue(change.getValue(),
+                    TaskSpec.class);
+            Task task = new Task(rowName, taskSpec);
+
+            if (mTaskIds.add(rowName)) {
+                mListener.onItemAdd(task);
+            } else {
+                mListener.onItemUpdate(task);
+            }
+        }
+    }
+
+    @Override
+    public void updateTodoList(ListSpec listSpec) {
+        trap(mList.put(mVContext, LIST_ROW_NAME, listSpec, ListSpec.class));
+    }
+
+    @Override
+    public void deleteTodoList() {
+        trap(getUserCollection().delete(mVContext, mList.id().getName()));
+        trap(mList.destroy(mVContext));
+    }
+
+    public static ListenableFuture<Void> updateListTimestamp(final VContext vContext,
+                                                             final Collection list) {
+        ListenableFuture<Object> get = list.get(vContext, LIST_ROW_NAME, ListSpec.class);
+        return Futures.transform(get, new AsyncFunction<Object, Void>() {
+            @Override
+            public ListenableFuture<Void> apply(Object oldValue) throws Exception {
+                ListSpec listSpec = (ListSpec) oldValue;
+                listSpec.setUpdatedAt(System.currentTimeMillis());
+                return list.put(vContext, LIST_ROW_NAME, listSpec, ListSpec.class);
+            }
+        });
+    }
+
+    private void updateListTimestamp() {
+        trap(updateListTimestamp(mVContext, mList));
+    }
+
+    @Override
+    public void addTask(TaskSpec task) {
+        trap(mList.put(mVContext, TASKS_PREFIX + randomName(), task, TaskSpec.class));
+        updateListTimestamp();
+    }
+
+    @Override
+    public void updateTask(Task task) {
+        trap(mList.put(mVContext, task.key, task.toSpec(), TaskSpec.class));
+        updateListTimestamp();
+    }
+
+    @Override
+    public void deleteTask(String key) {
+        trap(mList.delete(mVContext, key));
+        updateListTimestamp();
+    }
+
+    @Override
+    public void setShowDone(boolean showDone) {
+        trap(getUserCollection().put(mVContext, SHOW_DONE_ROW_NAME, showDone, Boolean.TYPE));
+    }
 }