TODOs: Move Edit List and Mark All Done to the overflow

Edit List is no longer an action on the toolbar, it's in overflow.
Mark All Done is no longer possible to swipe. It's now in the overflow
of the single list view. We don't have mark all as undone.

(To delete, you can just go into edit list, so I didn't expose it.)

UI tests were updated to correspond, as were the Firebase and Mock build
flavors.

Change-Id: I6cd2767bae1941549ce3b738aee53479a1d19244
diff --git a/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java b/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
index 1e8956c..dd21bf1 100644
--- a/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
+++ b/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
@@ -83,7 +83,6 @@
 
         verify(mocked).addTodoList(any(ListSpec.class));
         verify(mocked, never()).deleteTodoList(anyString());
-        verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
     }
 
     // Press the fab but don't actually add the item.
@@ -99,7 +98,6 @@
 
         verify(mocked, never()).addTodoList(any(ListSpec.class));
         verify(mocked, never()).deleteTodoList(anyString());
-        verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
     }
 
     // Press the fab but don't actually add the item.
@@ -115,7 +113,6 @@
 
         verify(mocked, never()).addTodoList(any(ListSpec.class));
         verify(mocked, never()).deleteTodoList(anyString());
-        verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
     }
 
     // Add some default items so that we can interact with them with swipes.
@@ -209,8 +206,8 @@
         assertEquals(recycler.getAdapter().getItemCount(), 1);
     }
 
-    // Swipe a todo list item to the right to mark all of its children as done.
-    public void testAttemptCompleteAllTasks() {
+    // Swipe a todo list item to the right to... have nothing happen.
+    public void testAttemptSwipeRight() {
         MainPersistence mocked = mockPersistence();
         addInitialData();
 
@@ -223,8 +220,7 @@
         pause();
 
         verify(mocked, never()).addTodoList(any(ListSpec.class));
-        verify(mocked, never()).deleteTodoList(anyString());
-        verify(mocked).setCompletion(any(ListMetadata.class), eq(true));
+        verify(mocked, never()).deleteTodoList(anyString()); // Nothing should happen.
     }
 
     // Swipe a todo list item to the left to attempt to delete it.
@@ -242,7 +238,6 @@
 
         verify(mocked, never()).addTodoList(any(ListSpec.class));
         verify(mocked).deleteTodoList(anyString());
-        verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
     }
 
     // Tap a todo list item to launch its corresponding TodoListActivity
diff --git a/app/src/androidTestMock/java/io/v/todos/TodoListActivityTest.java b/app/src/androidTestMock/java/io/v/todos/TodoListActivityTest.java
index 22be53e..c004d48 100644
--- a/app/src/androidTestMock/java/io/v/todos/TodoListActivityTest.java
+++ b/app/src/androidTestMock/java/io/v/todos/TodoListActivityTest.java
@@ -78,10 +78,12 @@
     }
 
     // Helper to verify the number of calls that occurred on the mock.
-    private void verifyMockPersistence(TodoListPersistence mocked, int updateTodoList, int
-            deleteTodoList, int addTask, int updateTask, int deleteTask, int setShowDone) {
+    private void verifyMockPersistence(TodoListPersistence mocked, int updateTodoList,
+            int deleteTodoList, int completeTodoList, int addTask, int updateTask, int deleteTask,
+            int setShowDone) {
         verify(mocked, times(updateTodoList)).updateTodoList(any(ListSpec.class));
         verify(mocked, times(deleteTodoList)).deleteTodoList();
+        verify(mocked, times(completeTodoList)).completeTodoList();
         verify(mocked, times(addTask)).addTask(any(TaskSpec.class));
         verify(mocked, times(updateTask)).updateTask(any(Task.class));
         verify(mocked, times(deleteTask)).deleteTask(anyString());
@@ -99,7 +101,7 @@
 
         assertFalse(dialog.isShowing());
 
-        verifyMockPersistence(mocked, 0, 0, 1, 0, 0, 0);
+        verifyMockPersistence(mocked, 0, 0, 0, 1, 0, 0, 0);
     }
 
     // Press the fab but don't actually add the item.
@@ -310,7 +312,7 @@
 
         pause();
 
-        verifyMockPersistence(mocked, 0, 0, 0, 1, 0, 0);
+        verifyMockPersistence(mocked, 0, 0, 0, 0, 1, 0, 0);
     }
 
     // Swipe a task item to the left to attempt to delete it.
