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();
}
}