TBR: TODOs: TodoLists track numCompleted. Can mark all as done.

These changes introduce MainListener, which allows the MainActivity
to update the UI when the tasks of a TodoList have changed.

FirebaseMain also supports tracking the child tasks of the TodoLists.
This also applies when a TodoList is updated.

TodoLists can also mark all as done, though it's an open question
what should happen when all of them are already marked as done.

Bugfix: 50-50's the UI for the message and timestamp correctly.
Bugfix: notifyDataSetChanged a little more often.
Change-Id: I587cb72ff2ea946177e6643310a5402c196fa557
diff --git a/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java b/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java
index 6c3508f..0d85b88 100644
--- a/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java
+++ b/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java
@@ -20,18 +20,20 @@
         mDelegate = delegate;
     }
 
-    @Override
-    public void onChildAdded(DataSnapshot dataSnapshot, String prevKey) {
+    private T prepareKeyedData(DataSnapshot dataSnapshot) {
         T value = dataSnapshot.getValue(mType);
         value.setKey(dataSnapshot.getKey());
-        mDelegate.onItemAdd(value);
+        return value;
+    }
+
+    @Override
+    public void onChildAdded(DataSnapshot dataSnapshot, String prevKey) {
+        mDelegate.onItemAdd(prepareKeyedData(dataSnapshot));
     }
 
     @Override
     public void onChildChanged(DataSnapshot dataSnapshot, String prevKey) {
-        T value = dataSnapshot.getValue(mType);
-        value.setKey(dataSnapshot.getKey());
-        mDelegate.onItemUpdate(value);
+        mDelegate.onItemUpdate(prepareKeyedData(dataSnapshot));
     }
 
     @Override
diff --git a/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java b/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
index 67f774e..2605728 100644
--- a/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
+++ b/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
@@ -7,8 +7,16 @@
 import android.content.Context;
 
 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.Map;
+
+import io.v.todos.Task;
 import io.v.todos.TodoList;
 import io.v.todos.persistence.ListEventListener;
 import io.v.todos.persistence.MainPersistence;
@@ -19,13 +27,45 @@
     private final Firebase mTodoLists;
     private final ChildEventListener mTodoListsListener;
 
+    private final ListEventListener<TodoList> mListener;
+
+    private final Map<String, ChildEventListener> mTodoListTaskListeners;
+    private final Map<String, TodoListTasksListener> mTodoListTrackers;
+
     public FirebaseMain(Context context, final ListEventListener<TodoList> listener) {
         super(context);
 
         mTodoLists = getFirebase().child(TODO_LISTS);
 
+        // This handler will forward events to the passed in listener after ensuring that all the
+        // data in the TodoList is set and can automatically update.
         mTodoListsListener = mTodoLists.addChildEventListener(
-                new ChildEventListenerAdapter<>(TodoList.class, listener));
+                new ChildEventListenerAdapter<>(TodoList.class, new ListEventListener<TodoList>() {
+                    @Override
+                    public void onItemAdd(TodoList item) {
+                        // Hook up listeners for the # completed and # tasks. Then forward the item.
+                        startWatchTodoListTasks(item);
+                        mListener.onItemAdd(item);
+                    }
+
+                    @Override
+                    public void onItemUpdate(TodoList item) {
+                        // Retrieve # completed and # tasks. Then forward the item.
+                        setTaskCompletion(item);
+                        mListener.onItemUpdate(item);
+                    }
+
+                    @Override
+                    public void onItemDelete(String key) {
+                        // Remove listeners for the # completed and # tasks. Then forward the item.
+                        stopWatchTodoListTasks(key);
+                        mListener.onItemDelete(key);
+                    }
+                }));
+
+        mListener = listener;
+        mTodoListTaskListeners = new HashMap<>();
+        mTodoListTrackers = new HashMap<>();
     }
 
     @Override
