| // 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.ImmutableList; |
| import com.google.common.util.concurrent.AsyncFunction; |
| import com.google.common.util.concurrent.Futures; |
| import com.google.common.util.concurrent.ListenableFuture; |
| |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.TimeUnit; |
| |
| import javax.annotation.Nullable; |
| |
| import io.v.impl.google.services.syncbase.SyncbaseServer; |
| import io.v.todos.model.ListMetadata; |
| import io.v.todos.model.ListSpec; |
| import io.v.todos.persistence.ListEventListener; |
| import io.v.todos.persistence.MainPersistence; |
| import io.v.v23.InputChannelCallback; |
| 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.SyncgroupJoinFailedException; |
| import io.v.v23.services.syncbase.SyncgroupMemberInfo; |
| import io.v.v23.services.syncbase.SyncgroupSpec; |
| import io.v.v23.syncbase.ChangeType; |
| import io.v.v23.syncbase.Collection; |
| import io.v.v23.syncbase.WatchChange; |
| import io.v.v23.syncbase.util.Util; |
| import io.v.v23.verror.NoExistException; |
| import io.v.v23.verror.VException; |
| |
| public class SyncbaseMain extends SyncbasePersistence implements MainPersistence { |
| private static final String |
| TAG = SyncbaseMain.class.getSimpleName(); |
| |
| private static final int DEFAULT_MAX_JOIN_ATTEMPTS = 30; |
| private static final long MIN_RETRY_DELAY = 1000; |
| |
| private final IdGenerator mIdGenerator = new IdGenerator(IdAlphabets.COLLECTION_ID, true); |
| private final Map<String, MainListTracker> mTaskTrackers = new HashMap<>(); |
| |
| /** |
| * This constructor blocks until the instance is ready for use. |
| */ |
| public SyncbaseMain(Activity activity, Bundle savedInstanceState, |
| final ListEventListener<ListMetadata> listener) |
| throws VException, SyncbaseServer.StartException { |
| super(activity, savedInstanceState); |
| |
| // Prepare a watch on top of the userdata collection to determine which todo lists need to |
| // be tracked by this application. |
| trap(watchUserCollection(new InputChannelCallback<WatchChange>() { |
| @Override |
| public ListenableFuture<Void> onNext(WatchChange change) { |
| try { |
| final String listIdStr = change.getRowName(); |
| final Id listId = convertStringToId(listIdStr); |
| |
| if (change.getChangeType() == ChangeType.DELETE_CHANGE) { |
| // (this is idempotent) |
| Log.d(TAG, listIdStr + " removed from index"); |
| deleteTodoList(listIdStr); |
| } else { |
| // If we are tracking this list already, don't bother doing anything. |
| // This might happen if a same-user device did a simultaneous put into the |
| // userdata collection. |
| if (mTaskTrackers.get(listIdStr) != null) { |
| return null; |
| } |
| |
| mIdGenerator.registerId(listId.getName().substring(LISTS_PREFIX.length())); |
| |
| Log.d(TAG, "Found a list id from userdata watch: " + listId.getName() + |
| " with owner: " + listId.getBlessing()); |
| trap(joinWithRetry(listId)); |
| |
| MainListTracker listTracker = new MainListTracker(getVContext(), |
| getDatabase(), listId, listener); |
| mTaskTrackers.put(listIdStr, listTracker); |
| |
| // If the watch fails with NoExistException, the collection has been deleted. |
| Futures.addCallback(listTracker.watchFuture, new SyncTrappingCallback<Void>() { |
| @Override |
| public void onFailure(@NonNull Throwable t) { |
| if (t instanceof NoExistException) { |
| // (this is idempotent) |
| trap(getUserCollection().delete(getVContext(), listIdStr)); |
| } else { |
| super.onFailure(t); |
| } |
| } |
| }); |
| } |
| } catch (Exception e) { |
| Log.w(TAG, "Error during watch handle", e); |
| } |
| return null; |
| } |
| })); |
| } |
| |
| @Override |
| public String addTodoList(final ListSpec listSpec) { |
| final String listName = LISTS_PREFIX + mIdGenerator.generateTailId(); |
| final Id listId = new Id(getPersonalBlessingsString(), listName); |
| final Collection listCollection = getDatabase().getCollection(listId); |
| Permissions permissions = Util.filterPermissionsByTags( |
| computePermissionsFromBlessings(getPersonalBlessings()), |
| io.v.v23.services.syncbase.Constants.ALL_COLLECTION_TAGS); |
| Futures.addCallback(listCollection.create(getVContext(), permissions), |
| new SyncTrappingCallback<Void>() { |
| @Override |
| public void onSuccess(@Nullable Void result) { |
| // These can happen in any order. |
| trap(listCollection.put(getVContext(), |
| SyncbaseTodoList.LIST_METADATA_ROW_NAME, listSpec)); |
| trap(rememberTodoList(listId)); |
| // TODO(alexfandrianto): Syncgroup creation is slow if you specify a cloud |
| // and are offline. https://github.com/vanadium/issues/issues/1326 |
| trap(createListSyncgroup(listCollection.id())); |
| } |
| }); |
| return convertIdToString(listId); |
| } |
| |
| private ListenableFuture<SyncgroupSpec> joinListSyncgroup(Id listId) { |
| SyncgroupMemberInfo memberInfo = getDefaultMemberInfo(); |
| String sgName = computeListSyncgroupName(listId.getName()); |
| return getDatabase().getSyncgroup(new Id(listId.getBlessing(), sgName)).join(getVContext(), |
| CLOUD_NAME, Arrays.asList(CLOUD_BLESSING), memberInfo); |
| } |
| |
| // Join the syncgroup. Retry if there are failures. |
| private ListenableFuture<SyncgroupSpec> joinWithRetry(Id listId) { |
| return joinWithRetry(listId, 0, DEFAULT_MAX_JOIN_ATTEMPTS); |
| } |
| |
| private ListenableFuture<SyncgroupSpec> joinWithRetry(final Id listId, final int numTimes, |
| final int limit) { |
| final String debugString = (numTimes + 1) + "/" + limit + " for: " + listId; |
| Log.d(TAG, "Join attempt " + debugString); |
| if (numTimes + 1 == limit) { // final attempt! |
| return joinListSyncgroup(listId); |
| } |
| // Note: This can be easily converted to exponential backoff. |
| final long startTime = System.currentTimeMillis(); |
| return Futures.catchingAsync( |
| joinListSyncgroup(listId), |
| SyncgroupJoinFailedException.class, |
| new AsyncFunction<SyncgroupJoinFailedException, SyncgroupSpec>() { |
| public ListenableFuture<SyncgroupSpec> apply(@Nullable |
| SyncgroupJoinFailedException |
| input) { |
| long failTime = System.currentTimeMillis(); |
| long delay = Math.max(0, MIN_RETRY_DELAY + startTime - failTime); |
| |
| Log.d(TAG, "Join failed. Sleeping " + debugString + " with delay " + delay); |
| return sExecutor.schedule(new Callable<SyncgroupSpec>() { |
| |
| |
| @Override |
| public SyncgroupSpec call() { |
| Log.d(TAG, "Sleep done. Retry " + debugString); |
| |
| // If this errors, then we will not get another chance to |
| // see this syncgroup until the app is restarted. |
| try { |
| return joinWithRetry(listId, numTimes + 1, limit).get(); |
| } catch (InterruptedException | ExecutionException e) { |
| return null; |
| } |
| } |
| }, delay, TimeUnit.MILLISECONDS); |
| } |
| }); |
| } |
| |
| private ListenableFuture<Void> createListSyncgroup(Id id) { |
| String listName = id.getName(); |
| final String sgName = computeListSyncgroupName(listName); |
| Permissions permissions = Util.filterPermissionsByTags( |
| computePermissionsFromBlessings(getPersonalBlessings()), |
| io.v.v23.services.syncbase.Constants.ALL_SYNCGROUP_TAGS); |
| |
| SyncgroupMemberInfo memberInfo = getDefaultMemberInfo(); |
| |
| SyncgroupSpec spec = new SyncgroupSpec( |
| "TODO list", CLOUD_NAME, permissions, |
| ImmutableList.of(id), |
| ImmutableList.of(MOUNTPOINT), false); |
| String blessingStr = getPersonalBlessingsString(); |
| return getDatabase().getSyncgroup(new Id(blessingStr, sgName)).create(getVContext(), |
| spec, memberInfo); |
| } |
| |
| @Override |
| public void deleteTodoList(String key) { |
| MainListTracker tracker = mTaskTrackers.remove(key); |
| if (tracker != null) { |
| trap(tracker.collection.destroy(getVContext())); |
| } |
| } |
| } |