@@ -326,7 +328,7 @@
 
         pause();
 
-        verifyMockPersistence(mocked, 0, 0, 0, 0, 1, 0);
+        verifyMockPersistence(mocked, 0, 0, 0, 0, 0, 1, 0);
     }
 
     // Tap a todo list item to enter its edit dialog. Tests dismiss, cancel, save, and delete.
@@ -377,7 +379,7 @@
         pause();
         assertFalse(dialog.isShowing());
 
-        verifyMockPersistence(mocked, 0, 0, 0, 1, 0, 0);
+        verifyMockPersistence(mocked, 0, 0, 0, 0, 1, 0, 0);
 
         // 4. DO UI INTERACTION (AND THEN DELETE!)
         onView(withId(R.id.recycler)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click
@@ -391,12 +393,7 @@
         pause();
         assertFalse(dialog.isShowing());
 
-        verifyMockPersistence(mocked, 0, 0, 0, 1, 1, 0);
-    }
-
-    private void tapMenuItem(int itemId) {
-        // This item is visible in the action bar. It has no text, so refer to it by id.
-        onView(withId(itemId)).perform(click());
+        verifyMockPersistence(mocked, 0, 0, 0, 0, 1, 1, 0);
     }
 
     private void tapMenuItemInMenu(int stringId) {
@@ -426,7 +423,7 @@
         pause();
 
         // 1. DISMISS THE DIALOG
-        tapMenuItem(R.id.action_edit);
+        tapMenuItemInMenu(R.string.action_edit);
 
         pause();
 
@@ -439,7 +436,7 @@
         verifyMockPersistence(mocked);
 
         // 2. CANCEL THE DIALOG
-        tapMenuItem(R.id.action_edit);
+        tapMenuItemInMenu(R.string.action_edit);
 
         pause();
 
@@ -452,7 +449,7 @@
         verifyMockPersistence(mocked);
 
         // 3. PRESS SAVE
-        tapMenuItem(R.id.action_edit);
+        tapMenuItemInMenu(R.string.action_edit);
 
         pause();
 
@@ -462,10 +459,10 @@
         pause();
         assertFalse(dialog.isShowing());
 
-        verifyMockPersistence(mocked, 1, 0, 0, 0, 0, 0);
+        verifyMockPersistence(mocked, 1, 0, 0, 0, 0, 0, 0);
 
         // 4. PRESS DELETE
-        tapMenuItem(R.id.action_edit);
+        tapMenuItemInMenu(R.string.action_edit);
 
         pause();
 
@@ -475,7 +472,17 @@
         pause();
         assertFalse(dialog.isShowing());
 
-        verifyMockPersistence(mocked, 1, 1, 0, 0, 0, 0);
+        verifyMockPersistence(mocked, 1, 1, 0, 0, 0, 0, 0);
+    }
+
+    public void testTapMenuMarkAllDone() {
+        TodoListPersistence mocked = mockPersistence();
+
+        tapMenuItemInMenu(R.string.action_all_done);
+
+        pause();
+
+        verifyMockPersistence(mocked, 0, 0, 1, 0, 0, 0, 0);
     }
 
     public void testTapMenuShowDone() {
@@ -485,6 +492,6 @@
 
         pause();
 
-        verifyMockPersistence(mocked, 0, 0, 0, 0, 0, 1);
+        verifyMockPersistence(mocked, 0, 0, 0, 0, 0, 0, 1);
     }
 }
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 a8f7f1b..433d43e 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
@@ -9,9 +9,6 @@
 import com.firebase.client.ChildEventListener;
 import com.firebase.client.DataSnapshot;
 import com.firebase.client.Firebase;
-import com.firebase.client.FirebaseError;
-import com.firebase.client.MutableData;
-import com.firebase.client.Transaction;
 
 import java.util.HashMap;
 import java.util.HashSet;
@@ -21,7 +18,6 @@
 import io.v.todos.model.ListMetadata;
 import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
