blob: e5d4ef80a742187172a5958839185462a3234ff0 [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.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.CallSuper;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import io.v.syncbase.Collection;
import io.v.syncbase.Database;
import io.v.syncbase.Syncbase;
import io.v.syncbase.Id;
import io.v.syncbase.Syncgroup;
import io.v.syncbase.SyncgroupInvite;
import io.v.syncbase.WatchChange;
import io.v.syncbase.exception.SyncbaseException;
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.todos.sharing.NeighborhoodFragment;
import io.v.todos.sharing.ShareListDialogFragment;
public abstract class SyncbasePersistence implements Persistence {
protected static final String SHOW_DONE_KEY = "showDoneKey";
protected static final String TODO_LIST_KEY = "todoListKey";
protected static final String TODO_LIST_COLLECTION_PREFIX = "list";
protected static final String TAG = "High-Level Syncbase";
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 static Database sDb;
private static final Object sSyncbaseMutex = new Object();
private static TodoListListener sTodoListListener;
private static Id sTodoListExpectedId;
private static ListEventListener<ListMetadata> sMainListener;
SyncbasePersistence(final Activity activity, Bundle savedInstanceState) {
Log.d(TAG, "Trying to start Syncbase Persistence...");
/**
* 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.Options opts = new Syncbase.Options();
opts.rootDir = activity.getFilesDir().getAbsolutePath();
opts.disableSyncgroupPublishing = true;
try {
Syncbase.init(opts);
} catch (SyncbaseException e) {
Log.e(TAG, "Failed to initialize", e);
return;
}
final Object initializeMutex = new Object();
Log.d(TAG, "Logging the user in!");
if (!Syncbase.isLoggedIn()) {
Syncbase.loginAndroid(activity, new Syncbase.LoginCallback() {
@Override
public void onSuccess() {
Log.d(TAG, "Successfully logged in!");
try {
sDb = Syncbase.database();
} catch (SyncbaseException e) {
Log.e(TAG, "Failed to create database", e);
callNotify();
return;
}
continueSetup();
sInitialized = true;
Log.d(TAG, "Successfully initialized!");
callNotify();
}
@Override
public void onError(Throwable e) {
Log.e(TAG, "Failed to login. :(", e);
callNotify();
}
private void callNotify() {
synchronized (initializeMutex) {
initializeMutex.notify();
}
}
});
}
Log.d(TAG, "Let's wait until we are logged in...");
synchronized (initializeMutex) {
try {
initializeMutex.wait();
} catch (InterruptedException e) {
Log.e(TAG, "could not wait for initialization to finish", e);
}
}
if (sInitialized) {
Log.d(TAG, "Syncbase Persistence initialization complete!");
} else {
Log.d(TAG, "Syncbase Persistence initialization FAILED!");
return;
}
}
}
// Prepare the share presence menu fragment.
FragmentManager mgr = activity.getFragmentManager();
if (savedInstanceState == null) {
FragmentTransaction t = mgr.beginTransaction();
addFeatureFragments(mgr, activity, t);
t.commit();
} else {
addFeatureFragments(mgr, activity, null);
}
}
/**
* Hook to insert or rebind fragments.
*
* @param manager
* @param transaction the fragment transaction to use to add fragments, or null if fragments are
* being restored by the system.
*/
@CallSuper
protected void addFeatureFragments(FragmentManager manager, Context context,
@Nullable FragmentTransaction transaction) {
if (transaction != null) {
NeighborhoodFragment fragment = new NeighborhoodFragment();
fragment.initSharePresence(context);
transaction.add(fragment, NeighborhoodFragment.FRAGMENT_TAG);
}
}
private void continueSetup() {
Log.d(TAG, "Watching everything");
// Watch everything.
// TODO(alexfandrianto): This can be simplified if we watch specific collections and the
// entrance/exit of collections. https://v.io/i/1376
sDb.addWatchChangeHandler(new Database.WatchChangeHandler() {
@Override
public void onInitialState(final Iterator<WatchChange> values) {
while (values.hasNext()) {
handlePutChange(values.next());
}
}
@Override
public void onChangeBatch(final 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) {
Log.d(TAG, "Handling put change " + value.getRowKey());
Log.d(TAG, "From collection: " + value.getCollectionId());
Log.d(TAG, "With entity type: " + value.getEntityType());
if (value.getEntityType() != WatchChange.EntityType.ROW) {
// TODO(alexfandrianto): I can't deal with non-row entities yet. Skip.
return;
}
Log.d(TAG, "With row...: " + value.getRowKey());
final Id collectionId = value.getCollectionId();
if (collectionId.getName().equals(Syncbase.USERDATA_NAME)) {
if (value.getRowKey().equals(SHOW_DONE_KEY)) {
try {
sShowDone = value.getValue(Boolean.class);
Log.d(TAG, "Got a show done" + sShowDone);
// Inform the relevant listener.
if (sTodoListListener != null) {
sTodoListListener.onUpdateShowDone(sShowDone);
}
} catch (SyncbaseException e) {
Log.e(TAG, "Failed to decode watch change as Boolean", e);
}
}
return; // Show done updated. Nothing left to do.
}
// If we are here, we must be modifying a todo list collection.
// 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)) {
try {
final ListSpec listSpec = value.getValue(ListSpec.class);
Log.d(TAG, "Got a list" + listSpec.toString());
sListSpecMap.put(collectionId, listSpec);
final ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
tracker.setSpec(listSpec);
// Inform the relevant listeners.
if (sMainListener != null) {
tracker.fireListener(sMainListener);
}
if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
sTodoListListener.onUpdate(listSpec);
}
} catch (SyncbaseException e) {
Log.e(TAG, "Failed to decode watch change value as ListSpec", e);
}
} else {
Map<String, TaskSpec> taskData = sTasksByListMap.get(collectionId);
final String rowKey = value.getRowKey();
try {
final TaskSpec newSpec = value.getValue(TaskSpec.class);
Log.d(TAG, "Got a task" + newSpec.toString());
final TaskSpec oldSpec = taskData.put(rowKey, newSpec);
final ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
tracker.adjustTask(rowKey, newSpec.getDone());
// Inform the relevant listeners.
if (sMainListener != null) {
tracker.fireListener(sMainListener);
}
if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
if (oldSpec == null) {
sTodoListListener.onItemAdd(new Task(rowKey, newSpec));
} else {
sTodoListListener.onItemUpdate(new Task(rowKey, newSpec));
}
}
} catch (SyncbaseException e) {
Log.e(TAG, "Failed to decode watch change value as TaskSpec", e);
}
}
}
// 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) {
Log.d(TAG, "Handling delete change " + value.getRowKey());
Log.d(TAG, "From collection: " + value.getCollectionId());
Log.d(TAG, "With entity type: " + value.getEntityType());
if (value.getEntityType() != WatchChange.EntityType.ROW ||
value.getCollectionId().getName().equals(Syncbase.USERDATA_NAME)) {
// TODO(alexfandrianto): I can't deal with non-row entities, and we don't need
// to watch deletes from userdata.
return;
}
Log.d(TAG, "With row...: " + value.getRowKey());
final Id collectionId = value.getCollectionId();
final 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 (sMainListener != null) {
sMainListener.onItemDelete(collectionId.encode());
}
if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
sTodoListListener.onDelete();
}
} else {
Map<String, TaskSpec> tasks = sTasksByListMap.get(collectionId);
if (tasks != null) {
tasks.remove(oldKey);
final ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
tracker.removeTask(oldKey);
// Inform the relevant listeners.
if (sMainListener != null) {
tracker.fireListener(sMainListener);
}
if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
sTodoListListener.onItemDelete(oldKey);
}
}
}
}
@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() {
@Override
public void onInvite(SyncgroupInvite invite) {
sDb.acceptSyncgroupInvite(invite, new Database.AcceptSyncgroupInviteCallback() {
@Override
public void onSuccess(Syncgroup sg) {
Log.d(TAG, "Successfully joined syncgroup: " + sg.getId().toString());
}
@Override
public void onFailure(Throwable 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());
// And do a background scan for peers near me.
ShareListDialogFragment.initScan();
}
public static boolean isInitialized() {
return sInitialized;
}
@Override
public abstract void close();
@Override
public String debugDetails() {
return null;
}
protected void setMainListener(ListEventListener<ListMetadata> listener) {
sMainListener = listener;
}
protected void removeMainListener() {
sMainListener = null;
}
protected void setTodoListListener(TodoListListener listener, Id expectedId) {
sTodoListListener = listener;
sTodoListExpectedId = expectedId;
}
protected void removeTodoListListener() {
sTodoListListener = 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() {
if (spec == null) {
return null;
}
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) {
ListMetadata metadata = computeListMetadata();
if (metadata == null) {
return; // cannot fire yet
}
if (!hasFired) {
hasFired = true;
listener.onItemAdd(computeListMetadata());
} else {
listener.onItemUpdate(computeListMetadata());
}
}
}
}