blob: f735fc438c644b35d0b6b33154d66aaed6170f0e [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.support.annotation.NonNull;
import android.util.Log;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import io.v.impl.google.services.syncbase.SyncbaseServer;
import io.v.todos.model.ListSpec;
import io.v.todos.model.Task;
import io.v.todos.model.TaskSpec;
import io.v.todos.persistence.TodoListListener;
import io.v.todos.persistence.TodoListPersistence;
import io.v.v23.InputChannel;
import io.v.v23.InputChannelCallback;
import io.v.v23.InputChannels;
import io.v.v23.VFutures;
import io.v.v23.context.VContext;
import io.v.v23.security.BlessingPattern;
import io.v.v23.security.access.AccessList;
import io.v.v23.security.access.Constants;
import io.v.v23.security.access.Permissions;
import io.v.v23.services.syncbase.Id;
import io.v.v23.services.syncbase.SyncgroupSpec;
import io.v.v23.syncbase.ChangeType;
import io.v.v23.syncbase.Collection;
import io.v.v23.syncbase.Syncgroup;
import io.v.v23.syncbase.WatchChange;
import io.v.v23.verror.NoExistException;
import io.v.v23.verror.VException;
public class SyncbaseTodoList extends SyncbasePersistence implements TodoListPersistence {
public static final String
TAG = "SyncbaseTodoList",
LIST_METADATA_ROW_NAME = "list",
TASKS_PREFIX = "tasks_";
private static final String
SHOW_DONE_ROW_NAME = "ShowDone";
private static final long
MEMBER_TIMER_DELAY = 100,
MEMBER_TIMER_PERIOD = 5000;
private final Collection mList;
private final TodoListListener mListener;
private final IdGenerator mIdGenerator = new IdGenerator(IdAlphabets.ROW_NAME, true);
private final Set<String> mTaskIds = new HashSet<>();
private final Timer mMemberTimer;
/**
* This assumes that the collection for this list already exists.
*/
public SyncbaseTodoList(Activity activity, Bundle savedInstanceState, String listId,
TodoListListener listener)
throws VException, SyncbaseServer.StartException {
super(activity, savedInstanceState);
mListener = listener;
mList = getDatabase().getCollection(getVContext(), listId);
InputChannel<WatchChange> listWatch = getDatabase().watch(getVContext(), mList.id(), "");
ListenableFuture<Void> listWatchFuture = InputChannels.withCallback(listWatch,
new InputChannelCallback<WatchChange>() {
@Override
public ListenableFuture<Void> onNext(WatchChange change) {
processWatchChange(change);
return null;
}
});
Futures.addCallback(listWatchFuture, new TrappingCallback<Void>(getErrorReporter()) {
@Override
public void onFailure(@NonNull Throwable t) {
if (t instanceof NoExistException) {
// The collection has been deleted.
mListener.onDelete();
} else {
super.onFailure(t);
}
}
});
final Syncgroup sgHandle = getListSyncgroup();
mMemberTimer = new Timer();
mMemberTimer.scheduleAtFixedRate(new TimerTask() {
private SyncgroupSpec lastSpec;
@Override
public void run() {
Map<String, SyncgroupSpec> specMap;
try {
// Ok to block; we don't want to try parallel polls.
specMap = VFutures.sync(sgHandle.getSpec(getVContext()));
} catch (Exception e) {
Log.w(TAG, "Failed to get syncgroup spec for list: " + mList.id().getName(), e);
return;
}
String version = Iterables.getOnlyElement(specMap.keySet());
SyncgroupSpec spec = specMap.get(version);
if (spec.equals(lastSpec)) {
return; // no changes, so no event should fire.
}
lastSpec = spec;
// Get the list of patterns that can read from the spec.
Permissions perms = spec.getPerms();
AccessList acl = perms.get(Constants.READ.getValue());
List<BlessingPattern> patterns = acl.getIn();
// Analyze these patterns to construct the emails, and fire the listener!
List<String> emails = parseEmailsFromPatterns(patterns);
mListener.onShareChanged(emails);
}
}, MEMBER_TIMER_DELAY, MEMBER_TIMER_PERIOD);
// Watch the "showDone" boolean in the userdata collection and forward changes to the
// listener.
InputChannel<WatchChange> showDoneWatch = getDatabase()
.watch(getVContext(), getUserCollection().id(), SHOW_DONE_ROW_NAME);
trap(InputChannels.withCallback(showDoneWatch, new InputChannelCallback<WatchChange>() {
@Override
public ListenableFuture<Void> onNext(WatchChange result) {
mListener.onUpdateShowDone((boolean) result.getValue());
return null;
}
}));
}
protected List<String> parseEmailsFromPatterns(List<BlessingPattern> patterns) {
List<String> emails = new ArrayList<>();
for (BlessingPattern pattern : patterns) {
if (pattern.isMatchedBy(CLOUD_BLESSING)) {
// Skip. It's the cloud, and that doesn't count.
continue;
}
if (pattern.toString().endsWith(getPersonalEmail(getVContext()))) {
// Skip. It's you, and that doesn't count.
continue;
}
emails.add(getEmailFromPattern(pattern));
}
return emails;
}
@Override
public void close() {
mMemberTimer.cancel();
super.close();
}
private void processWatchChange(WatchChange change) {
String rowName = change.getRowName();
if (rowName.equals(SyncbaseTodoList.LIST_METADATA_ROW_NAME)) {
ListSpec listSpec = SyncbasePersistence.castFromSyncbase(change.getValue(),
ListSpec.class);
mListener.onUpdate(listSpec);
} else if (change.getChangeType() == ChangeType.DELETE_CHANGE) {
mTaskIds.remove(rowName);
mListener.onItemDelete(rowName);
} else {
mIdGenerator.registerId(change.getRowName().substring(TASKS_PREFIX.length()));
TaskSpec taskSpec = SyncbasePersistence.castFromSyncbase(change.getValue(),
TaskSpec.class);
Task task = new Task(rowName, taskSpec);
if (mTaskIds.add(rowName)) {
mListener.onItemAdd(task);
} else {
mListener.onItemUpdate(task);
}
}
}
@Override
public void updateTodoList(ListSpec listSpec) {
trap(mList.put(getVContext(), LIST_METADATA_ROW_NAME, listSpec, ListSpec.class));
}
@Override
public void deleteTodoList() {
trap(getUserCollection().delete(getVContext(), mList.id().getName()));
trap(mList.destroy(getVContext()));
}
private Syncgroup getListSyncgroup() {
return getDatabase().getSyncgroup(new Id(getPersonalBlessingsString(getVContext()),
computeListSyncgroupName(mList.id().getName())));
}
@Override
public void shareTodoList(final Iterable<String> emails) {
// Get the syncgroup
final Syncgroup sgHandle = getListSyncgroup();
// Get the Syncgroup Spec and add read access. Then get the collection permissions and add
// both read and write access. Along the way, trigger the listener's onShareChanged.
trap(sExecutor.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
Map<String, SyncgroupSpec> specMap = VFutures.sync(sgHandle.getSpec(getVContext()));
String version = Iterables.getOnlyElement(specMap.keySet());
SyncgroupSpec spec = specMap.get(version);
// Modify the syncgroup spec to update the permissions.
Permissions perms = spec.getPerms();
addPermissions(perms, emails, Constants.READ.getValue());
VFutures.sync(sgHandle.setSpec(getVContext(), spec, version));
// TODO(alexfandrianto): This should be the right place to send the invite
// explicitly to the selected emails.
// Analyze these patterns to construct the emails, and fire the listener!
List<String> specEmails = parseEmailsFromPatterns(
perms.get(Constants.READ.getValue()).getIn());
mListener.onShareChanged(specEmails);
// Add read and write access to the collection permissions.
perms = VFutures.sync(mList.getPermissions(getVContext()));
addPermissions(perms, emails, Constants.READ.getValue());
addPermissions(perms, emails, Constants.WRITE.getValue());
VFutures.sync(mList.setPermissions(getVContext(), perms));
return null;
}
}));
}
// TODO(alexfandrianto): We should consider moving this helper into the main Java repo.
// https://github.com/vanadium/issues/issues/1321
private static void addPermissions(Permissions perms, Iterable<String> emails, String tag) {
AccessList acl = perms.get(tag);
List<BlessingPattern> patterns = acl.getIn();
for (String email : emails) {
patterns.add(new BlessingPattern(blessingsStringFromEmail(email)));
}
perms.put(tag, acl);
}
public static ListenableFuture<Void> updateListTimestamp(final VContext vContext,
final Collection list) {
ListenableFuture<Object> get = list.get(vContext, LIST_METADATA_ROW_NAME, ListSpec.class);
return Futures.transformAsync(get, new AsyncFunction<Object, Void>() {
@Override
public ListenableFuture<Void> apply(Object oldValue) throws Exception {
ListSpec listSpec = (ListSpec) oldValue;
listSpec.setUpdatedAt(System.currentTimeMillis());
return list.put(vContext, LIST_METADATA_ROW_NAME, listSpec, ListSpec.class);
}
});
}
private void updateListTimestamp() {
trap(updateListTimestamp(getVContext(), mList));
}
@Override
public void addTask(TaskSpec task) {
trap(mList.put(getVContext(), TASKS_PREFIX + mIdGenerator.generateTailId(), task,
TaskSpec.class));
updateListTimestamp();
}
@Override
public void updateTask(Task task) {
trap(mList.put(getVContext(), task.key, task.toSpec(), TaskSpec.class));
updateListTimestamp();
}
@Override
public void deleteTask(String key) {
trap(mList.delete(getVContext(), key));
updateListTimestamp();
}
@Override
public void setShowDone(boolean showDone) {
trap(getUserCollection().put(getVContext(), SHOW_DONE_ROW_NAME, showDone, Boolean.TYPE));
}
}