-import io.v.todos.model.TaskSpec;
 import io.v.todos.persistence.ListEventListener;
 import io.v.todos.persistence.MainPersistence;
 
@@ -83,35 +79,6 @@
         tasksRef.removeValue();
     }
 
-    @Override
-    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() {
-            @Override
-            public Transaction.Result doTransaction(MutableData mutableData) {
-                // Note: This is very easy to make conflicts with. It may be better to avoid doing
-                // 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(done);
-                    taskData.setValue(spec);
-                }
-                return Transaction.success(mutableData);
-            }
-
-            @Override
-            public void onComplete(FirebaseError firebaseError, boolean b,
-                                   DataSnapshot dataSnapshot) {
-            }
-        });
-
-        // Further, update this todo list to set its last updated time.
-        ListSpec spec = listMetadata.toSpec();
-        spec.setUpdatedAt(System.currentTimeMillis());
-        mTodoLists.child(listMetadata.key).setValue(spec);
-    }
-
     private ListMetadata updateListSpec(String key, ListSpec updatedSpec) {
         TodoListTasksListener tracker = mTodoListTrackers.get(key);
         tracker.listSpec = updatedSpec;
diff --git a/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseTodoList.java b/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseTodoList.java
index 678158f..0e429b2 100644
--- a/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseTodoList.java
+++ b/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseTodoList.java
@@ -12,6 +12,8 @@
 import com.firebase.client.DataSnapshot;
 import com.firebase.client.Firebase;
 import com.firebase.client.FirebaseError;
+import com.firebase.client.MutableData;
+import com.firebase.client.Transaction;
 import com.firebase.client.ValueEventListener;
 
 import io.v.todos.model.ListSpec;
@@ -85,8 +87,32 @@
     }
 
     @Override