@@ -36,10 +76,140 @@
     @Override
     public void deleteTodoList(String key) {
         mTodoLists.child(key).removeValue();
+
+        // After deleting the list itself, delete all the orphaned tasks!
+        Firebase tasksRef = getFirebase().child(FirebaseTodoList.TASKS).child(key);
+        tasksRef.removeValue();
+    }
+
+    @Override
+    public void completeAllTasks(final TodoList todoList) {
+        // Update all child tasks for this key to have done = true.
+        Firebase tasksRef = getFirebase().child(FirebaseTodoList.TASKS).child(todoList.getKey());
+        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 (Task t : mTodoListTrackers.get(todoList.getKey()).mTasks.values()) {
+                    Task tCopy = t.copy();
+                    tCopy.setDone(true);
+                    mutableData.child(t.getKey()).setValue(tCopy);
+                }
+                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.
+        mTodoLists.child(todoList.getKey()).setValue(new TodoList(todoList.getName()));
+    }
+
+    private void setTaskCompletion(TodoList todoList) {
+        TodoListTasksListener tracker = mTodoListTrackers.get(todoList.getKey());
+        tracker.swapTodoList(todoList);
+    }
+
+    private void startWatchTodoListTasks(final TodoList todoList) {
+        final String todoListKey = todoList.getKey();
+
+        Firebase taskRef = getFirebase().child(FirebaseTodoList.TASKS).child(todoListKey);
+        TodoListTasksListener tasksListener = new TodoListTasksListener(todoList);
+        ChildEventListener l = taskRef.addChildEventListener(
+                new ChildEventListenerAdapter<>(Task.class, tasksListener)
+        );
+        mTodoListTrackers.put(todoListKey, tasksListener);
+        mTodoListTaskListeners.put(todoListKey, l);
+    }
+
+    private void stopWatchTodoListTasks(String key) {
+        mTodoListTrackers.remove(key).disable(); // Disable; we don't want this listener anymore.
+        ChildEventListener l = mTodoListTaskListeners.remove(key);
+        getFirebase().removeEventListener(l);
     }
 
     @Override
     public void close() {
         getFirebase().removeEventListener(mTodoListsListener);
+        for (String key : mTodoListTaskListeners.keySet()) {
+            getFirebase().removeEventListener(mTodoListTaskListeners.get(key));
+        }
+    }
+
+    private class TodoListTasksListener implements ListEventListener<Task> {
+        TodoList mTodoList; // The list whose numCompleted and numTasks fields will be updated.
+        final Map<String, Task> mTasks;
+        boolean disabled = false;
+
+        TodoListTasksListener(TodoList todoList) {
+            mTodoList = todoList;
+            mTasks = new HashMap<>();
+        }
+
+        // Prevent this listener from propagating any more updates.
+        // Note: It looks like Firebase will continue firing listeners if they have more data, so
+        // call this if you absolutely don't need any more events to fire.
+        public void disable() {
+            disabled = true;
+        }
+
+        public void swapTodoList(TodoList otherList) {
+            if (disabled) {
+                return;
+            }
+            assert mTodoList.getKey() == otherList.getKey();
+            otherList.numCompleted = mTodoList.numCompleted;
+            otherList.numTasks = mTodoList.numTasks;
+            mTodoList = otherList;
+        }
+
+        @Override
+        public void onItemAdd(Task item) {
+            if (disabled) {
+                return;
+            }
+            mTodoList.numTasks++;
+            if (item.getDone()) {
+                mTodoList.numCompleted++;
+            }
+            mTasks.put(item.getKey(), item);
+
+            mListener.onItemUpdate(mTodoList);
+        }
+
+        @Override
+        public void onItemUpdate(Task item) {
+            if (disabled) {
+                return;
+            }
+            Task oldItem = mTasks.get(item.getKey());
+            mTasks.put(item.getKey(), item);
+
+            if (oldItem.getDone() != item.getDone()) {
+                if (item.getDone()) {
+                    mTodoList.numCompleted++;
+                } else {
+                    mTodoList.numCompleted--;
+                }
+                mListener.onItemUpdate(mTodoList);
+            }
+        }
+
+        @Override
+        public void onItemDelete(String key) {
+            if (disabled) {
+                return;
+            }
+            mTodoList.numTasks--;
+            Task t = mTasks.remove(key);
+            if (t.getDone()) {
+                mTodoList.numCompleted--;
+            }
+
+            mListener.onItemUpdate(mTodoList);
+        }
     }
 }
diff --git a/projects/todos/app/src/main/java/io/v/todos/MainActivity.java b/projects/todos/app/src/main/java/io/v/todos/MainActivity.java
index 1f7467a..708ffff 100644
--- a/projects/todos/app/src/main/java/io/v/todos/MainActivity.java
+++ b/projects/todos/app/src/main/java/io/v/todos/MainActivity.java
@@ -74,16 +74,22 @@
         RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler);
         recyclerView.setAdapter(adapter);
 
