TODOs: Wiring in neighborhood advertisement

Seems to work alright for me in the neighborhood.
- Not sure how to advertise globally though.
- We also have poorly formatted ads since we have no endpoints to
  expose.

Is related to https://vanadium-review.googlesource.com/#/c/22301/
except with some rebasing and comments addressed.

Change-Id: Ie2f66b6ebe81fb8a13015c2ae45e87561c578e9e
diff --git a/app/src/main/java/io/v/todos/TodoListActivity.java b/app/src/main/java/io/v/todos/TodoListActivity.java
index 01392fa..43b99ce 100644
--- a/app/src/main/java/io/v/todos/TodoListActivity.java
+++ b/app/src/main/java/io/v/todos/TodoListActivity.java
@@ -9,15 +9,10 @@
 import android.support.annotation.VisibleForTesting;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.helper.ItemTouchHelper;
-import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
 import io.v.todos.model.DataList;
 import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
@@ -40,7 +35,6 @@
  */
 public class TodoListActivity extends TodosAppActivity<TodoListPersistence, TaskRecyclerAdapter> {
     private ListSpec snackoo;
-    private List<String> mSharedTo = new ArrayList<>();
     private DataList<Task> snackoosList = new DataList<>();
 
     // The menu item that toggles whether done items are shown or not.
@@ -128,11 +122,6 @@
             }
 
             @Override
-            public void onShareChanged(List<String> sharedTo) {
-                mSharedTo = sharedTo;
-            }
-
-            @Override
             public void onItemAdd(Task item) {
                 int position = snackoosList.insertInOrder(item);
                 mAdapter.notifyItemInserted(position);
@@ -216,9 +205,8 @@
         // Handle action bar item clicks here. The action bar will
         // automatically handle clicks on the Home/Up button, so long
         // as you specify a parent activity in AndroidManifest.xml.
-        int id = item.getItemId();
 
-        switch (id) {
+        switch (item.getItemId()) {
             case R.id.show_done:
                 mPersistence.setShowDone(!item.isChecked());
                 return true;
@@ -228,29 +216,6 @@
             case R.id.action_debug:
                 sharePersistenceDebugDetails();
                 return true;
-            case R.id.action_share:
-                // TODO(alexfandrianto): We should figure out who is near us.
-                List<String> fakeNearby = new ArrayList<>();
-
-                // TODO(alexfandrianto): mSharedTo will not be live-updated, so the dialog can show
-                // stale shares. Perhaps this dialog should return a notifier object.
-                UIUtil.showShareDialog(this, mSharedTo, fakeNearby,
-                        new UIUtil.ShareDialogResponseListener() {
-                            @Override
-                            public void handleShareChanges(Set<String> emailsAdded, Set<String>
-                                    emailsRemoved) {
-                                Log.d("SHARE COMPLETE!", emailsAdded.toString() + emailsRemoved
-                                        .toString());
-                                if (emailsAdded.size() > 0) {
-                                    mPersistence.shareTodoList(emailsAdded);
-                                    // TODO(alexfandrianto): We may need to advertise this somehow.
-                                }
-                                // TODO(alexfandrianto): We can't actually handle removing
-                                // members yet, so it may be better to hide the ability to remove in
-                                // the share dialog.
-                            }
-                        });
-                return true;
         }
 
         return super.onOptionsItemSelected(item);
diff --git a/app/src/main/java/io/v/todos/UIUtil.java b/app/src/main/java/io/v/todos/UIUtil.java
index 645f357..7760ea9 100644
--- a/app/src/main/java/io/v/todos/UIUtil.java
+++ b/app/src/main/java/io/v/todos/UIUtil.java
@@ -100,143 +100,4 @@
         public void handleDelete() {
         }
     }
-
-    // The share dialog contains two recycler views. The top one shows the existing items, and the
-    // bottom shows ones that are nearby but not shared to yet. There's also a freeform text box
-    // to allow entry of any value.
-    // Confirming this dialog sends the set of added and removed emails. Tap to add/remove.
-    public static void showShareDialog(Context context, List<String> existing, List<String> nearby,
-                                       final ShareDialogResponseListener shareListener) {
-        final Set<String> emailsRemoved = new HashSet<>();
-        final Set<String> emailsAdded = new HashSet<>();
-        final List<String> manualEmailsTyped = new ArrayList<>();
-
-        View sharingView = View.inflate(context, R.layout.sharing, null);
-
-        final RecyclerView rvAlready = (RecyclerView) sharingView.findViewById(R.id
-                .recycler_already);
-        rvAlready.setAdapter(new ContactAdapter(existing, emailsRemoved, true));
-        final RecyclerView rvPossible = (RecyclerView) sharingView.findViewById(R.id
-                .recycler_possible);
-        rvPossible.setAdapter(new ContactAdapter(nearby, manualEmailsTyped, emailsAdded, false));
-
-        final EditText editText = (EditText) sharingView.findViewById(R.id.custom_email);
-        editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
-            @Override
-            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
-                boolean handled = false;
-                if (actionId == EditorInfo.IME_ACTION_SEND) {
-                    String email = editText.getText().toString();
-                    if (!manualEmailsTyped.contains(email)) {
-                        manualEmailsTyped.add(email);
-                    }
-                    emailsAdded.add(email);
-                    rvPossible.getAdapter().notifyDataSetChanged();
-                    editText.setText("");
-                    handled = true;
-                }
-                return handled;
-            }
-        });
-
-        new AlertDialog.Builder(context)
-                .setView(sharingView)
-                .setPositiveButton("Save", new DialogInterface.OnClickListener() {
-                    public void onClick(DialogInterface dialog, int whichButton) {
-                        shareListener.handleShareChanges(emailsAdded, emailsRemoved);
-                    }
-                })
-                .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
-                    public void onClick(DialogInterface dialog, int whichButton) {
-                    }
-                }).show();
-    }
-
-    public interface ShareDialogResponseListener {
-        void handleShareChanges(Set<String> emailsAdded, Set<String> emailsRemoved);
-    }
-
-    private static class ContactAdapter extends RecyclerView.Adapter<ContactViewHolder> {
-        private final List<String> backup;
-        private final List<String> bonus;
-        private final Set<String> toggledOn;
-        private final boolean strikethrough; // If false, then bold.
-
-        public ContactAdapter(List<String> backup, Set<String> toggledOn, boolean strikethrough) {
-            super();
-            this.backup = backup;
-            this.bonus = null;
-            this.toggledOn = toggledOn;
-            this.strikethrough = strikethrough;
-        }
-
-        public ContactAdapter(List<String> backup, List<String> bonus, Set<String> toggledOn,
-                              boolean strikethrough) {
-            super();
-            this.backup = backup;
-            this.bonus = bonus;
-            this.toggledOn = toggledOn;
-            this.strikethrough = strikethrough;
-        }
-
-        @Override
-        public ContactViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-            return new ContactViewHolder(new TextView(parent.getContext()));
-        }
-
-        @Override
-        public void onBindViewHolder(final ContactViewHolder holder, int position) {
-            final String name = position < backup.size() ? backup.get(position) :
-                    bonus.get(position - backup.size());
-            final boolean present = toggledOn.contains(name);
-            holder.bindString(name, present, strikethrough, new View.OnClickListener() {
-
-                @Override
-                public void onClick(View view) {
-                    if (present) {
-                        toggledOn.remove(name);
-                    } else {
-                        toggledOn.add(name);
-                    }
-                    notifyItemChanged(holder.getAdapterPosition());
-                }
-            });
-        }
-
-        @Override
-        public int getItemCount() {
-            int extra = bonus == null ? 0 : bonus.size();
-            return backup.size() + extra;
-        }
-    }
-
-    private static class ContactViewHolder extends RecyclerView.ViewHolder {
-        public ContactViewHolder(View itemView) {
-            super(itemView);
-        }
-
-        public void bindString(String name, boolean isActive, boolean strikethrough, View
-                .OnClickListener listener) {
-            TextView text = (TextView) itemView;
-
-            text.setText(name);
-            text.setTextSize(18);
-            if (strikethrough) {
-                if (isActive) {
-                    text.setPaintFlags(text.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
-                } else {
-                    text.setPaintFlags(text.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
-                }
-            } else {
-                // We should bold!
-                if (isActive) {
-                    text.setTypeface(null, 1); // 1 is bold
-                } else {
-                    text.setTypeface(null, 0); // 0 is default text style
-                }
-            }
-
-            text.setOnClickListener(listener);
-        }
-    }
 }
diff --git a/app/src/main/java/io/v/todos/persistence/TodoListListener.java b/app/src/main/java/io/v/todos/persistence/TodoListListener.java
index 069bd0e..fa9e355 100644
--- a/app/src/main/java/io/v/todos/persistence/TodoListListener.java
+++ b/app/src/main/java/io/v/todos/persistence/TodoListListener.java
@@ -4,8 +4,6 @@
 
 package io.v.todos.persistence;
 
-import java.util.List;
-
 import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
 
@@ -13,6 +11,4 @@
     void onUpdate(ListSpec value);
     void onDelete();
     void onUpdateShowDone(boolean showDone);
-
-    void onShareChanged(List<String> sharedTo);
 }
