TODOs: Use constant ad and scan to share lists to invited users

While the new Syncbase API is being worked on, we will perform sharing
with Discovery directly.

- constant scan for syncgroups advertised to you
- constant ad for syncgroups you shared with others

Issues:
- Discovery is a little flaky; the shares don't always succeed after the
  first few
- Nexus 9 still likes to crash (because of the CGo pointer problem)
- Discovery doesn't let you make these ads private
  https://github.com/vanadium/issues/issues/1328
- Menu to Share is shown even though the user may only have Read access
  for the todo list syncgroup.

Change-Id: I31e54ca0032b1a2186091b7fddeebbe2a439a86c
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
index ed6fd8f..33bfa9e 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
@@ -47,14 +47,12 @@
 import io.v.v23.syncbase.Collection;
 import io.v.v23.syncbase.RowRange;
 import io.v.v23.syncbase.WatchChange;
-import io.v.v23.vdl.VdlAny;
 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(),
-            LISTS_PREFIX = "lists_";
+            TAG = SyncbaseMain.class.getSimpleName();
 
     private final IdGenerator mIdGenerator = new IdGenerator(IdAlphabets.COLLECTION_ID, true);
     private final Map<String, MainListTracker> mTaskTrackers = new HashMap<>();
@@ -69,22 +67,30 @@
 
         // Prepare a watch on top of the userdata collection to determine which todo lists need to
         // be tracked by this application.