-    public void shareTodoList(Iterable<String> emails) {
-        // Not implemented.
+    public void completeTodoList() {
+        // Update all child tasks for this key to have done = true.
+        Firebase tasksRef = mTasks;
+        tasksRef.runTransaction(new Transaction.Handler() {
+            @Override
+            public Transaction.Result doTransaction(MutableData mutableData) {
+                // Note: This is very easy to make conflicts with. It may be better to avoid doing
+                // this in a batch or to split up the Task into components.
+                for (MutableData taskData : mutableData.getChildren()) {
+                    TaskSpec spec = taskData.getValue(TaskSpec.class);
+                    if (!spec.getDone()) {
+                        spec.setDone(true);
+                    }
+                    taskData.setValue(spec);
+                }
+                return Transaction.success(mutableData);
+            }
+
+            @Override
+            public void onComplete(FirebaseError firebaseError, boolean b,
+                                   DataSnapshot dataSnapshot) {
+            }
+        });
+
+        // Further, update this todo list to set its last updated time.
+        updateListTimestamp();
     }
 
     private void updateListTimestamp() {
diff --git a/app/src/main/java/io/v/todos/MainActivity.java b/app/src/main/java/io/v/todos/MainActivity.java
index 127d1dc..43c6324 100644
--- a/app/src/main/java/io/v/todos/MainActivity.java
+++ b/app/src/main/java/io/v/todos/MainActivity.java
@@ -61,24 +61,11 @@
         mRecyclerView.setAdapter(mAdapter);
         mRecyclerView.setHasFixedSize(true);
 
-        new ItemTouchHelper(new SwipeableTouchHelperCallback() {
+        new ItemTouchHelper(new SwipeableTouchHelperCallback(0, ItemTouchHelper.LEFT) {
             @Override
             public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int direction) {
                 String todoListKey = (String)viewHolder.itemView.getTag();
-                if (direction == ItemTouchHelper.RIGHT) {
-                    int position = mMainList.findIndexByKey(todoListKey);
-                    if (position == -1) {
-                        return;
-                    }
-                    ListMetadata l = mMainList.get(position);
-                    if (l.canCompleteAll()) {
-                        mPersistence.setCompletion(l, true);
-                    } else if (l.numTasks > 0) {
-                        mPersistence.setCompletion(l, false);
-                    } else {
-                        mAdapter.notifyItemChanged(position);
-                    }
-                } else if (direction == ItemTouchHelper.LEFT) {
+                if (direction == ItemTouchHelper.LEFT) {
                     mPersistence.deleteTodoList(todoListKey);
                 }
             }
diff --git a/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java b/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java
index 717461e..8ea6c05 100644
--- a/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java
+++ b/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java
@@ -13,9 +13,8 @@
  * Subclasses should only override the onSwiped method since onMove is disabled.
  */
 public abstract class SwipeableTouchHelperCallback extends ItemTouchHelper.SimpleCallback {
-    SwipeableTouchHelperCallback() {
-        super(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
-              ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
+    SwipeableTouchHelperCallback(int upDown, int leftRight) {
+        super(upDown, leftRight);
     }
 
     @Override
diff --git a/app/src/main/java/io/v/todos/TodoListActivity.java b/app/src/main/java/io/v/todos/TodoListActivity.java
index 43b99ce..7393fae 100644
--- a/app/src/main/java/io/v/todos/TodoListActivity.java
+++ b/app/src/main/java/io/v/todos/TodoListActivity.java
@@ -61,7 +61,8 @@
         recyclerView.setAdapter(mAdapter);
         recyclerView.setHasFixedSize(true);
 
-        new ItemTouchHelper(new SwipeableTouchHelperCallback() {
+        new ItemTouchHelper(new SwipeableTouchHelperCallback(0, ItemTouchHelper.LEFT |
+                ItemTouchHelper.RIGHT) {
             @Override
             public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int direction) {
                 String fbKey = (String) viewHolder.itemView.getTag();
@@ -213,6 +214,9 @@
             case R.id.action_edit:
                 initiateTodoListEdit();
                 return true;
+            case R.id.action_all_done:
+                mPersistence.completeTodoList();
+                return true;
             case R.id.action_debug:
                 sharePersistenceDebugDetails();
                 return true;
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 d5ac712..11327e2 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,4 @@
 public interface MainPersistence extends Persistence {
     void addTodoList(ListSpec listSpec);
     void deleteTodoList(String key);
-    void setCompletion(ListMetadata listMetadata, boolean done);
 }
diff --git a/app/src/main/java/io/v/todos/persistence/TodoListPersistence.java b/app/src/main/java/io/v/todos/persistence/TodoListPersistence.java
index 89b8d10..15f49b4 100644
--- a/app/src/main/java/io/v/todos/persistence/TodoListPersistence.java
+++ b/app/src/main/java/io/v/todos/persistence/TodoListPersistence.java
@@ -11,7 +11,7 @@
 public interface TodoListPersistence extends Persistence {
     void updateTodoList(ListSpec listSpec);
     void deleteTodoList();
-    void shareTodoList(Iterable<String> emails);
+    void completeTodoList();
     void addTask(TaskSpec task);
     void updateTask(Task task);
     void deleteTask(String key);
diff --git a/app/src/main/res/menu/menu_task.xml b/app/src/main/res/menu/menu_task.xml
index 03c1083..315cae7 100644
--- a/app/src/main/res/menu/menu_task.xml
+++ b/app/src/main/res/menu/menu_task.xml
@@ -3,20 +3,24 @@
     xmlns:tools="http://schemas.android.com/tools"
     tools:context="io.v.todos.TodoListActivity">
     <item
-        android:orderInCategory="102"
-        android:id="@+id/action_edit"
-        android:title="@string/action_edit"
-        android:icon="@drawable/ic_create_white_24dp"
-        android:showAsAction="always" />
-    <item
         android:id="@+id/show_done"
         android:orderInCategory="104"
         android:checkable="true"
         android:title="@string/show_done"
         android:showAsAction="never"/>
     <item
-        android:id="@+id/action_debug"
         android:orderInCategory="105"
+        android:id="@+id/action_edit"
+        android:title="@string/action_edit"
+        android:showAsAction="never" />
+    <item
+        android:orderInCategory="106"
+        android:id="@+id/action_all_done"
+        android:title="@string/action_all_done"
+        android:showAsAction="never" />
+    <item
+        android:id="@+id/action_debug"
+        android:orderInCategory="107"
         android:title="@string/action_debug"
         android:showAsAction="never" />
 </menu>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3d878aa..9eee6fe 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2,6 +2,7 @@
     <string name="action_debug">Debug DB</string>
     <string name="action_edit">Edit List</string>
     <string name="action_share">Share List</string>
+    <string name="action_all_done">Mark All Done</string>
     <string name="show_done">Show Done</string>
     <string name="set_button">Set</string>
     <string name="add_button">Add</string>
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 2619945..253b2d1 100644
--- a/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
+++ b/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
@@ -59,10 +59,6 @@
         }
 
         @Override
-        public void setCompletion(ListMetadata listMetadata, boolean done) {
-        }
-
-        @Override
         public void close() {
         }
 
@@ -82,7 +78,7 @@
         }
 
         @Override
-        public void shareTodoList(Iterable<String> emails) {
+        public void completeTodoList() {
         }
 
         @Override
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 04958b0..aeb60c1 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
@@ -14,9 +14,7 @@
 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.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -27,26 +25,17 @@
 import io.v.impl.google.services.syncbase.SyncbaseServer;
 import io.v.todos.model.ListMetadata;
 import io.v.todos.model.ListSpec;
-import io.v.todos.model.TaskSpec;
 import io.v.todos.persistence.ListEventListener;
 import io.v.todos.persistence.MainPersistence;
-import io.v.v23.InputChannel;
 import io.v.v23.InputChannelCallback;
-import io.v.v23.InputChannels;
-import io.v.v23.VFutures;
 import io.v.v23.security.access.Permissions;
-import io.v.v23.services.syncbase.BatchOptions;
 import io.v.v23.services.syncbase.CollectionRow;
 import io.v.v23.services.syncbase.Id;
-import io.v.v23.services.syncbase.KeyValue;
 import io.v.v23.services.syncbase.SyncgroupJoinFailedException;
 import io.v.v23.services.syncbase.SyncgroupMemberInfo;
 import io.v.v23.services.syncbase.SyncgroupSpec;
-import io.v.v23.syncbase.Batch;
-import io.v.v23.syncbase.BatchDatabase;
 import io.v.v23.syncbase.ChangeType;
 import io.v.v23.syncbase.Collection;
-import io.v.v23.syncbase.RowRange;
 import io.v.v23.syncbase.WatchChange;
 import io.v.v23.verror.NoExistException;
 import io.v.v23.verror.VException;
@@ -218,43 +207,4 @@
             trap(tracker.collection.destroy(getVContext()));
         }
     }
-
-    @Override
-    public void setCompletion(ListMetadata listMetadata, final boolean done) {
-        final String listId = listMetadata.key;
-        trap(Batch.runInBatch(getVContext(), getDatabase(), new BatchOptions(),
-                new Batch.BatchOperation() {
-                    @Override
-                    public ListenableFuture<Void> run(final BatchDatabase db) {
-                        return sExecutor.submit(new Callable<Void>() {
-                            @Override
-                            public Void call() throws Exception {
-                                final Collection list = db.getCollection(getVContext(), listId);
-
-                                InputChannel<KeyValue> scan = list.scan(getVContext(),
-                                        RowRange.prefix(SyncbaseTodoList.TASKS_PREFIX));
-
-                                List<ListenableFuture<Void>> puts = new ArrayList<>();
-
-                                for (KeyValue kv : InputChannels.asIterable(scan)) {
-                                    TaskSpec taskSpec = castFromSyncbase(kv.getValue().getElem(),
-                                            TaskSpec.class);
-                                    if (taskSpec.getDone() != done) {
-                                        taskSpec.setDone(done);
-                                        puts.add(list.put(getVContext(), kv.getKey(), taskSpec,
-                                                TaskSpec.class));
-                                    }
-                                }
-
-                                if (!puts.isEmpty()) {
-                                    puts.add(SyncbaseTodoList.updateListTimestamp(
-                                            getVContext(), list));
-                                }
-                                VFutures.sync(Futures.allAsList(puts));
-                                return null;
-                            }
-                        });
-                    }
-                }));
-    }
 }
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 c4639fb..b5a9624 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
@@ -35,15 +35,19 @@
 import io.v.v23.InputChannelCallback;
 import io.v.v23.InputChannels;
 import io.v.v23.VFutures;
-import io.v.v23.context.VContext;
 import io.v.v23.security.BlessingPattern;
 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.services.syncbase.BatchOptions;
 import io.v.v23.services.syncbase.Id;
+import io.v.v23.services.syncbase.KeyValue;
 import io.v.v23.services.syncbase.SyncgroupSpec;
+import io.v.v23.syncbase.Batch;
+import io.v.v23.syncbase.BatchDatabase;
 import io.v.v23.syncbase.ChangeType;
 import io.v.v23.syncbase.Collection;
+import io.v.v23.syncbase.RowRange;
 import io.v.v23.syncbase.Syncgroup;
 import io.v.v23.syncbase.WatchChange;
 import io.v.v23.verror.NoExistException;
@@ -198,7 +202,6 @@
                 computeListSyncgroupName(mList.id().getName())));
     }
 
-    @Override
     public void shareTodoList(final Iterable<String> emails) {
         // Get the syncgroup
         final Syncgroup sgHandle = getListSyncgroup();
@@ -238,6 +241,41 @@
         }));
     }
 
+    @Override
+    public void completeTodoList() {
+        trap(Batch.runInBatch(getVContext(), getDatabase(), new BatchOptions(),
+                new Batch.BatchOperation() {
+                    @Override
+                    public ListenableFuture<Void> run(final BatchDatabase db) {
+                        return sExecutor.submit(new Callable<Void>() {
+                            @Override
+                            public Void call() throws Exception {
+                                InputChannel<KeyValue> scan = mList.scan(getVContext(),
+                                        RowRange.prefix(SyncbaseTodoList.TASKS_PREFIX));
+
+                                List<ListenableFuture<Void>> puts = new ArrayList<>();
+
+                                for (KeyValue kv : InputChannels.asIterable(scan)) {
+                                    TaskSpec taskSpec = castFromSyncbase(kv.getValue().getElem(),
+                                            TaskSpec.class);
+                                    if (!taskSpec.getDone()) {
+                                        taskSpec.setDone(true);
+                                        puts.add(mList.put(getVContext(), kv.getKey(), taskSpec,
+                                                TaskSpec.class));
+                                    }
+                                }
+
+                                if (!puts.isEmpty()) {
+                                    puts.add(updateListTimestamp());
+                                }
+                                VFutures.sync(Futures.allAsList(puts));
+                                return null;
+                            }
+                        });
+                    }
+                }));
+    }
+
     // TODO(alexfandrianto): We should consider moving this helper into the main Java repo.
     // https://github.com/vanadium/issues/issues/1321
     // TODO(alexfandrianto): This allows you to repeatedly add the same blessings to the permission
@@ -251,40 +289,36 @@
         perms.put(tag, acl);
     }
 
-    public static ListenableFuture<Void> updateListTimestamp(final VContext vContext,
-                                                             final Collection list) {
-        ListenableFuture<Object> get = list.get(vContext, LIST_METADATA_ROW_NAME, ListSpec.class);
+    public ListenableFuture<Void> updateListTimestamp() {
+        ListenableFuture<Object> get = mList.get(getVContext(), LIST_METADATA_ROW_NAME,
+                ListSpec.class);
         return Futures.transformAsync(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_METADATA_ROW_NAME, listSpec, ListSpec.class);
+                return mList.put(getVContext(), LIST_METADATA_ROW_NAME, listSpec, ListSpec.class);
             }
         });
     }
 
-    private void updateListTimestamp() {
-        trap(updateListTimestamp(getVContext(), mList));
-    }
-
     @Override
     public void addTask(TaskSpec task) {
         trap(mList.put(getVContext(), TASKS_PREFIX + mIdGenerator.generateTailId(), task,
                 TaskSpec.class));
-        updateListTimestamp();
+        trap(updateListTimestamp());
     }
 
     @Override
     public void updateTask(Task task) {
         trap(mList.put(getVContext(), task.key, task.toSpec(), TaskSpec.class));
-        updateListTimestamp();
+        trap(updateListTimestamp());
     }
 
     @Override
     public void deleteTask(String key) {
         trap(mList.delete(getVContext(), key));
-        updateListTimestamp();
+        trap(updateListTimestamp());
     }
 
     @Override