| // 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.os.Handler; |
| import android.support.annotation.CallSuper; |
| import android.support.annotation.Nullable; |
| import android.util.Log; |
| |
| import com.google.common.base.Function; |
| import com.google.common.base.Supplier; |
| import com.google.common.base.Suppliers; |
| import com.google.common.base.Throwables; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| import com.google.common.util.concurrent.Futures; |
| import com.google.common.util.concurrent.ListenableFuture; |
| import com.google.common.util.concurrent.ListeningScheduledExecutorService; |
| import com.google.common.util.concurrent.MoreExecutors; |
| import com.google.common.util.concurrent.SettableFuture; |
| |
| import org.joda.time.DateTime; |
| import org.joda.time.Duration; |
| import org.joda.time.format.DateTimeFormat; |
| |
| import java.io.File; |
| import java.net.InetAddress; |
| import java.net.UnknownHostException; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Timer; |
| import java.util.TimerTask; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.Executors; |
| |
| import io.v.android.inspectors.RemoteInspectors; |
| import io.v.android.ManagedVAndroidContext; |
| import io.v.android.VAndroidContext; |
| import io.v.android.VAndroidContexts; |
| import io.v.android.error.ErrorReporter; |
| import io.v.android.error.ToastingErrorReporter; |
| import io.v.android.security.BlessingsManager; |
| import io.v.android.v23.V; |
| import io.v.impl.google.services.syncbase.SyncbaseServer; |
| import io.v.todos.R; |
| import io.v.todos.persistence.Persistence; |
| import io.v.todos.sharing.NeighborhoodFragment; |
| import io.v.todos.sharing.ShareListDialogFragment; |
| import io.v.todos.sharing.Sharing; |
| 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.naming.Endpoint; |
| import io.v.v23.rpc.ListenSpec; |
| import io.v.v23.rpc.Server; |
| import io.v.v23.security.BlessingPattern; |
| import io.v.v23.security.Blessings; |
| 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.SyncgroupJoinFailedException; |
| import io.v.v23.services.syncbase.SyncgroupMemberInfo; |
| import io.v.v23.services.syncbase.SyncgroupSpec; |
| import io.v.v23.syncbase.Collection; |
| import io.v.v23.syncbase.Database; |
| import io.v.v23.syncbase.Syncbase; |
| import io.v.v23.syncbase.SyncbaseService; |
| import io.v.v23.syncbase.Syncgroup; |
| import io.v.v23.syncbase.WatchChange; |
| import io.v.v23.syncbase.util.Util; |
| import io.v.v23.vdl.VdlStruct; |
| import io.v.v23.verror.ExistException; |
| import io.v.v23.verror.VException; |
| import io.v.v23.vom.VomUtil; |
| import io.v.x.ref.lib.discovery.BadAdvertisementException; |
| |
| /** |
| * TODO(rosswang): Move most of this to vanadium-android. |
| */ |
| public class SyncbasePersistence implements Persistence { |
| public static final String LISTS_PREFIX = "lists_"; |
| public static final String |
| LIST_COLLECTION_SYNCGROUP_PREFIX = "list_"; |
| |
| private static final String |
| TAG = "SyncbasePersistence", |
| FILENAME = "syncbase", |
| PROXY = "proxy", |
| DATABASE = "db", |
| BLESSINGS_KEY = "blessings", |
| USER_COLLECTION_SYNCGROUP_SUFFIX = "sg_", |
| DEFAULT_APP_BLESSING_STRING = "dev.v" + |
| ".io:o:608941808256-43vtfndets79kf5hac8ieujto8837660" + |
| ".apps.googleusercontent.com"; |
| protected static final long |
| SHORT_TIMEOUT = 2500, |
| MEMBER_TIMER_DELAY = 100, |
| MEMBER_TIMER_PERIOD = 1000; |
| public static final String |
| USER_COLLECTION_NAME = "userdata", |
| MOUNTPOINT = "/ns.dev.v.io:8101/tmp/todos/users/", |
| CLOUD_NAME = null, // MOUNTPOINT + "cloud", |
| // TODO(alexfandrianto): Restore the cloud once we can rely on it again. |
| // TODO(alexfandrianto): This shouldn't be me running the cloud. |
| CLOUD_BLESSING = "dev.v.io:u:alexfandrianto@google.com"; |
| |
| // BlessingPattern initialization has to be deferred until after V23 init due to native binding. |
| private static final Supplier<AccessList> OPEN_ACL = Suppliers.memoize( |
| new Supplier<AccessList>() { |
| @Override |
| public AccessList get() { |
| return new AccessList(ImmutableList.of(new BlessingPattern("...")), |
| ImmutableList.<String>of()); |
| } |
| }); |
| |
| protected static final ListeningScheduledExecutorService sExecutor = |
| MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(10)); |
| |
| private static final Object sVContextMutex = new Object(); |
| private static VAndroidContext<Context> sVAndroidContext; |
| |
| private static final Object sSyncbaseMutex = new Object(); |
| private static SyncbaseService sSyncbase; |
| private static RemoteInspectors sRemoteInspectors; |
| |
| private static void appVInit(Context appContext) { |
| synchronized (sVContextMutex) { |
| if (sVAndroidContext == null) { |
| sVAndroidContext = new ManagedVAndroidContext<>(appContext, |
| new ToastingErrorReporter(appContext)); |
| } |
| } |
| } |
| |
| public static Context getAppContext() { |
| return sVAndroidContext.getAndroidContext(); |
| } |
| |
| public static VContext getAppVContext() { |
| return sVAndroidContext.getVContext(); |
| } |
| |
| public static ErrorReporter getAppErrorReporter() { |
| return sVAndroidContext.getErrorReporter(); |
| } |
| |
| private static String startSyncbaseServer(VContext vContext, Context appContext, |
| Permissions serverPermissions) |
| throws SyncbaseServer.StartException { |
| try { |
| ListenSpec ls = V.getListenSpec(vContext); |
| ListenSpec.Address[] addresses = ls.getAddresses(); |
| addresses = Arrays.copyOf(addresses, addresses.length + 1); |
| addresses[addresses.length - 1] = new ListenSpec.Address("bt", "/0"); |
| ListenSpec newLs = new ListenSpec(addresses, PROXY, ls.getChooser()); |
| vContext = V.withListenSpec(vContext, newLs); |
| //vContext = V.withListenSpec(vContext, ls.withProxy(PROXY)); |
| } catch (VException e) { |
| Log.w(TAG, "Unable to set up Vanadium proxy for Syncbase"); |
| } |
| |
| File storageRoot = new File(appContext.getFilesDir(), FILENAME); |
| storageRoot.mkdirs(); |
| |
| Log.i(TAG, "Starting Syncbase"); |
| SyncbaseServer.Params params = new SyncbaseServer.Params() |
| .withPermissions(serverPermissions) |
| .withStorageRootDir(storageRoot.getAbsolutePath()); |
| |
| |
| VContext serverContext = SyncbaseServer.withNewServer(vContext, params); |
| |
| Server server = V.getServer(serverContext); |
| try { |
| // TODO(ashankar): For initial debugging it is proving useful to allow remote |
| // inspection to browse through syncbase data. But this should be removed at |
| // some point? |
| sRemoteInspectors = new RemoteInspectors(serverContext, Constants.READ); |
| } catch (VException e) { |
| Log.w(TAG, "Unable to start remote inspection service:" + e); |
| } |
| // TODO(ashankar): This is not a good idea. For one, endpoints of a service may change |
| // as the device changes networks. But I believe in a few weeks (mid June 2016) we'll |
| // switch to a mode where there are no "local RPCs" between the syncbase client and the |
| // server, so this will hopefully go away before it matters. |
| // TODO(suharshs): Although in a few weeks (mid June 2016) the new mode mentioned above |
| // will solve this issue, there is currently still an bug where initialization can hang due |
| // to the wrong endpoint getting returned here. In particular, it is important that the |
| // endpoint returned is locally accessible for the "local RPC" to succeed. |
| // So for now, we make sure to return an locally accessible endpoint. |
| Endpoint[] endpoints = server.getStatus().getEndpoints(); |
| for (Endpoint ep : endpoints) { |
| try { |
| String[] hostPort = ep.address().address().split(":"); |
| if (hostPort.length != 2) { |
| continue; |
| } |
| InetAddress addr = InetAddress.getByName(hostPort[0]); |
| if (addr.isLoopbackAddress()) { |
| return ep.name(); |
| } |
| } catch (UnknownHostException e) { |
| // Try the next address. |
| } |
| } |
| String errString = ""; |
| for (Endpoint ep : endpoints) { |
| errString += " " + ep.name(); |
| } |
| Log.e(TAG, "No locally accessible addresses in" + errString); |
| return endpoints[0].name(); |
| } |
| |
| /** |
| * Ensures that Syncbase is running. This should not be called until after the Vanadium |
| * principal has assumed blessings. The Syncbase server will run until the process is killed. |
| * |
| * @throws IllegalStateException if blessings were |
| * not attached to |
| * the principal |
| * beforehand |
| * @throws io.v.impl.google.services.syncbase.SyncbaseServer.StartException if there was an |
| * error starting |
| * the syncbase service |
| */ |
| private static void ensureSyncbaseStarted(Context androidContext) |
| throws SyncbaseServer.StartException, VException { |
| synchronized (sSyncbaseMutex) { |
| if (sSyncbase == null) { |
| VContext serverRun = getAppVContext().withCancel(); |
| try { |
| // Retrieve this context's personal permissions to set ACLs on the server. |
| Blessings personalBlessings = getPersonalBlessings(); |
| if (personalBlessings == null) { |
| throw new IllegalStateException("Blessings must be attached to the " + |
| "Vanadium principal before Syncbase initialization."); |
| } |
| Permissions permissions = computePermissionsFromBlessings(personalBlessings); |
| |
| sSyncbase = Syncbase.newService(startSyncbaseServer( |
| serverRun, getAppContext(), permissions)); |
| } catch (SyncbaseServer.StartException | RuntimeException e) { |
| serverRun.cancel(); |
| throw e; |
| } |
| } |
| } |
| } |
| |
| // TODO(rosswang): Factor into v23 |
| public static Blessings getPersonalBlessings() { |
| return V.getPrincipal(getAppVContext()).blessingStore().defaultBlessings(); |
| } |
| |
| public static String getPersonalBlessingsString() { |
| return getPersonalBlessings().toString(); |
| } |
| |
| public static String getEmailFromBlessings(Blessings blessings) { |
| // TODO(alexfandrianto): This should be in v23, but it should also verify that the app |
| // component is the right one in the blessing. |
| return getEmailFromBlessingsString(blessings.toString()); |
| } |
| |
| public static String getEmailFromPattern(BlessingPattern pattern) { |
| return getEmailFromBlessingsString(pattern.toString()); |
| } |
| |
| public static String getEmailFromBlessingsString(String blessingsStr) { |
| String[] split = blessingsStr.split(io.v.v23.security.Constants.CHAIN_SEPARATOR); |
| return split[split.length - 1]; |
| } |
| |
| public static String getPersonalEmail() { |
| return getEmailFromBlessings(getPersonalBlessings()); |
| } |
| |
| public static String blessingsStringFromEmail(String email) { |
| // TODO(alexfandrianto): We may need a more sophisticated method for producing this |
| // blessings string. Currently, the app's id is fixed to the anonymous Android app. |
| return DEFAULT_APP_BLESSING_STRING + io.v.v23.security.Constants.CHAIN_SEPARATOR + email; |
| } |
| |
| protected static Permissions computePermissionsFromBlessings(Blessings blessings) { |
| AccessList clientAcl = new AccessList(ImmutableList.of( |
| new BlessingPattern(blessings.toString()), new BlessingPattern(CLOUD_BLESSING)), |
| ImmutableList.<String>of()); |
| |
| return new Permissions(ImmutableMap.of( |
| Constants.RESOLVE.getValue(), OPEN_ACL.get(), |
| Constants.READ.getValue(), clientAcl, |
| Constants.WRITE.getValue(), clientAcl, |
| Constants.ADMIN.getValue(), clientAcl)); |
| } |
| |
| private static final Object sDatabaseMutex = new Object(); |
| private static Database sDatabase; |
| |
| private static void ensureDatabaseExists() throws VException { |
| synchronized (sDatabaseMutex) { |
| if (sDatabase == null) { |
| final Database db = sSyncbase.getDatabase(new Id(DEFAULT_APP_BLESSING_STRING, |
| DATABASE), null); |
| Permissions permissions = computePermissionsFromBlessings(getPersonalBlessings()); |
| |
| try { |
| VFutures.sync(db.create(getAppVContext(), permissions)); |
| } catch (ExistException e) { |
| // This is fine. |
| } |
| sDatabase = db; |
| } |
| } |
| } |
| |
| private static final Object sUserCollectionMutex = new Object(); |
| private static Collection sUserCollection; |
| |
| private static void ensureUserCollectionExists() throws VException { |
| synchronized (sUserCollectionMutex) { |
| if (sUserCollection == null) { |
| Collection userCollection = sDatabase.getCollection( |
| new Id(getPersonalBlessingsString(), USER_COLLECTION_NAME)); |
| |
| Permissions permissions = Util.filterPermissionsByTags( |
| computePermissionsFromBlessings(getPersonalBlessings()), |
| io.v.v23.services.syncbase.Constants.ALL_COLLECTION_TAGS); |
| try { |
| VFutures.sync(userCollection.create(getAppVContext(), permissions)); |
| } catch (ExistException e) { |
| // This is fine. |
| } |
| sUserCollection = userCollection; |
| } |
| } |
| } |
| |
| private static final Object sCloudDatabaseMutex = new Object(); |
| private static Database sCloudDatabase; |
| |
| // TODO(alexfandrianto): Remove. Cloud database creation shouldn't be done in the app. |
| private static void ensureCloudDatabaseExists() { |
| synchronized (sCloudDatabaseMutex) { |
| if (sCloudDatabase == null) { |
| SyncbaseService cloudService = Syncbase.newService(CLOUD_NAME); |
| Database db = cloudService.getDatabase(getAppVContext(), DATABASE, null); |
| |
| try { |
| VFutures.sync(db.create(getAppVContext() |
| .withTimeout(Duration.millis(SHORT_TIMEOUT)), null)); |
| } catch (ExistException e) { |
| // This is acceptable. No need to do it again. |
| } catch (Exception e) { |
| Log.w(TAG, "Could not ensure cloud database exists: " + e.getMessage()); |
| } |
| sCloudDatabase = db; |
| } |
| } |
| } |
| |
| private static final Object sUserSyncgroupMutex = new Object(); |
| private static Syncgroup sUserSyncgroup; |
| |
| private static void ensureUserSyncgroupExists() throws VException { |
| synchronized (sUserSyncgroupMutex) { |
| if (sUserSyncgroup == null) { |
| Blessings clientBlessings = getPersonalBlessings(); |
| String email = getEmailFromBlessings(clientBlessings); |
| Log.d(TAG, email); |
| |
| Permissions permissions = Util.filterPermissionsByTags( |
| computePermissionsFromBlessings(clientBlessings), |
| io.v.v23.services.syncbase.Constants.ALL_SYNCGROUP_TAGS); |
| |
| String sgName = USER_COLLECTION_SYNCGROUP_SUFFIX + Math.abs(email.hashCode()); |
| |
| final SyncgroupMemberInfo memberInfo = getDefaultMemberInfo(); |
| final Syncgroup sgHandle = sDatabase.getSyncgroup(new Id(clientBlessings.toString |
| (), sgName)); |
| |
| try { |
| Log.d(TAG, "Trying to join the syncgroup: " + sgName); |
| VFutures.sync(sgHandle.join(getAppVContext(), CLOUD_NAME, |
| Arrays.asList(CLOUD_BLESSING), memberInfo)); |
| Log.d(TAG, "JOINED the syncgroup: " + sgName); |
| } catch (SyncgroupJoinFailedException e) { |
| Log.w(TAG, "Failed join. Trying to create the syncgroup: " + sgName, e); |
| SyncgroupSpec spec = new SyncgroupSpec( |
| "TODOs User Data Collection", CLOUD_NAME, permissions, |
| ImmutableList.of(sUserCollection.id()), |
| ImmutableList.of(MOUNTPOINT), false); |
| try { |
| VFutures.sync(sgHandle.create(getAppVContext(), spec, memberInfo)); |
| } catch (BadAdvertisementException e2) { |
| Log.d(TAG, "Bad advertisement exception. Can we fix this?"); |
| } |
| Log.d(TAG, "CREATED the syncgroup: " + sgName); |
| } catch (Exception e) { |
| Log.d(TAG, "Failed to join or create the syncgroup: " + sgName); |
| if (!(e instanceof BadAdvertisementException)) { // joined, I guess |
| throw e; |
| } |
| } |
| sUserSyncgroup = sgHandle; |
| } |
| } |
| } |
| |
| protected static SyncgroupMemberInfo getDefaultMemberInfo() { |
| SyncgroupMemberInfo memberInfo = new SyncgroupMemberInfo(); |
| memberInfo.setSyncPriority((byte) 3); |
| return memberInfo; |
| |
| } |
| |
| protected static String computeListSyncgroupName(String listId) { |
| return LIST_COLLECTION_SYNCGROUP_PREFIX + listId; |
| } |
| |
| private static String BLESSING_NAME_SEPARATOR = "___"; |
| |
| public static String convertIdToString(Id id) { |
| // Put the name first since it has a useful prefix for watch to switch on. |
| return id.getName() + BLESSING_NAME_SEPARATOR + id.getBlessing(); |
| } |
| |
| public static Id convertStringToId(String idString) { |
| String[] parts = idString.split(BLESSING_NAME_SEPARATOR); |
| return new Id(parts[1], parts[0]); |
| } |
| |
| private static volatile boolean sInitialized; |
| |
| public static boolean isInitialized() { |
| return sInitialized; |
| } |
| |
| /** |
| * Extracts the value from a watch change or scan stream. |
| * TODO(rosswang): This method is a temporary hack, awaiting resolution of the following issues: |
| * <ul> |
| * <li><a href="https://github.com/vanadium/issues/issues/1305">#1305</a> |
| * <li><a href="https://github.com/vanadium/issues/issues/1310">#1310</a> |
| * </ul> |
| */ |
| @SuppressWarnings("unchecked") |
| public static <T> T castFromSyncbase(Object watchValue, Class<T> type) { |
| if (type.isInstance(watchValue)) { |
| return (T) watchValue; |
| } |
| |
| try { |
| return (T) VomUtil.decode(VomUtil.encode((VdlStruct) watchValue), type); |
| } catch (VException e) { |
| Log.e(TAG, Throwables.getStackTraceAsString(e)); |
| throw new ClassCastException("Could not cast " + watchValue + " to " + type); |
| } |
| } |
| |
| protected class SyncTrappingCallback<T> extends TrappingCallback<T> { |
| public SyncTrappingCallback() { |
| super(R.string.err_sync, TAG, getErrorReporter()); |
| } |
| } |
| |
| private final VAndroidContext<Activity> mVAndroidContext; |
| |
| public VContext getVContext() { |
| return mVAndroidContext.getVContext(); |
| } |
| |
| public ErrorReporter getErrorReporter() { |
| return mVAndroidContext.getErrorReporter(); |
| } |
| |
| @Override |
| public void close() { |
| mVAndroidContext.close(); |
| } |
| |
| protected Database getDatabase() { |
| return sDatabase; |
| } |
| |
| protected Collection getUserCollection() { |
| return sUserCollection; |
| } |
| |
| /** |
| * @see TrappingCallback |
| */ |
| protected void trap(ListenableFuture<?> future) { |
| Futures.addCallback(future, new SyncTrappingCallback<>()); |
| } |
| |
| /** |
| * 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, |
| @Nullable FragmentTransaction transaction) { |
| if (transaction != null) { |
| transaction.add(new NeighborhoodFragment(), NeighborhoodFragment.FRAGMENT_TAG); |
| } |
| } |
| |
| /** |
| * This constructor is blocking for simplicity. |
| */ |
| public SyncbasePersistence(final Activity activity, Bundle savedInstanceState) |
| throws VException, SyncbaseServer.StartException { |
| mVAndroidContext = VAndroidContexts.withDefaults(activity, savedInstanceState); |
| |
| FragmentManager mgr = activity.getFragmentManager(); |
| if (savedInstanceState == null) { |
| FragmentTransaction t = mgr.beginTransaction(); |
| addFeatureFragments(mgr, t); |
| t.commit(); |
| } else { |
| addFeatureFragments(mgr, null); |
| } |
| |
| // We might not actually have to seek blessings each time, but getBlessings does not |
| // block if we already have blessings and this has better-behaved lifecycle |
| // implications than trying to seek blessings in the static code. |
| final SettableFuture<ListenableFuture<Blessings>> blessings = SettableFuture.create(); |
| if (activity.getMainLooper().getThread() == Thread.currentThread()) { |
| blessings.set(BlessingsManager.getBlessings(getVContext(), activity, |
| BLESSINGS_KEY, true)); |
| } else { |
| new Handler(activity.getMainLooper()).post(new Runnable() { |
| @Override |
| public void run() { |
| blessings.set(BlessingsManager.getBlessings(getVContext(), |
| activity, BLESSINGS_KEY, true)); |
| } |
| }); |
| } |
| |
| VFutures.sync(Futures.dereference(blessings)); |
| appVInit(activity.getApplicationContext()); |
| final SyncbasePersistence self = this; |
| /*final Future<?> ensureCloudDatabaseExists = sExecutor.submit(new Runnable() { |
| @Override |
| public void run() { |
| ensureCloudDatabaseExists(); |
| } |
| });*/ |
| ensureSyncbaseStarted(activity); |
| ensureDatabaseExists(); |
| ensureUserCollectionExists(); |
| // TODO(alexfandrianto): If the cloud is dependent on me, then we must do this too. |
| // VFutures.sync(ensureCloudDatabaseExists); // must finish before syncgroup setup |
| ensureUserSyncgroupExists(); |
| Sharing.initDiscovery(sDatabase); // requires that db and collection exist |
| ShareListDialogFragment.initScan(); |
| sInitialized = true; |
| } |
| |
| @Override |
| public String debugDetails() { |
| synchronized (sSyncbaseMutex) { |
| if (sRemoteInspectors == null) { |
| return "Syncbase has not been initialized"; |
| } |
| final String timestamp = DateTimeFormat.forPattern("yyyy-MM-dd").print(new DateTime()); |
| try { |
| return sRemoteInspectors.invite("invited-on-" + timestamp, Duration.standardDays |
| (1)); |
| } catch (VException e) { |
| return "Unable to setup remote inspection: " + e; |
| } |
| } |
| } |
| |
| public static void acceptSharedTodoList(final Id listId) { |
| sExecutor.submit(new Callable<Void>() { |
| @Override |
| public Void call() throws VException { |
| Boolean exists = VFutures.sync(sUserCollection.getRow(convertIdToString(listId)). |
| exists(getAppVContext())); |
| if (!exists) { |
| VFutures.sync(rememberTodoList(listId)); |
| } |
| return null; |
| } |
| }); |
| } |
| |
| protected static ListenableFuture<Void> rememberTodoList(Id listId) { |
| return sUserCollection.put(getAppVContext(), convertIdToString(listId), ""); |
| } |
| |
| public static ListenableFuture<Void> watchUserCollection(InputChannelCallback<WatchChange> |
| callback) { |
| InputChannel<WatchChange> watch = sDatabase.watch(getAppVContext(), |
| ImmutableList.of(Util.rowPrefixPattern(sUserCollection.id(), LISTS_PREFIX))); |
| return InputChannels.withCallback(watch, callback); |
| } |
| |
| public static Timer watchSharedTo(final Id listId, final Function<List<BlessingPattern>, |
| Void> callback) { |
| final Syncgroup sgHandle = sDatabase.getSyncgroup(new Id(listId.getBlessing(), |
| computeListSyncgroupName(listId.getName()))); |
| |
| Timer timer = new Timer(); |
| timer.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(getAppVContext())); |
| } catch (Exception e) { |
| Log.w(TAG, "Failed to get syncgroup spec for list: " + listId, 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. |
| } |
| Log.d(TAG, "Spec changed for list: " + listId); |
| lastSpec = spec; |
| |
| Permissions perms = spec.getPerms(); |
| AccessList acl = perms.get(Constants.READ.getValue()); |
| List<BlessingPattern> patterns = acl.getIn(); |
| |
| callback.apply(patterns); |
| } |
| }, MEMBER_TIMER_DELAY, MEMBER_TIMER_PERIOD); |
| return timer; |
| } |
| } |