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