TODOs: First pass higher level API implementation

Note: Builds but can't fully run.
- Start up is successful as are restarts.
- Creating a todo list (collection creation + put) takes too long on
  the UI thread, so we cannot test further.

May need to revisit the single "watch everything" with listeners
idea.

Change-Id: I1f631ec9be0392ea8e6c820a5f07102ffa283421
diff --git a/app/build.gradle b/app/build.gradle
index 7cc822e..dea5533 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -83,5 +83,5 @@
     )
     firebaseCompile 'com.firebase:firebase-client-android:2.5.2'
     syncbaseCompile 'io.v:vanadium-android:2.1.+'
-    syncbase2Compile 'io.v:syncbase:0.1.1'
+    syncbase2Compile 'io.v:syncbase:0.1.4'
 }
diff --git a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
index 69956e3..aeeab28 100644
--- a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
+++ b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
@@ -7,6 +7,11 @@
 import android.app.Activity;
 import android.os.Bundle;
 
+import java.util.UUID;
+
+import io.v.syncbase.Collection;
+import io.v.syncbase.DatabaseHandle;
+import io.v.syncbase.Id;
 import io.v.todos.model.ListMetadata;
 import io.v.todos.model.ListSpec;
 import io.v.todos.persistence.ListEventListener;
@@ -16,15 +21,41 @@
     public SyncbaseMain(Activity activity, Bundle savedInstanceState,
                         ListEventListener<ListMetadata> listener) {
         super(activity, savedInstanceState);
+
+
+        // Fire the listener for existing list metadata.
+        for (Id listId : sListMetadataTrackerMap.keySet()) {
+            ListMetadataTracker tracker = sListMetadataTrackerMap.get(listId);
+            tracker.fireListener(listener);
+        }
+
+        // Register the listener for future updates.
+        setMainListener(listener);
     }
 
     @Override
     public String addTodoList(ListSpec listSpec) {
-        return null;
+        // TODO(alexfandrianto): We do want to create this with a syncgroup, but even if we set
+        // the flag to off, it takes too long to create (and put) on the UI thread. To work around
+        // this, we might mock the encoded Id synchronously and then do creation/put asynchronously.
+        DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions();
+        opts.withoutSyncgroup = true;
+        Collection c = mDb.collection(UUID.randomUUID().toString(), opts);
+        c.put(TODO_LIST_KEY, listSpec);
+        return c.getId().encode();
     }
 
     @Override
     public void deleteTodoList(String key) {
+        Id listId = Id.decode(key);
+        Collection c = mDb.getCollection(listId);
+        c.delete(TODO_LIST_KEY);
+        // TODO(alexfandrianto): Instead of deleting the key, should we destroy the collection?
+    }
 
+    @Override
+    public void close() {
+        removeMainListener();
+        super.close();
     }
 }
diff --git a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
index d22f28a..475054e 100644
--- a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
+++ b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
@@ -6,62 +6,300 @@
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
 
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.util.HashMap;
 import java.util.Iterator;
+import java.util.Map;
 
+import io.v.android.VAndroidContexts;
+import io.v.android.security.BlessingsManager;
+import io.v.syncbase.Collection;
 import io.v.syncbase.Database;
 import io.v.syncbase.Syncbase;
+import io.v.syncbase.Id;
 
-import io.v.syncbase.SyncgroupInvite;
 import io.v.syncbase.WatchChange;
