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