diff --git a/app/src/main/res/menu/menu_task.xml b/app/src/main/res/menu/menu_task.xml
index c405b5b..449ae38 100644
--- a/app/src/main/res/menu/menu_task.xml
+++ b/app/src/main/res/menu/menu_task.xml
@@ -13,11 +13,6 @@
         android:title="@string/action_edit"
         app:showAsAction="never" />
     <item
-        android:id="@+id/action_share"
-        android:orderInCategory="103"
-        android:title="@string/action_share"
-        app:showAsAction="never" />
-    <item
         android:id="@+id/action_debug"
         android:orderInCategory="104"
         android:title="@string/action_debug"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5c7280c..5b062a7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -11,10 +11,6 @@
     <string name="just_now">Just now</string>
     <string name="email_debug_subject">Debug the persistence layer</string>
     <string name="no_email_client">An app to share emails is not installed, cannot send debug request</string>
-    <!-- For Sharing Menu -->
-    <string name="sharing_already">Sharing With</string>
-    <string name="sharing_possible">Nearby</string>
-    <string name="sharing_custom_hint">Add an email address...</string>
     <!-- Errors -->
     <string name="err_init">Unable to initialize persistence</string>
     <string name="err_sync">Unable to sync</string>
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 d20c44e..ed6fd8f 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
@@ -84,31 +84,27 @@
                     mIdGenerator.registerId(change.getRowName().substring(LISTS_PREFIX.length()));
 
                     Log.d(TAG, "Found a list id from userdata watch: " + listId);
-                    Futures.catchingAsync(joinListSyncgroup(listId),
+                    trap(Futures.catchingAsync(joinListSyncgroup(listId),
                             SyncgroupJoinFailedException.class, new
                                     AsyncFunction<SyncgroupJoinFailedException, SyncgroupSpec>() {
-                                        public ListenableFuture<SyncgroupSpec> apply(@Nullable
-                                                                                     SyncgroupJoinFailedException
-                                                                                             input) throws
-                                                Exception {
-                                            Log.d(TAG, "Join failed. Sleeping and trying again: "
-                                                    + listId);
-                                            return sExecutor.schedule(new Callable<SyncgroupSpec>
-                                                    () {
+                        public ListenableFuture<SyncgroupSpec> apply(@Nullable
+                                                                     SyncgroupJoinFailedException
+                                                                             input) throws
+                                Exception {
+                            Log.d(TAG, "Join failed. Sleeping and trying again: " + listId);
+                            return sExecutor.schedule(new Callable<SyncgroupSpec>() {
 
-                                                @Override
-                                                public SyncgroupSpec call() throws Exception {
-                                                    Log.d(TAG, "Sleep done. Trying again: " +
-                                                            listId);
+                                @Override
+                                public SyncgroupSpec call() throws Exception {
+                                    Log.d(TAG, "Sleep done. Trying again: " + listId);
 
-                                                    // If this errors, then we will not get
-                                                    // another chance to see
-                                                    // this syncgroup until the app is restarted.
-                                                    return joinListSyncgroup(listId).get();
-                                                }
-                                            }, RETRY_DELAY, TimeUnit.MILLISECONDS);
-                                        }
-                                    });
+                                    // If this errors, then we will not get another chance to see
+                                    // this syncgroup until the app is restarted.
+                                    return joinListSyncgroup(listId).get();
+                                }
+                            }, RETRY_DELAY, TimeUnit.MILLISECONDS);
+                        }
+                    }));
 
                     MainListTracker listTracker = new MainListTracker(getVContext(), getDatabase(),
                             listId, listener);
@@ -120,18 +116,17 @@
                     }
 
                     // If the watch fails with NoExistException, the collection has been deleted.
-                    Futures.addCallback(listTracker.watchFuture,
-                            new TrappingCallback<Void>(getErrorReporter()) {
-                                @Override
-                                public void onFailure(@NonNull Throwable t) {
-                                    if (t instanceof NoExistException) {
-                                        // (this is idempotent)
-                                        trap(getUserCollection().delete(getVContext(), listId));
-                                    } else {
-                                        super.onFailure(t);
-                                    }
-                                }
-                            });
+                    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(), listId));
+                            } else {
+                                super.onFailure(t);
+                            }
+                        }
+                    });
                 }
                 return null;
             }