+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.Persistence;
+import io.v.todos.persistence.TodoListListener;
+import io.v.v23.VFutures;
+import io.v.v23.context.VContext;
+import io.v.v23.security.Blessings;
+import io.v.v23.verror.VException;
 
 public class SyncbasePersistence implements Persistence {
-    protected static boolean sInitialized = false;
-    protected static Database sDb;
+    protected static final String SETTINGS_COLLECTION = "settings";
+    protected static final String SHOW_DONE_KEY = "showDoneKey";
+    protected static final String TODO_LIST_KEY = "todoListKey";
+    protected static final String TAG = "High-Level Syncbase";
 
-    public SyncbasePersistence(Activity activity, Bundle savedInstanceState) {
+    private static final String BLESSINGS_KEY = "blessings";
+
+    protected static boolean sInitialized = false;
+
+    protected static final Map<Id, ListSpec> sListSpecMap = new HashMap<>();
+    protected static final Map<Id, ListMetadataTracker> sListMetadataTrackerMap = new HashMap<>();
+    protected static final Map<Id, Map<String, TaskSpec>> sTasksByListMap = new HashMap<>();
+    protected static boolean sShowDone = true;
+
+    protected Database mDb;
+    protected Collection mSettings;
+
+    private static final Object sSyncbaseMutex = new Object();
+    private TodoListListener mTodoListListener;
+    private ListEventListener<ListMetadata> mMainListener;
+
+    public SyncbasePersistence(final Activity activity, Bundle savedInstanceState) {
         /**
          * Initializes Syncbase Server
          * Starts up a watch stream to watch all the data with methods to access/modify the data.
          * This watch stream will also allow us to "watch" who has been shared to, if we desire.
          * Starts up an invite handler to automatically accept invitations.
          */
-        Syncbase.DatabaseOptions dbOpts = new Syncbase.DatabaseOptions();
-        dbOpts.rootDir = activity.getFilesDir().getAbsolutePath();
+        synchronized (sSyncbaseMutex) {
+            if (!sInitialized) {
+                Log.d(TAG, "Initializing Syncbase Persistence...");
 
-        // Start Syncbase Server
-        // sDb = Syncbase.database(dbOpts); // TODO(alexfandrianto): This will crash though.
+                Syncbase.DatabaseOptions dbOpts = new Syncbase.DatabaseOptions();
+                dbOpts.rootDir = activity.getFilesDir().getAbsolutePath();
+                dbOpts.disableUserdataSyncgroup = true;
+                dbOpts.vContext = VAndroidContexts.withDefaults(activity,
+                        savedInstanceState).getVContext();
 
+                final VContext vContext = dbOpts.vContext;
+
+                Log.d(TAG, "Done getting vanadium context!");
+
+                final SettableFuture<ListenableFuture<Blessings>> blessings =
+                        SettableFuture.create();
+                if (activity.getMainLooper().getThread() == Thread.currentThread()) {
+                    blessings.set(BlessingsManager.getBlessings(vContext, activity,
+                            BLESSINGS_KEY, true));
+                } else {
+                    new Handler(activity.getMainLooper()).post(new Runnable() {
+                        @Override
+                        public void run() {
+                            blessings.set(BlessingsManager.getBlessings(vContext,
+                                    activity, BLESSINGS_KEY, true));
+                        }
+                    });
+                }
+
+                try {
+                    VFutures.sync(Futures.dereference(blessings));
+                } catch (VException e) {
+                    Log.e(TAG, "Failed to get blessings", e);
+                }
+
+                Log.d(TAG, "Done getting blessings!");
+
+                final Object initializeMutex = new Object();
+
+                Syncbase.database(new Syncbase.DatabaseCallback() {
+                    @Override
+                    public void onSuccess(Database db) {
+                        super.onSuccess(db);
+                        Log.d(TAG, "Got a db handle!");
+                        mDb = db;
+                        continueSetup();
+                        sInitialized = true;
+                        Log.d(TAG, "Successfully initialized!");
+                        synchronized (initializeMutex) {
+                            initializeMutex.notify();
+                        }
+                    }
+
+                    @Override
+                    public void onError(Throwable e) {
+                        super.onError(e);
+
+                        Log.e(TAG, "Failed to get database handle", e);
+                        synchronized (initializeMutex) {
+                            initializeMutex.notify();
+                        }
+                    }
+                }, dbOpts);
+
+                Log.d(TAG, "Let's wait until the database is ready...");
+                synchronized (initializeMutex) {
+                    try {
+                        initializeMutex.wait();
+                    } catch (InterruptedException e) {
+                        Log.e(TAG, "could not wait for initialization to finish", e);
+                    }
+                }
+
+                Log.d(TAG, "Syncbase Persistence initialization complete!");
+            }
+        }
+    }
+
+    private void continueSetup() {
+        Log.d(TAG, "Creating settings collection");
+        // Create a settings collection.
+        mSettings = mDb.collection(SETTINGS_COLLECTION);
+
+        Log.d(TAG, "Watching everything");
         // Watch everything.
-        sDb.addWatchChangeHandler(new Database.WatchChangeHandler() {
+        mDb.addWatchChangeHandler(new Database.WatchChangeHandler() {
             @Override
             public void onInitialState(Iterator<WatchChange> values) {
-
+                while (values.hasNext()) {
+                    handlePutChange(values.next());
+                }
             }
 
             @Override
             public void onChangeBatch(Iterator<WatchChange> changes) {
+                while (changes.hasNext()) {
+                    WatchChange change = changes.next();
+                    if (change.getChangeType() == WatchChange.ChangeType.DELETE) {
+                        handleDeleteChange(change);
+                    } else {
+                        handlePutChange(change);
+                    }
+                }
+            }
+
+            // TODO(alexfandrianto): This will fire listeners despite the WatchChange's potentially
+            // being within a batch. Over-firing the listeners isn't ideal, but the app should be
+            // okay.
+            private void handlePutChange(WatchChange value) {
+                Id collectionId = value.getCollectionId();
+
+                if (collectionId.getName().equals(SETTINGS_COLLECTION)) {
+                    if (value.getRowKey().equals(SHOW_DONE_KEY)) {
+                        sShowDone = (Boolean)value.getValue();
+
+                        // Inform the relevant listener.
+                        if (mTodoListListener != null) {
+                            mTodoListListener.onUpdateShowDone(sShowDone);
+                        }
+                    }
+                }
+
+                // Initialize the task spec map, if necessary.
+                if (sTasksByListMap.get(collectionId) == null) {
+                    sTasksByListMap.put(collectionId, new HashMap<String, TaskSpec>());
+                }
+
+                if (value.getRowKey().equals(TODO_LIST_KEY)) {
+                    ListSpec listSpec = (ListSpec) value.getValue();
+                    sListSpecMap.put(collectionId, listSpec);
+
+                    ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
+                    tracker.setSpec(listSpec);
+
+                    // Inform the relevant listeners.
+                    if (mMainListener != null) {
+                        tracker.fireListener(mMainListener);
+                    }
+                    if (mTodoListListener != null) {
+                        mTodoListListener.onUpdate(listSpec);
+                    }
+                } else {
+                    Map<String, TaskSpec> taskData = sTasksByListMap.get(collectionId);
+                    TaskSpec newSpec = (TaskSpec)value.getValue();
+                    TaskSpec oldSpec = taskData.put(value.getRowKey(), newSpec);
+
+                    ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
+                    tracker.adjustTask(value.getRowKey(), newSpec.getDone());
+
+                    // Inform the relevant listeners.
+                    if (mMainListener != null) {
+                        tracker.fireListener(mMainListener);
+                    }
+                    if (mTodoListListener != null) {
+                        if (oldSpec == null) {
+                            mTodoListListener.onItemAdd(new Task(value.getRowKey(), newSpec));
+                        } else {
+                            mTodoListListener.onItemUpdate(new Task(value.getRowKey(), newSpec));
+                        }
+                    }
+                }
+            }
+
+            // TODO(alexfandrianto): This will fire listeners despite the WatchChange's potentially
+            // being within a batch. Over-firing the listeners isn't ideal, but the app should be
+            // okay.
+            private void handleDeleteChange(WatchChange value) {
+                Id collectionId = value.getCollectionId();
+                String oldKey = value.getRowKey();
+                if (oldKey.equals(TODO_LIST_KEY)) {
+                    sListSpecMap.remove(collectionId);
+                    sListMetadataTrackerMap.remove(collectionId);
+
+                    // TODO(alexfandrianto): Potentially destroy the collection too?
+
+                    // Inform the relevant listeners.
+                    if (mMainListener != null) {
+                        mMainListener.onItemDelete(oldKey);
+                    }
+                    if (mTodoListListener != null) {
+                        mTodoListListener.onDelete();
+                    }
+                } else {
+                    Map<String, TaskSpec> tasks = sTasksByListMap.get(collectionId);
+                    if (tasks != null) {
+                        tasks.remove(oldKey);
+
+                        ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
+                        tracker.removeTask(oldKey);
+
+                        // Inform the relevant listeners.
+                        if (mMainListener != null) {
+                            tracker.fireListener(mMainListener);
+                        }
+                        if (mTodoListListener != null) {
+                            mTodoListListener.onItemDelete(value.getRowKey());
+                        }
+                    }
+                }
             }
 
             @Override
             public void onError(Throwable e) {
+                Log.w(TAG, "error during watch", e);
             }
         }, new Database.AddWatchChangeHandlerOptions());
 
+        Log.d(TAG, "Accepting all invitations");
 
         // Automatically accept invitations.
-        sDb.addSyncgroupInviteHandler(new Database.SyncgroupInviteHandler() {
+        // TODO(alexfandrianto): Uncomment. This part of the high-level API isn't implemented yet.
+        /*mDb.addSyncgroupInviteHandler(new Database.SyncgroupInviteHandler() {
             @Override
             public void onInvite(SyncgroupInvite invite) {
+                mDb.acceptSyncgroupInvite(invite, new Database.AcceptSyncgroupInviteCallback() {
+                    @Override
+                    public void onSuccess(Syncgroup sg) {
+                        super.onSuccess(sg);
+                        Log.d(TAG, "Successfully joined syncgroup: " + sg.getId().toString());
+                    }
+
+                    @Override
+                    public void onFailure(Throwable e) {
+                        super.onFailure(e);
+                        Log.w(TAG, "Failed to accept invitation", e);
+                    }
+                });
             }
 
             @Override
             public void onError(Throwable e) {
+                Log.w(TAG, "error while handling invitations", e);
             }
-        }, new Database.AddSyncgroupInviteHandlerOptions());
-
-        sInitialized = true;
+        }, new Database.AddSyncgroupInviteHandlerOptions());*/
     }
 
     public static boolean isInitialized() {
@@ -77,4 +315,73 @@
     public String debugDetails() {
         return null;
     }
+
+    protected void setMainListener(ListEventListener<ListMetadata> listener) {
+        mMainListener = listener;
+    }
+    protected void removeMainListener() {
+        mMainListener = null;
+    }
+
+    protected void setTodoListListener(TodoListListener listener) {
+        mTodoListListener = listener;
+    }
+    protected void removeTodoListListener() {
+        mTodoListListener = null;
+    }
+
+    private ListMetadataTracker getListMetadataTrackerSafe(Id listId) {
+        ListMetadataTracker tracker = sListMetadataTrackerMap.get(listId);
+        if (tracker == null) {
+            tracker = new ListMetadataTracker(listId);
+            sListMetadataTrackerMap.put(listId, tracker);
+        }
+        return tracker;
+    }
+
+    class ListMetadataTracker {
+        private final Id collectionId;
+        private ListSpec spec;
+        private int numCompleted = 0;
+        private Map<String, Boolean> taskCompletion = new HashMap<>();
+        private boolean hasFired;
+
+        ListMetadataTracker(Id collectionId) {
+            this.collectionId = collectionId;
+        }
+
+        ListMetadata computeListMetadata() {
+            return new ListMetadata(collectionId.encode(), spec, numCompleted,
+                    taskCompletion.size());
+        }
+
+        void setSpec(ListSpec newSpec) {
+            spec = newSpec;
+        }
+
+        void adjustTask(String taskKey, boolean done) {
+            Boolean oldDone = taskCompletion.put(taskKey, done);
+            if ((oldDone == null || !oldDone) && done) {
+                numCompleted++;
+            } else if (oldDone != null && oldDone && !done) {
+                numCompleted--;
+            }
+        }
+
+        void removeTask(String taskKey) {
+            Boolean oldDone = taskCompletion.remove(taskKey);
+            if (oldDone != null && oldDone) {
+                numCompleted--;
+            }
+        }
+
+        void fireListener(ListEventListener<ListMetadata> listener) {
+            if (!hasFired) {
+                listener.onItemAdd(computeListMetadata());
+            } else {
+                hasFired = true;
+                listener.onItemUpdate(computeListMetadata());
+            }
+        }
+    }
 }
diff --git a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
index b29b794..ba6aa17 100644
--- a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
+++ b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
@@ -7,6 +7,13 @@
 import android.app.Activity;
 import android.os.Bundle;
 
+import java.util.Map;
+import java.util.UUID;
+
+import io.v.syncbase.BatchDatabase;
+import io.v.syncbase.Collection;
+import io.v.syncbase.Database;
+import io.v.syncbase.Id;
 import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
 import io.v.todos.model.TaskSpec;
@@ -14,43 +21,82 @@
 import io.v.todos.persistence.TodoListPersistence;
 
 public class SyncbaseTodoList extends SyncbasePersistence implements TodoListPersistence {
+    private Collection mCollection;
+
     public SyncbaseTodoList(Activity activity, Bundle savedInstanceState, String key,
                             TodoListListener listener) {
         super(activity, savedInstanceState);
+
+        Id listId = Id.decode(key);
+        mCollection = mDb.getCollection(listId);
+
+        // Fire the listener for existing data (list, tasks, show done status).
+        ListSpec currentList = sListSpecMap.get(listId);
+        if (currentList != null) {
+            listener.onUpdate(currentList);
+        }
+        Map<String, TaskSpec> currentTasks = sTasksByListMap.get(listId);
+        if (currentTasks != null) {
+            for (String taskKey : currentTasks.keySet()) {
+                listener.onItemAdd(new Task(taskKey, currentTasks.get(taskKey)));
+            }
+        }
+        listener.onUpdateShowDone(sShowDone);
+
+        // Register the listener for future updates.
+        setTodoListListener(listener);
     }
 
     @Override
     public void updateTodoList(ListSpec listSpec) {
-
+        mCollection.put(TODO_LIST_KEY, listSpec);
     }
 
     @Override
     public void deleteTodoList() {
-
+        mCollection.delete(TODO_LIST_KEY);
     }
 
     @Override
     public void completeTodoList() {
-
+        mDb.runInBatch(new Database.BatchOperation() {
+            @Override
+            public void run(BatchDatabase bDb) {
+                Collection bCollection = bDb.getCollection(mCollection.getId());
+                Map<String, TaskSpec> curTasks = sTasksByListMap.get(mCollection.getId());
+                for (Map.Entry<String, TaskSpec> entry : curTasks.entrySet()) {
+                    String rowKey = entry.getKey();
+                    TaskSpec curSpec= entry.getValue();
+                    TaskSpec newSpec = new TaskSpec(curSpec.getText(), curSpec.getAddedAt(), true);
+                    bCollection.put(rowKey, newSpec);
+                }
+            }
+        }, new Database.BatchOptions());
     }
 
     @Override
     public void addTask(TaskSpec task) {
-
+        mCollection.put(UUID.randomUUID().toString(), task);
     }
 
     @Override
     public void updateTask(Task task) {
-
+        mCollection.put(task.key, task.toSpec());
     }
 
     @Override
     public void deleteTask(String key) {
-
+        mCollection.delete(key);
     }
 
     @Override
     public void setShowDone(boolean showDone) {
+        mSettings.put(SHOW_DONE_KEY, showDone);
+    }
 
+    @Override
+    public void close() {
+        removeTodoListListener();
+        super.close();
     }
 }