-        InputChannel<WatchChange> watch = getDatabase().watch(getVContext(),
-                getUserCollection().id(), LISTS_PREFIX);
-        trap(InputChannels.withCallback(watch, new InputChannelCallback<WatchChange>() {
+        trap(watchUserCollection(new InputChannelCallback<WatchChange>() {
             @Override
             public ListenableFuture<Void> onNext(WatchChange change) {
                 final String listId = change.getRowName();
+                final String ownerBlessing = SyncbasePersistence.castFromSyncbase(change.getValue(),
+                        String.class);
 
                 if (change.getChangeType() == ChangeType.DELETE_CHANGE) {
                     // (this is idempotent)
                     Log.d(TAG, listId + " removed from index");
                     deleteTodoList(listId);
                 } 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(listId) != null) {
+                        return null;
+                    }
+
                     mIdGenerator.registerId(change.getRowName().substring(LISTS_PREFIX.length()));
 
-                    Log.d(TAG, "Found a list id from userdata watch: " + listId);
-                    trap(Futures.catchingAsync(joinListSyncgroup(listId),
+                    Log.d(TAG, "Found a list id from userdata watch: " + listId + " with owner: "
+                            + ownerBlessing);
+                    trap(Futures.catchingAsync(joinListSyncgroup(listId, ownerBlessing),
                             SyncgroupJoinFailedException.class, new
                                     AsyncFunction<SyncgroupJoinFailedException, SyncgroupSpec>() {
                         public ListenableFuture<SyncgroupSpec> apply(@Nullable
@@ -100,7 +106,7 @@
 
                                     // If this errors, then we will not get another chance to see
                                     // this syncgroup until the app is restarted.
-                                    return joinListSyncgroup(listId).get();
+                                    return joinListSyncgroup(listId, ownerBlessing).get();
                                 }
                             }, RETRY_DELAY, TimeUnit.MILLISECONDS);
                         }
@@ -108,12 +114,7 @@
 
                     MainListTracker listTracker = new MainListTracker(getVContext(), getDatabase(),
                             listId, listener);
-                    if (mTaskTrackers.put(listId, listTracker) != null) {
-                        // List entries in the main collection are just ( list ID => nil ), so we
-                        // never expect updates other than an initial add...
-                        Log.w(TAG, "Unexpected update to " + USER_COLLECTION_NAME + " collection " +
-                                "for list " + listId);
-                    }
+                    mTaskTrackers.put(listId, listTracker);
 
                     // If the watch fails with NoExistException, the collection has been deleted.
                     Futures.addCallback(listTracker.watchFuture, new SyncTrappingCallback<Void>() {
@@ -152,8 +153,7 @@
                                     @Override
                                     public void onSuccess(@Nullable Void result) {
                                         // These can happen in either order.
-                                        trap(getUserCollection().put(getVContext(), listName, null,
-                                                VdlAny.class));
+                                        trap(rememberTodoList(listName));
                                         trap(listCollection.put(getVContext(),
                                                 SyncbaseTodoList.LIST_METADATA_ROW_NAME, listSpec,
                                                 ListSpec.class));
@@ -163,11 +163,10 @@
                 });
     }
 
-    private ListenableFuture<SyncgroupSpec> joinListSyncgroup(String listId) {
+    private ListenableFuture<SyncgroupSpec> joinListSyncgroup(String listId, String ownerBlessing) {
         SyncgroupMemberInfo memberInfo = getDefaultMemberInfo();
         String sgName = computeListSyncgroupName(listId);
-        String blessingStr = getPersonalBlessingsString();
-        return getDatabase().getSyncgroup(new Id(blessingStr, sgName)).join(getVContext(),
+        return getDatabase().getSyncgroup(new Id(ownerBlessing, sgName)).join(getVContext(),
                 CLOUD_NAME, CLOUD_BLESSING, memberInfo);
     }
 
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
index e0692f9..13365d4 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
@@ -14,11 +14,14 @@
 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.AsyncFunction;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningScheduledExecutorService;
@@ -30,6 +33,10 @@
 import org.joda.time.format.DateTimeFormat;
 
 import java.io.File;
+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 java.util.concurrent.Future;
@@ -47,6 +54,9 @@
 import io.v.todos.persistence.Persistence;
 import io.v.todos.sharing.NeighborhoodFragment;
 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.rpc.Server;
@@ -65,6 +75,7 @@
 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.vdl.VdlStruct;
 import io.v.v23.verror.ExistException;
 import io.v.v23.verror.VException;
@@ -85,9 +96,12 @@
             LIST_COLLECTION_SYNCGROUP_SUFFIX = "list_",
             DEFAULT_BLESSING_STRING = "dev.v.io:o:608941808256-43vtfndets79kf5hac8ieujto8837660" +
                     ".apps.googleusercontent.com:";
+    protected static final String LISTS_PREFIX = "lists_";
     protected static final long
             SHORT_TIMEOUT = 2500,
-            RETRY_DELAY = 2000;
+            RETRY_DELAY = 2000,
+            MEMBER_TIMER_DELAY = 100,
+            MEMBER_TIMER_PERIOD = 5000;
     public static final String
             USER_COLLECTION_NAME = "userdata",
             MOUNTPOINT = "/ns.dev.v.io:8101/tmp/todos/users/",
@@ -431,6 +445,7 @@
 
     /**
      * 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.
@@ -478,13 +493,7 @@
 
         VFutures.sync(Futures.dereference(blessings));
         appVInit(activity.getApplicationContext());
-        Future<?> initDiscovery = sExecutor.submit(new Callable<Void>() {
-            @Override
-            public Void call() throws VException {
-                Sharing.initDiscovery();
-                return null;
-            }
-        });
+        final SyncbasePersistence self = this;
         final Future<?> ensureCloudDatabaseExists = sExecutor.submit(new Runnable() {
             @Override
             public void run() {
@@ -496,7 +505,7 @@
         ensureUserCollectionExists();
         VFutures.sync(ensureCloudDatabaseExists); // must finish before syncgroup setup
         ensureUserSyncgroupExists();
-        VFutures.sync(initDiscovery);
+        Sharing.initDiscovery(); // requires that db and collection exist
         sInitialized = true;
     }
 
@@ -515,4 +524,73 @@
             }
         }
     }
+
+    public static void acceptSharedTodoList(final String listId, final String owner) {
+        sExecutor.submit(new Callable<Void>() {
+            @Override
+            public Void call() throws VException {
+                Boolean exists = VFutures.sync(sUserCollection.getRow(listId).exists
+                        (getAppVContext()));
+                if (!exists) {
+                    VFutures.sync(rememberTodoList(listId, owner));
+                }
+                return null;
+            }
+        });
+    }
+
+    protected static ListenableFuture<Void> rememberTodoList(String listId) {
+        return rememberTodoList(listId, getPersonalBlessingsString());
+    }
+
+    protected static ListenableFuture<Void> rememberTodoList(String listId, String owner) {
+        return sUserCollection.put(getAppVContext(), listId, owner, String.class);
+    }
+
+    public static ListenableFuture<Void> watchUserCollection(InputChannelCallback<WatchChange>
+                                                                     callback) {
+        InputChannel<WatchChange> watch = sDatabase.watch(getAppVContext(),
+                sUserCollection.id(), LISTS_PREFIX);
+        return InputChannels.withCallback(watch, callback);
+    }
+
+    public static Timer watchSharedTo(final String listId, final Function<List<BlessingPattern>,
+            Void>
+            callback) {
+        final Syncgroup sgHandle = sDatabase.getSyncgroup(new Id(getPersonalBlessingsString(),
+                computeListSyncgroupName(listId)));
+
+        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;
+    }
 }
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
index 6fad570..c4639fb 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
@@ -9,8 +9,8 @@
 import android.app.FragmentTransaction;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.util.Log;
 
+import com.google.common.base.Function;
 import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.AsyncFunction;
 import com.google.common.util.concurrent.Futures;
@@ -22,7 +22,6 @@
 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;
@@ -59,10 +58,6 @@
     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);
@@ -81,6 +76,8 @@
         }
         mShareListMenuFragment.persistence = this;
         mShareListMenuFragment.setEmail(getPersonalEmail());
+        // TODO(alexfandrianto): I shouldn't show the sharing menu item when this person cannot
+        // share the todo list with other people. (Cannot re-share in this app.)
     }
 
     /**
@@ -114,40 +111,15 @@
             }
         });
 
-        final Syncgroup sgHandle = getListSyncgroup();
-        mMemberTimer = new Timer();
-        mMemberTimer.scheduleAtFixedRate(new TimerTask() {
-            private SyncgroupSpec lastSpec;
-
+        mMemberTimer = watchSharedTo(listId, new Function<List<BlessingPattern>, Void>() {
             @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();
-
+            public Void apply(List<BlessingPattern> patterns) {
                 // Analyze these patterns to construct the emails, and fire the listener!
                 List<String> emails = parseEmailsFromPatterns(patterns);
                 mShareListMenuFragment.setSharedTo(emails);
+                return null;
             }
-        }, MEMBER_TIMER_DELAY, MEMBER_TIMER_PERIOD);
+        });
 
         // Watch the "showDone" boolean in the userdata collection and forward changes to the
         // listener.
diff --git a/app/src/syncbase/java/io/v/todos/sharing/Sharing.java b/app/src/syncbase/java/io/v/todos/sharing/Sharing.java
index 73bc757..0be7f38 100644
--- a/app/src/syncbase/java/io/v/todos/sharing/Sharing.java
+++ b/app/src/syncbase/java/io/v/todos/sharing/Sharing.java
@@ -4,16 +4,47 @@
 
 package io.v.todos.sharing;
 
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
 import io.v.android.v23.V;
+import io.v.todos.R;
 import io.v.todos.persistence.syncbase.SyncbasePersistence;
+import io.v.v23.InputChannelCallback;
+import io.v.v23.InputChannels;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Advertisement;
 import io.v.v23.discovery.Discovery;
+import io.v.v23.discovery.Update;
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.syncbase.ChangeType;
+import io.v.v23.syncbase.WatchChange;
 import io.v.v23.verror.VException;
 
 public final class Sharing {
-    private Sharing(){}
+    private Sharing() {
+    }
 
+    private static final String OWNER_KEY = "owner";
     private static final Object sDiscoveryMutex = new Object();
     private static Discovery sDiscovery;
+    private static VContext sScanContext;
+    private static VContext sAdvertiseContext;
+    private final static Map<String, VContext> sAdContextMap = new HashMap<>();
 
     public static Discovery getDiscovery() {
         return sDiscovery;
@@ -26,10 +57,22 @@
 
                 // Rely on the neighborhood fragment to initialize presence advertisement.
                 NeighborhoodFragment.initSharePresence();
+
+                sScanContext = initScanForInvites();
+                sAdvertiseContext = initAdvertiseLists();
             }
         }
     }
 
+    // TODO(alexfandrianto): Nobody calls this, so we never stop sharing.
+    public static void stopDiscovery() {
+        synchronized (sDiscoveryMutex) {
+            sScanContext.cancel();
+            sAdvertiseContext.cancel();
+            sAdContextMap.clear();
+        }
+    }
+
     private static String getRootInterface() {
         return SyncbasePersistence.getAppContext().getPackageName();
     }
@@ -41,4 +84,183 @@
     public static String getInvitationInterface() {
         return getRootInterface() + ".invitation";
     }
+
+    /**
+     * Starts a scanner seeking advertisements that invite this user to a todo list. When an invite
+     * is found, the app will automatically accept it.
+     */
+    public static VContext initScanForInvites()
+            throws VException {
+        VContext vContext = SyncbasePersistence.getAppVContext().withCancel();
+        try {
+            ListenableFuture<Void> scan = InputChannels.withCallback(
+                    Sharing.getDiscovery().scan(vContext,
+                            "v.InterfaceName = \"" + Sharing.getInvitationInterface() + "\""),
+                    new InputChannelCallback<Update>() {
+                        @Override
+                        public ListenableFuture<Void> onNext(Update result) {
+                            final String listName = Iterables.getOnlyElement(result.getAddresses());
+                            if (listName == null) {
+                                return null;
+                            }
+                            Log.d("SHARING", "Noticed advertised list: " + listName + " by: " +
+                                    result.getAttribute(OWNER_KEY));
+
+                            // TODO(alexfandrianto): Remove hack.
+                            // https://github.com/vanadium/issues/issues/1328
+                            if (result.getAttribute(SyncbasePersistence
+                                    .getPersonalBlessingsString()) == null) {
+                                Log.d("SHARING", "...but the ad was not meant for this user.");
+                                return null; // ignore; this isn't meant for us
+                            }
+
+                            // Never mind about losses, just handle found advertisements.
+                            if (!result.isLost()) {
+                                Log.d("SHARING", "...and will accept it.");
+
+                                SyncbasePersistence.acceptSharedTodoList(listName, result
+                                        .getAttribute(OWNER_KEY));
+                            }
+                            return null;
+                        }
+                    });
+            Futures.addCallback(scan, new FutureCallback<Void>() {
+                @Override
+                public void onSuccess(@Nullable Void result) {
+                }
+
+                @Override
+                public void onFailure(Throwable t) {
+                    handleScanListsError(t);
+                }
+            });
+        } catch (VException e) {
+            handleScanListsError(e);
+        }
+        return vContext;
+    }
+
+    private static void handleScanListsError(Throwable t) {
+        SyncbasePersistence.getAppErrorReporter().onError(R.string.err_scan_lists, t);
+    }
+
+    /**
+     * Creates advertisements based on the todo lists this user has created thus far and those that
+     * are created in the future. The advertisements will need to be targeted to the users that have
+     * been invited to the list.
+     *
+     * @return
+     * @throws VException
+     */
+    public static VContext initAdvertiseLists()
+            throws VException {
+        final VContext vContext = SyncbasePersistence.getAppVContext().withCancel();
+
+        // Prepare a watch on top of the userdata collection to determine which todo lists need to
+        // be tracked by this application.
+        SyncbasePersistence.watchUserCollection(new InputChannelCallback<WatchChange>() {
+            @Override
+            public ListenableFuture<Void> onNext(WatchChange change) {
+                final String listId = change.getRowName();
+                final String owner = SyncbasePersistence.castFromSyncbase(change.getValue(),
+                        String.class);
+                if (!owner.equals(SyncbasePersistence.getPersonalBlessingsString())) {
+                    return Futures.immediateFuture((Void) null);
+                }
+
+                if (change.getChangeType() == ChangeType.DELETE_CHANGE) {
+                    VContext ctx = sAdContextMap.remove(listId);
+                    ctx.cancel(); // Stop advertising the list; it's been deleted.
+                } else {
+                    // We should probably start to advertise this collection and check its spec.
+                    SyncbasePersistence.watchSharedTo(listId, new Function<List<BlessingPattern>,
+                            Void>() {
+                        @Override
+                        public Void apply(List<BlessingPattern> patterns) {
+                            // Make a copy of the patterns list that excludes the cloud and this
+                            // user's blessings.
+                            List<BlessingPattern> filteredPatterns = new ArrayList<>();
+                            for (BlessingPattern pattern : patterns) {
+                                String pStr = pattern.toString();
+                                if (pStr.equals(SyncbasePersistence.getPersonalBlessingsString()) ||
+                                        pStr.equals(SyncbasePersistence.CLOUD_BLESSING)) {
+                                    continue;
+                                }
+                                filteredPatterns.add(pattern);
+                            }
+
+                            // Advertise to the remaining patterns.
+                            if (filteredPatterns.size() > 0) {
+                                Log.d("SHARING", "Must advertise for " + listId + " to " +
+                                        filteredPatterns.toString());
+                                advertiseList(vContext, listId, filteredPatterns);
+                            }
+                            return null;
+                        }
+                    });
+                }
+                return null;
+            }
+        });
+        return vContext;
+    }
+
+    /**
+     * Advertises that this list is available to this set of people. Cancels the old advertisement
+     * if one exists. Only called by initAdvertiseLists.
+     *
+     * @param baseContext The context for all advertisements
+     * @param listId      The list to be advertised
+     * @param patterns    Blessings that the advertisement should target
+     */
+    private static void advertiseList(VContext baseContext, String listId, List<BlessingPattern>
+            patterns) {
+        if (baseContext.isCanceled()) {
+            Log.w("SHARING", "Base context was canceled; cannot advertise");
+            return;
+        }
+        // Swap out the ad context...
+        VContext oldAdContext = sAdContextMap.remove(listId);
+        if (oldAdContext != null) {
+            oldAdContext.cancel();
+        }
+        VContext newAdContext = baseContext.withCancel();
+        sAdContextMap.put(listId, newAdContext);
+
+
+        try {
+            Advertisement ad = new Advertisement();
+            ad.setInterfaceName(Sharing.getInvitationInterface());
+            ad.getAddresses().add(listId);
+            ad.getAttributes().put(OWNER_KEY, SyncbasePersistence.getPersonalBlessingsString());
+
+            // TODO(alexfandrianto): Remove hack. https://github.com/vanadium/issues/issues/1328
+            for (BlessingPattern pattern : patterns) {
+                ad.getAttributes().put(pattern.toString(), "");
+            }
+
+            Futures.addCallback(Sharing.getDiscovery().advertise(sAdvertiseContext, ad,
+                    // TODO(alexfandrianto): Crypto crash if I use patterns instead of null.
+                    // https://github.com/vanadium/issues/issues/1328 and
+                    // https://github.com/vanadium/issues/issues/1331
+                    null),
+                    //patterns),
+                    new FutureCallback<Void>() {
+                        @Override
+                        public void onSuccess(@android.support.annotation.Nullable Void result) {
+                        }
+
+                        @Override
+                        public void onFailure(@NonNull Throwable t) {
+                            handleAdvertiseListError(t);
+                        }
+                    });
+        } catch (VException e) {
+            handleAdvertiseListError(e);
+        }
+    }
+
+    private static void handleAdvertiseListError(Throwable t) {
+        SyncbasePersistence.getAppErrorReporter().onError(R.string.err_advertise_list, t);
+    }
 }
diff --git a/app/src/syncbase/res/values/strings.xml b/app/src/syncbase/res/values/strings.xml
index c4538d2..dbffb09 100644
--- a/app/src/syncbase/res/values/strings.xml
+++ b/app/src/syncbase/res/values/strings.xml
@@ -8,4 +8,6 @@
     <!-- Errors -->
     <string name="err_share_location">Could not share location</string>
     <string name="err_scan_nearby">Unable to scan for nearby users</string>
+    <string name="err_scan_lists">Unable to scan for todo lists shared with you</string>
+    <string name="err_advertise_list">Unable to send todo list invitation(s)</string>
 </resources>