@@ -149,11 +144,11 @@
         // collections anyway. If https://github.com/vanadium/issues/issues/1326 is done, however,
         // we won't need to change this code.
         Futures.addCallback(listCollection.create(getVContext(), null),
-                new TrappingCallback<Void>(getErrorReporter()) {
+                new SyncTrappingCallback<Void>() {
                     @Override
                     public void onSuccess(@Nullable Void result) {
                         Futures.addCallback(createListSyncgroup(listCollection.id()),
-                                new TrappingCallback<Void>(getErrorReporter()) {
+                                new SyncTrappingCallback<Void>() {
                                     @Override
                                     public void onSuccess(@Nullable Void result) {
                                         // These can happen in either order.
@@ -171,7 +166,7 @@
     private ListenableFuture<SyncgroupSpec> joinListSyncgroup(String listId) {
         SyncgroupMemberInfo memberInfo = getDefaultMemberInfo();
         String sgName = computeListSyncgroupName(listId);
-        String blessingStr = getPersonalBlessingsString(getVContext());
+        String blessingStr = getPersonalBlessingsString();
         return getDatabase().getSyncgroup(new Id(blessingStr, sgName)).join(getVContext(),
                 CLOUD_NAME, CLOUD_BLESSING, memberInfo);
     }
@@ -180,7 +175,7 @@
         String listId = id.getName();
         final String sgName = computeListSyncgroupName(listId);
         Permissions permissions =
-                computePermissionsFromBlessings(getPersonalBlessings(getVContext()));
+                computePermissionsFromBlessings(getPersonalBlessings());
 
         SyncgroupMemberInfo memberInfo = getDefaultMemberInfo();
 
@@ -188,7 +183,7 @@
                 "TODOs User Data Collection", CLOUD_NAME, permissions,
                 ImmutableList.of(new CollectionRow(id, "")),
                 ImmutableList.of(MOUNTPOINT), false);
-        String blessingStr = getPersonalBlessingsString(getVContext());
+        String blessingStr = getPersonalBlessingsString();
         return getDatabase().getSyncgroup(new Id(blessingStr, sgName)).create(getVContext(),
                 spec, 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 df27fea..e0692f9 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
@@ -5,11 +5,13 @@
 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.NonNull;
+import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
 import android.util.Log;
 
 import com.google.common.base.Supplier;
@@ -17,7 +19,6 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningScheduledExecutorService;
@@ -29,22 +30,23 @@
 import org.joda.time.format.DateTimeFormat;
 
 import java.io.File;
+import java.util.concurrent.Callable;
 import java.util.concurrent.Executors;
-
-import javax.annotation.Nullable;
+import java.util.concurrent.Future;
 
 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.security.BlessingsManager;
 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.v23.OptionDefs;
-import io.v.v23.Options;
+import io.v.todos.sharing.Sharing;
 import io.v.v23.VFutures;
 import io.v.v23.context.VContext;
 import io.v.v23.rpc.Server;
@@ -64,7 +66,6 @@
 import io.v.v23.syncbase.SyncbaseService;
 import io.v.v23.syncbase.Syncgroup;
 import io.v.v23.vdl.VdlStruct;
-import io.v.v23.verror.CanceledException;
 import io.v.v23.verror.ExistException;
 import io.v.v23.verror.VException;
 import io.v.v23.vom.VomUtil;
@@ -107,11 +108,34 @@
     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 VContext sVContext;
     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 {
@@ -161,14 +185,10 @@
             throws SyncbaseServer.StartException, VException {
         synchronized (sSyncbaseMutex) {
             if (sSyncbase == null) {
-                final Context appContext = androidContext.getApplicationContext();
-                VContext singletonContext = V.init(appContext, new Options()
-                        .set(OptionDefs.LOG_VLEVEL, 0)
-                        .set(OptionDefs.LOG_VMODULE, "vsync*=0"));
-
+                VContext serverRun = getAppVContext().withCancel();
                 try {
                     // Retrieve this context's personal permissions to set ACLs on the server.
-                    Blessings personalBlessings = getPersonalBlessings(singletonContext);
+                    Blessings personalBlessings = getPersonalBlessings();
                     if (personalBlessings == null) {
                         throw new IllegalStateException("Blessings must be attached to the " +
                                 "Vanadium principal before Syncbase initialization.");
@@ -176,39 +196,44 @@
                     Permissions permissions = computePermissionsFromBlessings(personalBlessings);
 
                     sSyncbase = Syncbase.newService(startSyncbaseServer(
-                            singletonContext, appContext, permissions));
+                            serverRun, getAppContext(), permissions));
                 } catch (SyncbaseServer.StartException | RuntimeException e) {
-                    singletonContext.cancel();
+                    serverRun.cancel();
                     throw e;
                 }
-                sVContext = singletonContext;
             }
         }
     }
 
-    protected static Blessings getPersonalBlessings(VContext ctx) {
-        return V.getPrincipal(ctx).blessingStore().defaultBlessings();
+    // TODO(rosswang): Factor into v23
+    public static Blessings getPersonalBlessings() {
+        return V.getPrincipal(getAppVContext()).blessingStore().defaultBlessings();
     }
 
-    protected static String getPersonalBlessingsString(VContext ctx) {
-        return getPersonalBlessings(ctx).toString();
+    public static String getPersonalBlessingsString() {
+        return getPersonalBlessings().toString();
     }
 
-    protected static String getEmailFromBlessings(Blessings blessings) {
-        String[] split = blessings.toString().split(":");
+    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(":");
         return split[split.length - 1];
     }
 
-    protected static String getEmailFromPattern(BlessingPattern pattern) {
-        String[] split = pattern.toString().split(":");
-        return split[split.length - 1];
+    public static String getPersonalEmail() {
+        return getEmailFromBlessings(getPersonalBlessings());
     }
 
-    protected static String getPersonalEmail(VContext ctx) {
-        return getEmailFromBlessings(getPersonalBlessings(ctx));
-    }
-
-    protected static String blessingsStringFromEmail(String email) {
+    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_BLESSING_STRING + email;
@@ -232,10 +257,10 @@
     private static void ensureDatabaseExists() throws VException {
         synchronized (sDatabaseMutex) {
             if (sDatabase == null) {
-                final Database db = sSyncbase.getDatabase(sVContext, DATABASE, null);
+                final Database db = sSyncbase.getDatabase(getAppVContext(), DATABASE, null);
 
                 try {
-                    VFutures.sync(db.create(sVContext, null));
+                    VFutures.sync(db.create(getAppVContext(), null));
                 } catch (ExistException e) {
                     // This is fine.
                 }
@@ -245,15 +270,15 @@
     }
 
     private static final Object sUserCollectionMutex = new Object();
-    private static volatile Collection sUserCollection;
+    private static Collection sUserCollection;
 
     private static void ensureUserCollectionExists() throws VException {
         synchronized (sUserCollectionMutex) {
             if (sUserCollection == null) {
                 Collection userCollection = sDatabase.getCollection(
-                        new Id(getPersonalBlessingsString(sVContext), USER_COLLECTION_NAME));
+                        new Id(getPersonalBlessingsString(), USER_COLLECTION_NAME));
                 try {
-                    VFutures.sync(userCollection.create(sVContext, null));
+                    VFutures.sync(userCollection.create(getAppVContext(), null));
                 } catch (ExistException e) {
                     // This is fine.
                 }
@@ -263,17 +288,16 @@
     }
 
     private static final Object sCloudDatabaseMutex = new Object();
-    private static volatile Database sCloudDatabase;
-
+    private static Database sCloudDatabase;
 
     private static void ensureCloudDatabaseExists() {
         synchronized (sCloudDatabaseMutex) {
             if (sCloudDatabase == null) {
                 SyncbaseService cloudService = Syncbase.newService(CLOUD_NAME);
-                Database db = cloudService.getDatabase(sVContext, DATABASE, null);
+                Database db = cloudService.getDatabase(getAppVContext(), DATABASE, null);
                 try {
-                    VFutures.sync(db.create(sVContext.withTimeout(Duration.millis(SHORT_TIMEOUT))
-                            , null));
+                    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) {
@@ -285,12 +309,12 @@
     }
 
     private static final Object sUserSyncgroupMutex = new Object();
-    private static volatile Syncgroup sUserSyncgroup;
+    private static Syncgroup sUserSyncgroup;
 
     private static void ensureUserSyncgroupExists() throws VException {
         synchronized (sUserSyncgroupMutex) {
             if (sUserSyncgroup == null) {
-                Blessings clientBlessings = getPersonalBlessings(sVContext);
+                Blessings clientBlessings = getPersonalBlessings();
                 String email = getEmailFromBlessings(clientBlessings);
                 Log.d(TAG, email);
 
@@ -304,7 +328,8 @@
 
                 try {
                     Log.d(TAG, "Trying to join the syncgroup: " + sgName);
-                    VFutures.sync(sgHandle.join(sVContext, CLOUD_NAME, CLOUD_BLESSING, memberInfo));
+                    VFutures.sync(sgHandle.join(getAppVContext(), CLOUD_NAME, 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);
@@ -313,7 +338,7 @@
                             ImmutableList.of(new CollectionRow(sUserCollection.id(), "")),
                             ImmutableList.of(MOUNTPOINT), false);
                     try {
-                        VFutures.sync(sgHandle.create(sVContext, spec, memberInfo));
+                        VFutures.sync(sgHandle.create(getAppVContext(), spec, memberInfo));
                     } catch (BadAdvertisementException e2) {
                         Log.d(TAG, "Bad advertisement exception. Can we fix this?");
                     }
@@ -336,7 +361,7 @@
 
     }
 
-    protected String computeListSyncgroupName(String listId) {
+    protected static String computeListSyncgroupName(String listId) {
         return LIST_COLLECTION_SYNCGROUP_SUFFIX + listId;
     }
 
@@ -347,39 +372,6 @@
     }
 
     /**
-     * A {@link FutureCallback} that reports persistence errors by toasting a short message to the
-     * user and logging the exception trace and the call stack from where the future was invoked.
-     */
-    public static class TrappingCallback<T> implements FutureCallback<T> {
-        private static final int FIRST_SIGNIFICANT_STACK_ELEMENT = 3;
-        private final ErrorReporter mErrorReporter;
-        private final StackTraceElement[] mCaller;
-
-        public TrappingCallback(ErrorReporter errorReporter) {
-            mErrorReporter = errorReporter;
-            mCaller = Thread.currentThread().getStackTrace();
-        }
-
-        @Override
-        public void onSuccess(@Nullable T result) {
-        }
-
-        @Override
-        public void onFailure(@NonNull Throwable t) {
-            if (!(t instanceof CanceledException || t instanceof ExistException)) {
-                mErrorReporter.onError(R.string.err_sync, t);
-
-                StringBuilder traceBuilder = new StringBuilder(t.getMessage())
-                        .append("\n invoked at ").append(mCaller[FIRST_SIGNIFICANT_STACK_ELEMENT]);
-                for (int i = FIRST_SIGNIFICANT_STACK_ELEMENT + 1; i < mCaller.length; i++) {
-                    traceBuilder.append("\n\tat ").append(mCaller[i]);
-                }
-                Log.e(TAG, traceBuilder.toString());
-            }
-        }
-    }
-
-    /**
      * 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>
@@ -401,6 +393,12 @@
         }
     }
 
+    protected class SyncTrappingCallback<T> extends TrappingCallback<T> {
+        public SyncTrappingCallback() {
+            super(R.string.err_sync, TAG, getErrorReporter());
+        }
+    }
+
     private final VAndroidContext<Activity> mVAndroidContext;
 
     public VContext getVContext() {
@@ -428,11 +426,21 @@
      * @see TrappingCallback
      */
     protected void trap(ListenableFuture<?> future) {
-        Futures.addCallback(future, new TrappingCallback<>(getErrorReporter()));
+        Futures.addCallback(future, new SyncTrappingCallback<>());
     }
 
-    protected void addFeatureFragments(FragmentTransaction fragments) {
-        fragments.add(new NeighborhoodFragment(), NeighborhoodFragment.FRAGMENT_TAG);
+    /**
+     * 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);
+        }
     }
 
     /**
@@ -442,10 +450,13 @@
             throws VException, SyncbaseServer.StartException {
         mVAndroidContext = VAndroidContexts.withDefaults(activity, savedInstanceState);
 
+        FragmentManager mgr = activity.getFragmentManager();
         if (savedInstanceState == null) {
-            FragmentTransaction fragments = activity.getFragmentManager().beginTransaction();
-            addFeatureFragments(fragments);
-            fragments.commit();
+            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
@@ -466,11 +477,26 @@
         }
 
         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 Future<?> ensureCloudDatabaseExists = sExecutor.submit(new Runnable() {
+            @Override
+            public void run() {
+                ensureCloudDatabaseExists();
+            }
+        });
         ensureSyncbaseStarted(activity);
         ensureDatabaseExists();
         ensureUserCollectionExists();
-        ensureCloudDatabaseExists();
+        VFutures.sync(ensureCloudDatabaseExists); // must finish before syncgroup setup
         ensureUserSyncgroupExists();
+        VFutures.sync(initDiscovery);
         sInitialized = true;
     }
 
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 f735fc4..6fad570 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
@@ -5,6 +5,8 @@
 package io.v.todos.persistence.syncbase;
 
 import android.app.Activity;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.util.Log;
@@ -29,6 +31,7 @@
 import io.v.todos.model.TaskSpec;
 import io.v.todos.persistence.TodoListListener;
 import io.v.todos.persistence.TodoListPersistence;
+import io.v.todos.sharing.ShareListMenuFragment;
 import io.v.v23.InputChannel;
 import io.v.v23.InputChannelCallback;
 import io.v.v23.InputChannels;
@@ -65,6 +68,20 @@
     private final IdGenerator mIdGenerator = new IdGenerator(IdAlphabets.ROW_NAME, true);
     private final Set<String> mTaskIds = new HashSet<>();
     private final Timer mMemberTimer;
+    private ShareListMenuFragment mShareListMenuFragment;
+
+    @Override
+    protected void addFeatureFragments(FragmentManager manager, FragmentTransaction transaction) {
+        super.addFeatureFragments(manager, transaction);
+        if (transaction == null) {
+            mShareListMenuFragment = ShareListMenuFragment.find(manager);
+        } else {
+            mShareListMenuFragment = new ShareListMenuFragment();
+            transaction.add(mShareListMenuFragment, ShareListMenuFragment.FRAGMENT_TAG);
+        }
+        mShareListMenuFragment.persistence = this;
+        mShareListMenuFragment.setEmail(getPersonalEmail());
+    }
 
     /**
      * This assumes that the collection for this list already exists.
@@ -85,7 +102,7 @@
                         return null;
                     }
                 });
-        Futures.addCallback(listWatchFuture, new TrappingCallback<Void>(getErrorReporter()) {
+        Futures.addCallback(listWatchFuture, new SyncTrappingCallback<Void>() {
             @Override
             public void onFailure(@NonNull Throwable t) {
                 if (t instanceof NoExistException) {
@@ -128,7 +145,7 @@
 
                 // Analyze these patterns to construct the emails, and fire the listener!
                 List<String> emails = parseEmailsFromPatterns(patterns);
-                mListener.onShareChanged(emails);
+                mShareListMenuFragment.setSharedTo(emails);
             }
         }, MEMBER_TIMER_DELAY, MEMBER_TIMER_PERIOD);
 
@@ -153,7 +170,7 @@
                 // Skip. It's the cloud, and that doesn't count.
                 continue;
             }
-            if (pattern.toString().endsWith(getPersonalEmail(getVContext()))) {
+            if (pattern.toString().endsWith(getPersonalEmail())) {
                 // Skip. It's you, and that doesn't count.
                 continue;
             }
@@ -205,7 +222,7 @@
     }
 
     private Syncgroup getListSyncgroup() {
-        return getDatabase().getSyncgroup(new Id(getPersonalBlessingsString(getVContext()),
+        return getDatabase().getSyncgroup(new Id(getPersonalBlessingsString(),
                 computeListSyncgroupName(mList.id().getName())));
     }
 
@@ -236,7 +253,7 @@
                 // Analyze these patterns to construct the emails, and fire the listener!
                 List<String> specEmails = parseEmailsFromPatterns(
                         perms.get(Constants.READ.getValue()).getIn());
-                mListener.onShareChanged(specEmails);
+                mShareListMenuFragment.setSharedTo(specEmails);
 
                 // Add read and write access to the collection permissions.
                 perms = VFutures.sync(mList.getPermissions(getVContext()));
@@ -251,6 +268,8 @@
 
     // TODO(alexfandrianto): We should consider moving this helper into the main Java repo.
     // https://github.com/vanadium/issues/issues/1321
+    // TODO(alexfandrianto): This allows you to repeatedly add the same blessings to the permission
+    // multiple times.
     private static void addPermissions(Permissions perms, Iterable<String> emails, String tag) {
         AccessList acl = perms.get(tag);
         List<BlessingPattern> patterns = acl.getIn();
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/TrappingCallback.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/TrappingCallback.java
new file mode 100644
index 0000000..969bbbc
--- /dev/null
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/TrappingCallback.java
@@ -0,0 +1,57 @@
+// 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.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import javax.annotation.Nullable;
+
+import io.v.android.error.ErrorReporter;
+import io.v.v23.verror.CanceledException;
+import io.v.v23.verror.ExistException;
+
+/**
+ * A {@link FutureCallback} that reports persistence errors by toasting a short message to the
+ * user and logging the exception trace and the call stack from where the future was invoked.
+ *
+ * TODO(rosswang): Factor into V23.
+ */
+public class TrappingCallback<T> implements FutureCallback<T> {
+    private static final int FIRST_SIGNIFICANT_STACK_ELEMENT = 3;
+    private final @StringRes int mFailureMessage;
+    private final String mLogTag;
+    private final ErrorReporter mErrorReporter;
+    private final StackTraceElement[] mCaller;
+
+    public TrappingCallback(@StringRes int failureMessage, String logTag,
+                            ErrorReporter errorReporter) {
+        mFailureMessage = failureMessage;
+        mLogTag = logTag;
+        mErrorReporter = errorReporter;
+        mCaller = Thread.currentThread().getStackTrace();
+    }
+
+    @Override
+    public void onSuccess(@Nullable T result) {
+    }
+
+    @Override
+    public void onFailure(@NonNull Throwable t) {
+        if (!(t instanceof CanceledException || t instanceof ExistException)) {
+            mErrorReporter.onError(mFailureMessage, t);
+
+            StringBuilder traceBuilder = new StringBuilder(t.getMessage())
+                    .append("\n invoked at ").append(mCaller[FIRST_SIGNIFICANT_STACK_ELEMENT]);
+            for (int i = FIRST_SIGNIFICANT_STACK_ELEMENT + 1; i < mCaller.length; i++) {
+                traceBuilder.append("\n\tat ").append(mCaller[i]);
+            }
+            Log.e(mLogTag, traceBuilder.toString());
+        }
+    }
+}
diff --git a/app/src/syncbase/java/io/v/todos/sharing/NeighborhoodFragment.java b/app/src/syncbase/java/io/v/todos/sharing/NeighborhoodFragment.java
index c7ab727..a7c478e 100644
--- a/app/src/syncbase/java/io/v/todos/sharing/NeighborhoodFragment.java
+++ b/app/src/syncbase/java/io/v/todos/sharing/NeighborhoodFragment.java
@@ -5,55 +5,156 @@
 import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+
 import io.v.todos.R;
+import io.v.todos.persistence.syncbase.SyncbasePersistence;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Advertisement;
+import io.v.v23.verror.VException;
 
 /**
  * A fragment encapsulating menu options and functionality related to list sharing.
  */
-public class NeighborhoodFragment extends Fragment {
-    public static final String FRAGMENT_TAG = NeighborhoodFragment.class.getSimpleName();
+public class NeighborhoodFragment extends Fragment
+        implements SharedPreferences.OnSharedPreferenceChangeListener {
+    public static final String
+            FRAGMENT_TAG = NeighborhoodFragment.class.getSimpleName();
 
     private static final String PREF_ADVERTISE_NEIGHBORHOOD = "advertise neighborhood";
 
-    private SharedPreferences mPrefs;
+    private static SharedPreferences sPrefs;
+    private static VContext sAdvertiseContext;
+    private static Advertisement sAd;
 
-    private boolean isAdvertising() {
-        return mPrefs.getBoolean(PREF_ADVERTISE_NEIGHBORHOOD, false);
+    // This has to be a field because registerOnSharedPreferenceChangeListener does not keep a hard
+    // reference to the listener, making it otherwise susceptible to garbage collection.
+    private static final SharedPreferences.OnSharedPreferenceChangeListener sSharedPrefListener =
+            new SharedPreferences.OnSharedPreferenceChangeListener() {
+                @Override
+                public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+                                                      String key) {
+                    if (PREF_ADVERTISE_NEIGHBORHOOD.equals(key)) {
+                        updateSharePresence();
+                    }
+                }
+            };
+
+    /**
+     * Initializes advertisement of one's presence to the neighborhood and watches shared
+     * preferences to toggle sharing on and off as the preference is changed.
+     */
+    public static void initSharePresence() {
+        sPrefs = sPrefs != null ? sPrefs : PreferenceManager.getDefaultSharedPreferences
+                (SyncbasePersistence.getAppContext());
+
+        sAd = new Advertisement();
+        sAd.setInterfaceName(Sharing.getPresenceInterface());
+        // TODO(alexfandrianto): Revisit why we must put an address inside the advertisement.
+        // If we are advertising our presence, then there isn't any need for it.
+        // For now, put our email address in the addresses, despite it being an attribute.
+        sAd.getAddresses().add(SyncbasePersistence.getPersonalEmail());
+
+        sPrefs.registerOnSharedPreferenceChangeListener(sSharedPrefListener);
+        updateSharePresence();
     }
 
+    private static void updateSharePresence() {
+        if (isAdvertising()) {
+            if (sAdvertiseContext == null) {
+                sAdvertiseContext = SyncbasePersistence.getAppVContext().withCancel();
+                try {
+                    Futures.addCallback(Sharing.getDiscovery().advertise(sAdvertiseContext, sAd,
+                            // TODO(rosswang): Restrict to contacts only. However, per discussion
+                            // with mattr@ and ashankar@, this could be a scalability challenge as
+                            // the advertisement would need to be IB-encrypted for each possible
+                            // recipient. This can be broken into multiple advertisements but at the
+                            // increased risk of over-the-air hash collision.
+                            null),
+                            new FutureCallback<Void>() {
+                                @Override
+                                public void onSuccess(@Nullable Void result) {
+                                }
+
+                                @Override
+                                public void onFailure(@NonNull Throwable t) {
+                                    handleAdvertisingError(t);
+                                }
+                            });
+                } catch (VException e) {
+                    handleAdvertisingError(e);
+                }
+            }
+        } else if (sAdvertiseContext != null) {
+            sAdvertiseContext.cancel();
+            sAdvertiseContext = null;
+        }
+    }
+
+    private static void handleAdvertisingError(Throwable t) {
+        SyncbasePersistence.getAppErrorReporter().onError(R.string.err_share_location, t);
+        setAdvertiseNeighborhood(false);
+    }
+
+    private static void setAdvertiseNeighborhood(boolean value) {
+        sPrefs.edit()
+                .putBoolean(PREF_ADVERTISE_NEIGHBORHOOD, value)
+                .apply();
+    }
+
+    private static boolean isAdvertising() {
+        return sPrefs.getBoolean(PREF_ADVERTISE_NEIGHBORHOOD, false);
+    }
+
+    private MenuItem mAdvertiseNeighborhoodMenuItem;
+
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setHasOptionsMenu(true);
-
-        mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
     }
 
     @Override
     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
-        inflater.inflate(R.menu.sharing, menu);
-        setAdvertiseNeighborhoodChecked(menu.findItem(R.id.advertise_neighborhood), isAdvertising());
+        inflater.inflate(R.menu.neighborhood, menu);
+        mAdvertiseNeighborhoodMenuItem = menu.findItem(R.id.advertise_neighborhood);
+        sPrefs = sPrefs != null ? sPrefs : PreferenceManager.getDefaultSharedPreferences
+                (getActivity());
+        sPrefs.registerOnSharedPreferenceChangeListener(this);
+        updateAdvertiseNeighborhoodChecked();
     }
 
-    private void setAdvertiseNeighborhoodChecked(MenuItem menuItem, boolean value) {
-        menuItem.setChecked(value);
-        menuItem.setIcon(value ? R.drawable.ic_advertise_neighborhood_on_white_24dp :
+    @Override
+    public void onDestroyOptionsMenu() {
+        sPrefs.unregisterOnSharedPreferenceChangeListener(this);
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+        if (PREF_ADVERTISE_NEIGHBORHOOD.equals(key)) {
+            updateAdvertiseNeighborhoodChecked();
+        }
+    }
+
+    private void updateAdvertiseNeighborhoodChecked() {
+        boolean value = isAdvertising();
+        mAdvertiseNeighborhoodMenuItem.setChecked(value);
+        mAdvertiseNeighborhoodMenuItem.setIcon(value ?
+                R.drawable.ic_advertise_neighborhood_on_white_24dp :
                 R.drawable.ic_advertise_neighborhood_off_white_24dp);
     }
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         if (item.getItemId() == R.id.advertise_neighborhood) {
-            boolean advertiseNeighborhood = !item.isChecked();
-            mPrefs.edit()
-                    .putBoolean(PREF_ADVERTISE_NEIGHBORHOOD, advertiseNeighborhood)
-                    .apply();
-            setAdvertiseNeighborhoodChecked(item, advertiseNeighborhood);
+            setAdvertiseNeighborhood(!item.isChecked());
             return true;
         } else {
             return super.onOptionsItemSelected(item);
diff --git a/app/src/syncbase/java/io/v/todos/sharing/ShareListDialogFragment.java b/app/src/syncbase/java/io/v/todos/sharing/ShareListDialogFragment.java
new file mode 100644
index 0000000..e380e05
--- /dev/null
+++ b/app/src/syncbase/java/io/v/todos/sharing/ShareListDialogFragment.java
@@ -0,0 +1,296 @@
+// 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.sharing;
+
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.DialogInterface;
+import android.graphics.Paint;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.RecyclerView;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+
+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.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+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.Update;
+import io.v.v23.verror.VException;
+
+/**
+ * The share dialog contains two recycler views. The top one shows the existing items, and the
+ * bottom shows ones that are nearby but not shared to yet. There's also a freeform text box to
+ * allow entry of any value. Confirming this dialog sends the set of added and removed emails. Tap
+ * to add/remove.
+ */
+public class ShareListDialogFragment extends DialogFragment {
+    public static final String FRAGMENT_TAG = ShareListDialogFragment.class.getSimpleName();
+
+    public static ShareListDialogFragment find(FragmentManager fragmentManager) {
+        return (ShareListDialogFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
+    }
+
+    private Set<String> mRemoved;
+    private List<String> mNearby = new ArrayList<>();
+    private ArrayList<String> mTyped;
+    private Set<String> mAdded;
+
+    private static final String
+            REMOVED_KEY = "removedShares",
+            TYPED_KEY = "explicitShares",
+            ADDED_KEY = "addedShares";
+
+    private ContactAdapter mAlreadyAdapter, mPossibleAdapter;
+
+    private VContext mScanContext;
+
+    private ShareListMenuFragment getParent() {
+        return ((ShareListMenuFragment) getParentFragment());
+    }
+
+    @Override
+    public void onDestroyView() {
+        mScanContext.cancel();
+        super.onDestroyView();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        outState.putStringArrayList(REMOVED_KEY, new ArrayList<>(mRemoved));
+        outState.putStringArrayList(TYPED_KEY, mTyped);
+        outState.putStringArrayList(ADDED_KEY, new ArrayList<>(mAdded));
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        View view = getActivity().getLayoutInflater().inflate(R.layout.sharing, null);
+
+        if (savedInstanceState == null) {
+            mRemoved = new HashSet<>();
+            mAdded = new HashSet<>();
+            mTyped = new ArrayList<>();
+        } else {
+            mRemoved = new HashSet<>(savedInstanceState.getStringArrayList(REMOVED_KEY));
+            mAdded = new HashSet<>(savedInstanceState.getStringArrayList(ADDED_KEY));
+            mTyped = savedInstanceState.getStringArrayList(TYPED_KEY);
+        }
+
+        mAlreadyAdapter = new ContactAdapter(getParent().getSharedTo(), mRemoved, true);
+        RecyclerView rvAlready = (RecyclerView) view.findViewById(R.id.recycler_already);
+        rvAlready.setAdapter(mAlreadyAdapter);
+        mNearby.clear();
+        final RecyclerView rvPossible = (RecyclerView) view.findViewById(R.id.recycler_possible);
+        mPossibleAdapter = new ContactAdapter(mNearby, mTyped, mAdded, false);
+        rvPossible.setAdapter(mPossibleAdapter);
+
+        mScanContext = SyncbasePersistence.getAppVContext().withCancel();
+        try {
+            ListenableFuture<Void> scan = InputChannels.withCallback(
+                    Sharing.getDiscovery().scan(mScanContext,
+                            "v.InterfaceName = \"" + Sharing.getPresenceInterface() + "\""),
+                    new InputChannelCallback<Update>() {
+                        private final Map<String, Integer> counterMap = new HashMap<>();
+
+                        @Override
+                        public ListenableFuture<Void> onNext(Update result) {
+                            final String email = Iterables.getOnlyElement(result.getAddresses());
+                            if (email == null) {
+                                return null;
+                            }
+                            // Note: binarySearch returns -|correct insert index| - 1 if it fails
+                            // to find a match. For Java ints, this is the bitwise complement of the
+                            // "correct" insertion index.
+                            int searchIndex = Collections.binarySearch(mNearby, email);
+                            if (result.isLost()) {
+                                Integer old = counterMap.get(email);
+                                counterMap.put(email, old == null ? 0 : Math.max(0, counterMap
+                                        .get(email) - 1));
+                                // Remove the email if the counter indicates that we should.
+                                if (counterMap.get(email) == 0 && searchIndex >= 0) {
+                                    mNearby.remove(searchIndex);
+                                    mPossibleAdapter.notifyItemRemoved(searchIndex);
+                                }
+                            } else {
+                                Integer old = counterMap.get(email);
+                                counterMap.put(email, old == null ? 1 : counterMap.get(email) + 1);
+                                // Show the email if it's a new one and not equal to our email.
+                                // TODO(alexfandrianto): This still lets you see emails of those
+                                // nearby who you've already invited.
+                                if (searchIndex < 0 && !email.equals(getParent().getEmail())) {
+                                    int insertIndex = ~searchIndex;
+                                    mNearby.add(insertIndex, email);
+                                    //mNearby.add(email);
+                                    mPossibleAdapter.notifyItemInserted(insertIndex);
+                                }
+                            }
+                            return null;
+                        }
+                    });
+            Futures.addCallback(scan, new FutureCallback<Void>() {
+                @Override
+                public void onSuccess(@Nullable Void result) {
+                }
+
+                @Override
+                public void onFailure(Throwable t) {
+                    handleScanningError(t);
+                }
+            });
+        } catch (VException e) {
+            handleScanningError(e);
+        }
+
+        final EditText editText = (EditText) view.findViewById(R.id.custom_email);
+        editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+            @Override
+            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+                boolean handled = false;
+                if (actionId == EditorInfo.IME_ACTION_SEND) {
+                    String email = editText.getText().toString();
+                    if (!mTyped.contains(email)) {
+                        mTyped.add(email);
+                    }
+                    mAdded.add(email);
+                    rvPossible.getAdapter().notifyDataSetChanged();
+                    editText.setText("");
+                    handled = true;
+                }
+                return handled;
+            }
+        });
+
+        return new AlertDialog.Builder(getActivity())
+                .setView(view)
+                .setPositiveButton("Save", new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                        getParent().persistence.shareTodoList(mAdded);
+                        // TODO(alexfandrianto/rosswang): removal
+                    }
+                })
+                .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                    }
+                })
+                .create();
+    }
+
+    private void handleScanningError(Throwable t) {
+        //TODO(rosswang): indicate error in the view
+        SyncbasePersistence.getAppErrorReporter().onError(R.string.err_scan_nearby, t);
+    }
+
+    public void onSharedToChanged() {
+        //TODO(rosswang)
+    }
+
+    private static class ContactAdapter extends RecyclerView.Adapter<ContactViewHolder> {
+        private final List<String> backup;
+        private final List<String> bonus;
+        private final Set<String> toggledOn;
+        private final boolean strikethrough; // If false, then bold.
+
+        public ContactAdapter(List<String> backup, Set<String> toggledOn, boolean strikethrough) {
+            super();
+            this.backup = backup;
+            this.bonus = null;
+            this.toggledOn = toggledOn;
+            this.strikethrough = strikethrough;
+        }
+
+        public ContactAdapter(List<String> backup, List<String> bonus, Set<String> toggledOn,
+                              boolean strikethrough) {
+            super();
+            this.backup = backup;
+            this.bonus = bonus;
+            this.toggledOn = toggledOn;
+            this.strikethrough = strikethrough;
+        }
+
+        @Override
+        public ContactViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            return new ContactViewHolder(new TextView(parent.getContext()));
+        }
+
+        @Override
+        public void onBindViewHolder(final ContactViewHolder holder, int position) {
+            final String name = position < backup.size() ? backup.get(position) :
+                    bonus.get(position - backup.size());
+            final boolean present = toggledOn.contains(name);
+            holder.bindString(name, present, strikethrough, new View.OnClickListener() {
+
+                @Override
+                public void onClick(View view) {
+                    if (present) {
+                        toggledOn.remove(name);
+                    } else {
+                        toggledOn.add(name);
+                    }
+                    notifyItemChanged(holder.getAdapterPosition());
+                }
+            });
+        }
+
+        @Override
+        public int getItemCount() {
+            int extra = bonus == null ? 0 : bonus.size();
+            return backup.size() + extra;
+        }
+    }
+
+    private static class ContactViewHolder extends RecyclerView.ViewHolder {
+        public ContactViewHolder(View itemView) {
+            super(itemView);
+        }
+
+        public void bindString(String name, boolean isActive, boolean strikethrough, View
+                .OnClickListener listener) {
+            TextView text = (TextView) itemView;
+
+            text.setText(name);
+            text.setTextSize(18);
+            if (strikethrough) {
+                if (isActive) {
+                    text.setPaintFlags(text.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
+                } else {
+                    text.setPaintFlags(text.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
+                }
+            } else {
+                // We should bold!
+                if (isActive) {
+                    text.setTypeface(null, 1); // 1 is bold
+                } else {
+                    text.setTypeface(null, 0); // 0 is default text style
+                }
+            }
+
+            text.setOnClickListener(listener);
+        }
+    }
+}
diff --git a/app/src/syncbase/java/io/v/todos/sharing/ShareListMenuFragment.java b/app/src/syncbase/java/io/v/todos/sharing/ShareListMenuFragment.java
new file mode 100644
index 0000000..70cc498
--- /dev/null
+++ b/app/src/syncbase/java/io/v/todos/sharing/ShareListMenuFragment.java
@@ -0,0 +1,79 @@
+// 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.sharing;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.todos.R;
+import io.v.todos.persistence.syncbase.SyncbaseTodoList;
+
+/**
+ * In addition to providing the menu item, this class brokers interaction between
+ * {@link SyncbaseTodoList} and {@link ShareListDialogFragment}.
+ */
+public class ShareListMenuFragment extends Fragment {
+    public static final String FRAGMENT_TAG = ShareListMenuFragment.class.getSimpleName();
+
+    public static ShareListMenuFragment find(FragmentManager mgr) {
+        return (ShareListMenuFragment) mgr.findFragmentByTag(FRAGMENT_TAG);
+    }
+
+    public SyncbaseTodoList persistence;
+
+    private List<String> mSharedTo = new ArrayList<>();
+
+    public void setSharedTo(List<String> sharedTo) {
+        mSharedTo = sharedTo;
+
+        ShareListDialogFragment dialog = ShareListDialogFragment.find(getChildFragmentManager());
+        if (dialog != null) {
+            dialog.onSharedToChanged();
+        }
+    }
+
+    public List<String> getSharedTo() {
+        return mSharedTo;
+    }
+
+    private String mEmail;
+    public void setEmail(String email) {
+        mEmail = email;
+    }
+    public String getEmail() {
+        return mEmail;
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        inflater.inflate(R.menu.menu_share, menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.action_share:
+                new ShareListDialogFragment()
+                        .show(getChildFragmentManager(), ShareListDialogFragment.FRAGMENT_TAG);
+                return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+}
diff --git a/app/src/syncbase/java/io/v/todos/sharing/Sharing.java b/app/src/syncbase/java/io/v/todos/sharing/Sharing.java
new file mode 100644
index 0000000..73bc757
--- /dev/null
+++ b/app/src/syncbase/java/io/v/todos/sharing/Sharing.java
@@ -0,0 +1,44 @@
+// 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.sharing;
+
+import io.v.android.v23.V;
+import io.v.todos.persistence.syncbase.SyncbasePersistence;
+import io.v.v23.discovery.Discovery;
+import io.v.v23.verror.VException;
+
+public final class Sharing {
+    private Sharing(){}
+
+    private static final Object sDiscoveryMutex = new Object();
+    private static Discovery sDiscovery;
+
+    public static Discovery getDiscovery() {
+        return sDiscovery;
+    }
+
+    public static void initDiscovery() throws VException {
+        synchronized (sDiscoveryMutex) {
+            if (sDiscovery == null) {
+                sDiscovery = V.newDiscovery(SyncbasePersistence.getAppVContext());
+
+                // Rely on the neighborhood fragment to initialize presence advertisement.
+                NeighborhoodFragment.initSharePresence();
+            }
+        }
+    }
+
+    private static String getRootInterface() {
+        return SyncbasePersistence.getAppContext().getPackageName();
+    }
+
+    public static String getPresenceInterface() {
+        return getRootInterface() + ".presence";
+    }
+
+    public static String getInvitationInterface() {
+        return getRootInterface() + ".invitation";
+    }
+}
diff --git a/app/src/main/res/layout/sharing.xml b/app/src/syncbase/res/layout/sharing.xml
similarity index 100%
rename from app/src/main/res/layout/sharing.xml
rename to app/src/syncbase/res/layout/sharing.xml
diff --git a/app/src/syncbase/res/menu/menu_share.xml b/app/src/syncbase/res/menu/menu_share.xml
new file mode 100644
index 0000000..f74bd86
--- /dev/null
+++ b/app/src/syncbase/res/menu/menu_share.xml
@@ -0,0 +1,10 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:context="io.v.todos.TodoListActivity">
+    <item
+        android:id="@+id/action_share"
+        android:orderInCategory="103"
+        android:title="@string/action_share"
+        app:showAsAction="never" />
+</menu>
diff --git a/app/src/syncbase/res/menu/sharing.xml b/app/src/syncbase/res/menu/neighborhood.xml
similarity index 100%
rename from app/src/syncbase/res/menu/sharing.xml
rename to app/src/syncbase/res/menu/neighborhood.xml
diff --git a/app/src/syncbase/res/values/strings.xml b/app/src/syncbase/res/values/strings.xml
index d6d2a51..c4538d2 100644
--- a/app/src/syncbase/res/values/strings.xml
+++ b/app/src/syncbase/res/values/strings.xml
@@ -1,4 +1,11 @@
 <resources>
     <string name="app_name">Syncbase Todos</string>
     <string name="share_location">Share Location</string>
+    <!-- For Sharing Menu -->
+    <string name="sharing_already">Sharing With</string>
+    <string name="sharing_possible">Nearby</string>
+    <string name="sharing_custom_hint">Add an email address...</string>
+    <!-- Errors -->
+    <string name="err_share_location">Could not share location</string>
+    <string name="err_scan_nearby">Unable to scan for nearby users</string>
 </resources>