blob: 475054e1c5b9482a49a14add4116892f6ac53089 [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.todos.persistence.syncbase;
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.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 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";
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.
*/
synchronized (sSyncbaseMutex) {
if (!sInitialized) {
Log.d(TAG, "Initializing Syncbase Persistence...");
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.
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.
// 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());*/
}
public static boolean isInitialized() {
return sInitialized;
}
@Override
public void close() {
}
@Override
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());
}
}
}
}