-        // TODO(alexfandrianto): Very much copy-pasted between MainActivity and TodoListActivity.
         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) {
-                    // TODO(alexfandrianto): This doesn't do anything yet. Should mark all child
-                    // Tasks as done.
-                    adapter.notifyDataSetChanged();
+                    TodoList l = snackoosList.findByKey(todoListKey);
+                    if (l != null && l.canCompleteAll()) {
+                        mPersistence.completeAllTasks(l);
+                    } else {
+                        // TODO(alexfandrianto): Can we remove this? The bug when we don't have it
+                        // here is that the swiped card doesn't return even though no data may have
+                        // been affected.
+                        adapter.notifyDataSetChanged();
+                    }
                 } else if (direction == ItemTouchHelper.LEFT) {
-                    mPersistence.deleteTodoList((String)viewHolder.itemView.getTag());
+                    mPersistence.deleteTodoList(todoListKey);
                 }
             }
         }).attachToRecyclerView(recyclerView);
@@ -92,27 +98,23 @@
             @Override
             public void onItemAdd(TodoList item) {
                 snackoosList.insertInOrder(item);
+
                 adapter.notifyDataSetChanged();
-
-                // TODO(alexfandrianto): In order to capture the computed values for this TodoList,
-                // we have to watch the Task's data.
-
                 setEmptyVisiblity();
             }
 
             @Override
             public void onItemUpdate(TodoList item) {
                 snackoosList.updateInOrder(item);
+
                 adapter.notifyDataSetChanged();
             }
 
             @Override
             public void onItemDelete(String key) {
                 snackoosList.removeByKey(key);
+
                 adapter.notifyDataSetChanged();
-
-                // TODO(alexfandrianto): Stop watching the Task data for this TodoList.
-
                 setEmptyVisiblity();
             }
         });
diff --git a/projects/todos/app/src/main/java/io/v/todos/TodoList.java b/projects/todos/app/src/main/java/io/v/todos/TodoList.java
index 31f27a1..cb3179c 100644
--- a/projects/todos/app/src/main/java/io/v/todos/TodoList.java
+++ b/projects/todos/app/src/main/java/io/v/todos/TodoList.java
@@ -41,6 +41,7 @@
     public boolean getDone() {
         return numTasks > 0 && numCompleted == numTasks;
     }
+    public boolean canCompleteAll() { return numCompleted < numTasks; }
     public void setKey(String key) {
         this.key = key;
     }
diff --git a/projects/todos/app/src/main/java/io/v/todos/TodoListActivity.java b/projects/todos/app/src/main/java/io/v/todos/TodoListActivity.java
index 0cc91f7..0789c43 100644
--- a/projects/todos/app/src/main/java/io/v/todos/TodoListActivity.java
+++ b/projects/todos/app/src/main/java/io/v/todos/TodoListActivity.java
@@ -109,6 +109,7 @@
             public void onItemUpdate(Task item) {
                 snackoosList.updateInOrder(item);
                 adapter.notifyDataSetChanged();
+                setEmptyVisiblity();
             }
 
             @Override
diff --git a/projects/todos/app/src/main/java/io/v/todos/persistence/MainPersistence.java b/projects/todos/app/src/main/java/io/v/todos/persistence/MainPersistence.java
index 1fbbef5..d59e4ac 100644
--- a/projects/todos/app/src/main/java/io/v/todos/persistence/MainPersistence.java
+++ b/projects/todos/app/src/main/java/io/v/todos/persistence/MainPersistence.java
@@ -9,4 +9,5 @@
 public interface MainPersistence extends Persistence {
     void addTodoList(TodoList todoList);
     void deleteTodoList(String key);
+    void completeAllTasks(TodoList todoList);
 }
diff --git a/projects/todos/app/src/main/res/layout/todo_list_row.xml b/projects/todos/app/src/main/res/layout/todo_list_row.xml
index 9006102..c76ce42 100644
--- a/projects/todos/app/src/main/res/layout/todo_list_row.xml
+++ b/projects/todos/app/src/main/res/layout/todo_list_row.xml
@@ -61,18 +61,18 @@
                 android:layout_weight = "1">
 
                 <TextView android:id="@+id/todo_list_completed"
-                    android:layout_width="wrap_content"
+                    android:layout_width="fill_parent"
                     android:layout_weight = "1"
-                    android:layout_height="fill_parent"
+                    android:layout_height="wrap_content"
                     android:gravity="center_vertical"
                     android:textSize="12sp"
                     android:textColor="#333333"
                     android:layout_margin="5dp" />
 
                 <TextView android:id="@+id/todo_list_time"
-                    android:layout_width="wrap_content"
+                    android:layout_width="fill_parent"
                     android:layout_weight = "1"
-                    android:layout_height="fill_parent"
+                    android:layout_height="wrap_content"
                     android:gravity="center_vertical"
                     android:textSize="12sp"
                     android:textColor="#333333"