TODOs: 2.1.5 High-level API and Restore Sharing

Major changes:
- Uses the high-level API in a synchronous manner.
- Restores the Sharing assets.
- Sharing was tested, watch was tested.
- Login helper is being used.
- Ensure lollipop is ok by adding permissions to Manifest.

Notes:
- Still crashes randomly. Callbacks have a problem.
- Discovery isn't that great. Sometimes some peers don't see others.
- There are a lot of TODOs needed still! The HLAPI has some
  catching up to do before I can finish them.

I would prefer to submit this bulk change sooner rather than later
though, or else it'll just get larger and larger.

Change-Id: Id8a8216fc5196cfcd37604829c07469bb5b16f94
diff --git a/app/build.gradle b/app/build.gradle
index 31ff599..85e4d95 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -83,5 +83,5 @@
     )
     firebaseCompile 'com.firebase:firebase-client-android:2.5.2'
     syncbaseCompile 'io.v:vanadium-android:2.2.+'
-    syncbase2Compile 'io.v:syncbase:0.1.4'
+    syncbase2Compile 'io.v:syncbase:0.1.5'
 }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 274a641..8c96879 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,12 @@
     package="io.v.todos">
 
     <uses-permission android:name="android.permission.INTERNET" />
+    <!-- TODO(alexfandrianto): We should include these permissions in syncbase instead -->
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
+                     android:maxSdkVersion="22"  />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
+                     android:maxSdkVersion="22"  />
+
 
     <application
         android:allowBackup="true"
diff --git a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
index aeeab28..5359f7e 100644
--- a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
+++ b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
@@ -6,12 +6,15 @@
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.util.Log;
 
 import java.util.UUID;
 
 import io.v.syncbase.Collection;
 import io.v.syncbase.DatabaseHandle;
 import io.v.syncbase.Id;
+import io.v.syncbase.Syncbase;
+import io.v.syncbase.core.VError;
 import io.v.todos.model.ListMetadata;
 import io.v.todos.model.ListSpec;
 import io.v.todos.persistence.ListEventListener;
@@ -35,27 +38,37 @@
 
     @Override
     public String addTodoList(ListSpec listSpec) {
-        // TODO(alexfandrianto): We do want to create this with a syncgroup, but even if we set
-        // the flag to off, it takes too long to create (and put) on the UI thread. To work around
-        // this, we might mock the encoded Id synchronously and then do creation/put asynchronously.
         DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions();
-        opts.withoutSyncgroup = true;
-        Collection c = mDb.collection(UUID.randomUUID().toString(), opts);
-        c.put(TODO_LIST_KEY, listSpec);
-        return c.getId().encode();
+        try {
+            // TODO(alexfandrianto): We're not allowed to have dashes in our collection names still!
+            // You also must start with a letter, not a number.
+            Collection c = sDb.collection("list_" + UUID.randomUUID().toString().replaceAll("-", ""), opts);
+            c.put(TODO_LIST_KEY, listSpec);
+            return c.getId().encode();
+        } catch (VError vError) {
+            Log.e(TAG, "Failed to create todo list collection", vError);
+            throw new RuntimeException(vError);
+        }
     }
 
     @Override
     public void deleteTodoList(String key) {
         Id listId = Id.decode(key);
-        Collection c = mDb.getCollection(listId);
-        c.delete(TODO_LIST_KEY);
-        // TODO(alexfandrianto): Instead of deleting the key, should we destroy the collection?
+        Collection c = sDb.getCollection(listId);
+        try {
+            c.delete(TODO_LIST_KEY);
+        } catch (VError vError) {
+            Log.e(TAG, "Failed to delete todo list key", vError);
+        }
+        // TODO(alexfandrianto): Instead of deleting the key, we should destroy the collection.
+        // Unfortunately, I can't yet: https://v.io/i/1374
     }
 
     @Override
     public void close() {
         removeMainListener();
-        super.close();
+        if (isInitialized()) {
+            Syncbase.shutdown();
+        }
     }
 }
diff --git a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
index 475054e..72affe4 100644
--- a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
+++ b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
@@ -5,26 +5,27 @@
 package io.v.todos.persistence.syncbase;
 
 import android.app.Activity;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
 import android.os.Bundle;
-import android.os.Handler;
+import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
 import android.util.Log;
 
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.SettableFuture;
-
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 
-import io.v.android.VAndroidContexts;
-import io.v.android.security.BlessingsManager;
 import io.v.syncbase.Collection;
 import io.v.syncbase.Database;
 import io.v.syncbase.Syncbase;
 import io.v.syncbase.Id;
 
+import io.v.syncbase.Syncgroup;
+import io.v.syncbase.SyncgroupInvite;
 import io.v.syncbase.WatchChange;
+import io.v.syncbase.core.VError;
 import io.v.todos.model.ListMetadata;
 import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
@@ -32,19 +33,15 @@
 import io.v.todos.persistence.ListEventListener;
 import io.v.todos.persistence.Persistence;
 import io.v.todos.persistence.TodoListListener;
-import io.v.v23.VFutures;
-import io.v.v23.context.VContext;
-import io.v.v23.security.Blessings;
-import io.v.v23.verror.VException;
+import io.v.todos.sharing.NeighborhoodFragment;
+import io.v.todos.sharing.ShareListDialogFragment;
 
-public class SyncbasePersistence implements Persistence {
+public abstract class SyncbasePersistence implements Persistence {
     protected static final String SETTINGS_COLLECTION = "settings";
     protected static final String SHOW_DONE_KEY = "showDoneKey";
     protected static final String TODO_LIST_KEY = "todoListKey";
     protected static final String TAG = "High-Level Syncbase";
 
-    private static final String BLESSINGS_KEY = "blessings";
-
     protected static boolean sInitialized = false;
 
     protected static final Map<Id, ListSpec> sListSpecMap = new HashMap<>();
@@ -52,14 +49,16 @@
     protected static final Map<Id, Map<String, TaskSpec>> sTasksByListMap = new HashMap<>();
     protected static boolean sShowDone = true;
 
-    protected Database mDb;
-    protected Collection mSettings;
+    protected static Database sDb;
+    protected static Collection sSettings;
 
     private static final Object sSyncbaseMutex = new Object();
-    private TodoListListener mTodoListListener;
-    private ListEventListener<ListMetadata> mMainListener;
+    private static TodoListListener sTodoListListener;
+    private static Id sTodoListExpectedId;
+    private static ListEventListener<ListMetadata> sMainListener;
 
-    public SyncbasePersistence(final Activity activity, Bundle savedInstanceState) {
+    SyncbasePersistence(final Activity activity, Bundle savedInstanceState) {
+        Log.d(TAG, "Trying to start Syncbase Persistence...");
         /**
          * Initializes Syncbase Server
          * Starts up a watch stream to watch all the data with methods to access/modify the data.
@@ -70,67 +69,54 @@
             if (!sInitialized) {
                 Log.d(TAG, "Initializing Syncbase Persistence...");
 
-                Syncbase.DatabaseOptions dbOpts = new Syncbase.DatabaseOptions();
-                dbOpts.rootDir = activity.getFilesDir().getAbsolutePath();
-                dbOpts.disableUserdataSyncgroup = true;
-                dbOpts.vContext = VAndroidContexts.withDefaults(activity,
-                        savedInstanceState).getVContext();
+                Syncbase.Options opts = new Syncbase.Options();
+                opts.rootDir = activity.getFilesDir().getAbsolutePath();
+                opts.disableSyncgroupPublishing = true;
+                // TODO(alexfandrianto): https://v.io/i/1375
+                opts.disableUserdataSyncgroup = true;
+                try {
+                    Syncbase.init(opts);
+                } catch (VError vError) {
+                    Log.e(TAG, "Failed to initialize", vError);
+                    return;
+                }
 
-                final VContext vContext = dbOpts.vContext;
+                final Object initializeMutex = new Object();
 
-                Log.d(TAG, "Done getting vanadium context!");
-
-                final SettableFuture<ListenableFuture<Blessings>> blessings =
-                        SettableFuture.create();
-                if (activity.getMainLooper().getThread() == Thread.currentThread()) {
-                    blessings.set(BlessingsManager.getBlessings(vContext, activity,
-                            BLESSINGS_KEY, true));
-                } else {
-                    new Handler(activity.getMainLooper()).post(new Runnable() {
+                Log.d(TAG, "Logging the user in!");
+                if (!Syncbase.isLoggedIn()) {
+                    Syncbase.loginAndroid(activity, new Syncbase.LoginCallback() {
                         @Override
-                        public void run() {
-                            blessings.set(BlessingsManager.getBlessings(vContext,
-                                    activity, BLESSINGS_KEY, true));
+                        public void onSuccess() {
+                            Log.d(TAG, "Successfully logged in!");
+                            try {
+                                sDb = Syncbase.database();
+                            } catch (VError vError) {
+                                Log.e(TAG, "Failed to create database", vError);
+                                callNotify();
+                                return;
+                            }
+                            continueSetup();
+                            sInitialized = true;
+                            Log.d(TAG, "Successfully initialized!");
+                            callNotify();
+                        }
+
+                        @Override
+                        public void onError(Throwable e) {
+                            Log.e(TAG, "Failed to login. :(", e);
+                            callNotify();
+                        }
+
+                        private void callNotify() {
+                            synchronized (initializeMutex) {
+                                initializeMutex.notify();
+                            }
                         }
                     });
                 }
 
-                try {
-                    VFutures.sync(Futures.dereference(blessings));
-                } catch (VException e) {
-                    Log.e(TAG, "Failed to get blessings", e);
-                }
-
-                Log.d(TAG, "Done getting blessings!");
-
-                final Object initializeMutex = new Object();
-
-                Syncbase.database(new Syncbase.DatabaseCallback() {
-                    @Override
-                    public void onSuccess(Database db) {
-                        super.onSuccess(db);
-                        Log.d(TAG, "Got a db handle!");
-                        mDb = db;
-                        continueSetup();
-                        sInitialized = true;
-                        Log.d(TAG, "Successfully initialized!");
-                        synchronized (initializeMutex) {
-                            initializeMutex.notify();
-                        }
-                    }
-
-                    @Override
-                    public void onError(Throwable e) {
-                        super.onError(e);
-
-                        Log.e(TAG, "Failed to get database handle", e);
-                        synchronized (initializeMutex) {
-                            initializeMutex.notify();
-                        }
-                    }
-                }, dbOpts);
-
-                Log.d(TAG, "Let's wait until the database is ready...");
+                Log.d(TAG, "Let's wait until we are logged in...");
                 synchronized (initializeMutex) {
                     try {
                         initializeMutex.wait();
@@ -139,28 +125,66 @@
                     }
                 }
 
-                Log.d(TAG, "Syncbase Persistence initialization complete!");
+                if (sInitialized) {
+                    Log.d(TAG, "Syncbase Persistence initialization complete!");
+                } else {
+                    Log.d(TAG, "Syncbase Persistence initialization FAILED!");
+                    return;
+                }
             }
         }
+
+        // Prepare the share presence menu fragment.
+        FragmentManager mgr = activity.getFragmentManager();
+        if (savedInstanceState == null) {
+            FragmentTransaction t = mgr.beginTransaction();
+            addFeatureFragments(mgr, activity, t);
+            t.commit();
+        } else {
+            addFeatureFragments(mgr, activity, null);
+        }
+    }
+
+    /**
+     * 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, Context context,
+                                       @Nullable FragmentTransaction transaction) {
+        if (transaction != null) {
+            NeighborhoodFragment fragment = new NeighborhoodFragment();
+            fragment.initSharePresence(context);
+            transaction.add(fragment, NeighborhoodFragment.FRAGMENT_TAG);
+        }
     }
 
     private void continueSetup() {
         Log.d(TAG, "Creating settings collection");
         // Create a settings collection.
-        mSettings = mDb.collection(SETTINGS_COLLECTION);
+        try {
+            sSettings = sDb.collection(SETTINGS_COLLECTION);
+        } catch (VError vError) {
+            Log.e(TAG, "couldn't create settings collection", vError);
+        }
 
         Log.d(TAG, "Watching everything");
         // Watch everything.
-        mDb.addWatchChangeHandler(new Database.WatchChangeHandler() {
+        // TODO(alexfandrianto): This can be simplified if we watch specific collections and the
+        // entrance/exit of collections. https://v.io/i/1376
+        sDb.addWatchChangeHandler(new Database.WatchChangeHandler() {
             @Override
-            public void onInitialState(Iterator<WatchChange> values) {
+            public void onInitialState(final Iterator<WatchChange> values) {
                 while (values.hasNext()) {
                     handlePutChange(values.next());
                 }
             }
 
             @Override
-            public void onChangeBatch(Iterator<WatchChange> changes) {
+            public void onChangeBatch(final Iterator<WatchChange> changes) {
                 while (changes.hasNext()) {
                     WatchChange change = changes.next();
                     if (change.getChangeType() == WatchChange.ChangeType.DELETE) {
@@ -175,56 +199,84 @@
             // being within a batch. Over-firing the listeners isn't ideal, but the app should be
             // okay.
             private void handlePutChange(WatchChange value) {
-                Id collectionId = value.getCollectionId();
+                Log.d(TAG, "Handling put change " + value.getRowKey());
+                Log.d(TAG, "From collection: " + value.getCollectionId());
+                Log.d(TAG, "With entity type: " + value.getEntityType());
+                if (value.getEntityType() != WatchChange.EntityType.ROW ||
+                        value.getCollectionId().getName().equals("userdata__")) {
+                    // TODO(alexfandrianto): I can't deal with these yet. Please skip to avoid crashing.
+                    // TODO(alexfandrianto): export/hide userdata__ https://v.io/i/1372
+                    return;
+                }
+                Log.d(TAG, "With row...: " + value.getRowKey());
+                final Id collectionId = value.getCollectionId();
 
                 if (collectionId.getName().equals(SETTINGS_COLLECTION)) {
                     if (value.getRowKey().equals(SHOW_DONE_KEY)) {
-                        sShowDone = (Boolean)value.getValue();
+                        try {
+                            sShowDone = value.getValue(Boolean.class);
+                            Log.d(TAG, "Got a show done" + sShowDone);
 
-                        // Inform the relevant listener.
-                        if (mTodoListListener != null) {
-                            mTodoListListener.onUpdateShowDone(sShowDone);
+                            // Inform the relevant listener.
+                            if (sTodoListListener != null) {
+                                sTodoListListener.onUpdateShowDone(sShowDone);
+                            }
+                        } catch (VError vError) {
+                            Log.e(TAG, "Failed to decode watch change as Boolean", vError);
                         }
                     }
+                    return; // Show done updated. Nothing left to do.
                 }
 
+                // If we are here, we must be modifying a todo list collection.
                 // Initialize the task spec map, if necessary.
                 if (sTasksByListMap.get(collectionId) == null) {
                     sTasksByListMap.put(collectionId, new HashMap<String, TaskSpec>());
                 }
 
                 if (value.getRowKey().equals(TODO_LIST_KEY)) {
-                    ListSpec listSpec = (ListSpec) value.getValue();
-                    sListSpecMap.put(collectionId, listSpec);
+                    try {
+                        final ListSpec listSpec = value.getValue(ListSpec.class);
+                        Log.d(TAG, "Got a list" + listSpec.toString());
+                        sListSpecMap.put(collectionId, listSpec);
 
-                    ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
-                    tracker.setSpec(listSpec);
+                        final ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
+                        tracker.setSpec(listSpec);
 
-                    // Inform the relevant listeners.
-                    if (mMainListener != null) {
-                        tracker.fireListener(mMainListener);
-                    }
-                    if (mTodoListListener != null) {
-                        mTodoListListener.onUpdate(listSpec);
+                        // Inform the relevant listeners.
+                        if (sMainListener != null) {
+                            tracker.fireListener(sMainListener);
+                        }
+                        if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
+                            sTodoListListener.onUpdate(listSpec);
+                        }
+                    } catch (VError vError) {
+                        Log.e(TAG, "Failed to decode watch change value as ListSpec", vError);
                     }
                 } else {
                     Map<String, TaskSpec> taskData = sTasksByListMap.get(collectionId);
-                    TaskSpec newSpec = (TaskSpec)value.getValue();
-                    TaskSpec oldSpec = taskData.put(value.getRowKey(), newSpec);
+                    final String rowKey = value.getRowKey();
+                    try {
+                        final TaskSpec newSpec = value.getValue(TaskSpec.class);
+                        Log.d(TAG, "Got a task" + newSpec.toString());
+                        final TaskSpec oldSpec = taskData.put(rowKey, newSpec);
 
-                    ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
-                    tracker.adjustTask(value.getRowKey(), newSpec.getDone());
+                        final ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
+                        tracker.adjustTask(rowKey, newSpec.getDone());
 
-                    // Inform the relevant listeners.
-                    if (mMainListener != null) {
-                        tracker.fireListener(mMainListener);
-                    }
-                    if (mTodoListListener != null) {
-                        if (oldSpec == null) {
-                            mTodoListListener.onItemAdd(new Task(value.getRowKey(), newSpec));
-                        } else {
-                            mTodoListListener.onItemUpdate(new Task(value.getRowKey(), newSpec));
+                        // Inform the relevant listeners.
+                        if (sMainListener != null) {
+                            tracker.fireListener(sMainListener);
                         }
+                        if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
+                            if (oldSpec == null) {
+                                sTodoListListener.onItemAdd(new Task(rowKey, newSpec));
+                            } else {
+                                sTodoListListener.onItemUpdate(new Task(rowKey, newSpec));
+                            }
+                        }
+                    } catch (VError vError) {
+                        Log.e(TAG, "Failed to decode watch change value as TaskSpec", vError);
                     }
                 }
             }
@@ -233,35 +285,44 @@
             // being within a batch. Over-firing the listeners isn't ideal, but the app should be
             // okay.
             private void handleDeleteChange(WatchChange value) {
-                Id collectionId = value.getCollectionId();
-                String oldKey = value.getRowKey();
+                Log.d(TAG, "Handling delete change " + value.getRowKey());
+                Log.d(TAG, "From collection: " + value.getCollectionId());
+                Log.d(TAG, "With entity type: " + value.getEntityType());
+                if (value.getEntityType() != WatchChange.EntityType.ROW || value.getCollectionId().getName().equals("userdata__")) {
+                    // TODO(alexfandrianto): I can't deal with these yet. Please skip to avoid crashing.
+                    // TODO(alexfandrianto): export/hide userdata__ https://v.io/i/1372
+                    return;
+                }
+                Log.d(TAG, "With row...: " + value.getRowKey());
+
+                final Id collectionId = value.getCollectionId();
+                final String oldKey = value.getRowKey();
                 if (oldKey.equals(TODO_LIST_KEY)) {
                     sListSpecMap.remove(collectionId);
                     sListMetadataTrackerMap.remove(collectionId);
 
                     // TODO(alexfandrianto): Potentially destroy the collection too?
-
                     // Inform the relevant listeners.
-                    if (mMainListener != null) {
-                        mMainListener.onItemDelete(oldKey);
+                    if (sMainListener != null) {
+                        sMainListener.onItemDelete(collectionId.encode());
                     }
-                    if (mTodoListListener != null) {
-                        mTodoListListener.onDelete();
+                    if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
+                        sTodoListListener.onDelete();
                     }
                 } else {
                     Map<String, TaskSpec> tasks = sTasksByListMap.get(collectionId);
                     if (tasks != null) {
                         tasks.remove(oldKey);
 
-                        ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
+                        final ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
                         tracker.removeTask(oldKey);
 
                         // Inform the relevant listeners.
-                        if (mMainListener != null) {
-                            tracker.fireListener(mMainListener);
+                        if (sMainListener != null) {
+                            tracker.fireListener(sMainListener);
                         }
-                        if (mTodoListListener != null) {
-                            mTodoListListener.onItemDelete(value.getRowKey());
+                        if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
+                            sTodoListListener.onItemDelete(oldKey);
                         }
                     }
                 }
@@ -276,20 +337,17 @@
         Log.d(TAG, "Accepting all invitations");
 
         // Automatically accept invitations.
-        // TODO(alexfandrianto): Uncomment. This part of the high-level API isn't implemented yet.
-        /*mDb.addSyncgroupInviteHandler(new Database.SyncgroupInviteHandler() {
+        sDb.addSyncgroupInviteHandler(new Database.SyncgroupInviteHandler() {
             @Override
             public void onInvite(SyncgroupInvite invite) {
-                mDb.acceptSyncgroupInvite(invite, new Database.AcceptSyncgroupInviteCallback() {
+                sDb.acceptSyncgroupInvite(invite, new Database.AcceptSyncgroupInviteCallback() {
                     @Override
                     public void onSuccess(Syncgroup sg) {
-                        super.onSuccess(sg);
                         Log.d(TAG, "Successfully joined syncgroup: " + sg.getId().toString());
                     }
 
                     @Override
                     public void onFailure(Throwable e) {
-                        super.onFailure(e);
                         Log.w(TAG, "Failed to accept invitation", e);
                     }
                 });
@@ -299,7 +357,10 @@
             public void onError(Throwable e) {
                 Log.w(TAG, "error while handling invitations", e);
             }
-        }, new Database.AddSyncgroupInviteHandlerOptions());*/
+        }, new Database.AddSyncgroupInviteHandlerOptions());
+
+        // And do a background scan for peers near me.
+        ShareListDialogFragment.initScan();
     }
 
     public static boolean isInitialized() {
@@ -307,9 +368,7 @@
     }
 
     @Override
-    public void close() {
-
-    }
+    public abstract void close();
 
     @Override
     public String debugDetails() {
@@ -317,17 +376,18 @@
     }
 
     protected void setMainListener(ListEventListener<ListMetadata> listener) {
-        mMainListener = listener;
+        sMainListener = listener;
     }
     protected void removeMainListener() {
-        mMainListener = null;
+        sMainListener = null;
     }
 
-    protected void setTodoListListener(TodoListListener listener) {
-        mTodoListListener = listener;
+    protected void setTodoListListener(TodoListListener listener, Id expectedId) {
+        sTodoListListener = listener;
+        sTodoListExpectedId = expectedId;
     }
     protected void removeTodoListListener() {
-        mTodoListListener = null;
+        sTodoListListener = null;
     }
 
     private ListMetadataTracker getListMetadataTrackerSafe(Id listId) {
@@ -351,6 +411,9 @@
         }
 
         ListMetadata computeListMetadata() {
+            if (spec == null) {
+                return null;
+            }
             return new ListMetadata(collectionId.encode(), spec, numCompleted,
                     taskCompletion.size());
         }
@@ -376,10 +439,14 @@
         }
 
         void fireListener(ListEventListener<ListMetadata> listener) {
+            ListMetadata metadata = computeListMetadata();
+            if (metadata == null) {
+                return; // cannot fire yet
+            }
             if (!hasFired) {
+                hasFired = true;
                 listener.onItemAdd(computeListMetadata());
             } else {
-                hasFired = true;
                 listener.onItemUpdate(computeListMetadata());
             }
         }
diff --git a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
index ba6aa17..556ecda 100644
--- a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
+++ b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
@@ -5,30 +5,57 @@
 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.util.Log;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 
+import io.v.syncbase.AccessList;
 import io.v.syncbase.BatchDatabase;
 import io.v.syncbase.Collection;
 import io.v.syncbase.Database;
 import io.v.syncbase.Id;
+import io.v.syncbase.User;
+import io.v.syncbase.core.VError;
 import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
 import io.v.todos.model.TaskSpec;
 import io.v.todos.persistence.TodoListListener;
 import io.v.todos.persistence.TodoListPersistence;
+import io.v.todos.sharing.ShareListMenuFragment;
 
 public class SyncbaseTodoList extends SyncbasePersistence implements TodoListPersistence {
     private Collection mCollection;
+    private ShareListMenuFragment mShareListMenuFragment;
+
+    @Override
+    protected void addFeatureFragments(FragmentManager manager, Context context,
+                                       FragmentTransaction transaction) {
+        super.addFeatureFragments(manager, context, transaction);
+        if (transaction == null) {
+            mShareListMenuFragment = ShareListMenuFragment.find(manager);
+        } else {
+            mShareListMenuFragment = new ShareListMenuFragment();
+            transaction.add(mShareListMenuFragment, ShareListMenuFragment.FRAGMENT_TAG);
+        }
+        mShareListMenuFragment.persistence = this;
+        // 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.)
+    }
 
     public SyncbaseTodoList(Activity activity, Bundle savedInstanceState, String key,
                             TodoListListener listener) {
         super(activity, savedInstanceState);
 
         Id listId = Id.decode(key);
-        mCollection = mDb.getCollection(listId);
+        mCollection = sDb.getCollection(listId);
 
         // Fire the listener for existing data (list, tasks, show done status).
         ListSpec currentList = sListSpecMap.get(listId);
@@ -44,59 +71,114 @@
         listener.onUpdateShowDone(sShowDone);
 
         // Register the listener for future updates.
-        setTodoListListener(listener);
+        setTodoListListener(listener, listId);
+
+        // TODO(alexfandrianto): Do we want this behavior? We need getLoggedInUser() if we do.
+        // if (!listId.getBlessing().equals(getPersonalBlessingsString())) {
+        //     mShareListMenuFragment.hideShareMenuItem();
+        // }
+
+        // TODO(alexfandrianto): We also have to watch who the collection has been shared to!
+        // mShareListMenuFragment.setSharedTo needs to happen!!!
     }
 
     @Override
     public void updateTodoList(ListSpec listSpec) {
-        mCollection.put(TODO_LIST_KEY, listSpec);
+        try {
+            mCollection.put(TODO_LIST_KEY, listSpec);
+        } catch (VError vError) {
+            Log.w(TAG, vError);
+        }
     }
 
     @Override
     public void deleteTodoList() {
-        mCollection.delete(TODO_LIST_KEY);
+        try {
+            mCollection.delete(TODO_LIST_KEY);
+        } catch (VError vError) {
+            Log.w(TAG, vError);
+        }
     }
 
     @Override
     public void completeTodoList() {
-        mDb.runInBatch(new Database.BatchOperation() {
-            @Override
-            public void run(BatchDatabase bDb) {
-                Collection bCollection = bDb.getCollection(mCollection.getId());
-                Map<String, TaskSpec> curTasks = sTasksByListMap.get(mCollection.getId());
-                for (Map.Entry<String, TaskSpec> entry : curTasks.entrySet()) {
-                    String rowKey = entry.getKey();
-                    TaskSpec curSpec= entry.getValue();
-                    TaskSpec newSpec = new TaskSpec(curSpec.getText(), curSpec.getAddedAt(), true);
-                    bCollection.put(rowKey, newSpec);
+        // TODO(alexfandrianto): All this try catch is getting excessive.
+        try {
+            sDb.runInBatch(new Database.BatchOperation() {
+                @Override
+                public void run(BatchDatabase bDb) {
+                    Collection bCollection = bDb.getCollection(mCollection.getId());
+                    Map<String, TaskSpec> curTasks = sTasksByListMap.get(mCollection.getId());
+                    for (Map.Entry<String, TaskSpec> entry : curTasks.entrySet()) {
+                        String rowKey = entry.getKey();
+                        TaskSpec curSpec = entry.getValue();
+                        TaskSpec newSpec = new TaskSpec(curSpec.getText(), curSpec.getAddedAt(),
+                                true);
+
+                        // TODO(alexfandrianto): If we're in a batch, it's okay to error, isn't it?
+                        try {
+                            bCollection.put(rowKey, newSpec);
+                        } catch (VError vError) {
+                            Log.w(TAG, vError);
+                        }
+                    }
                 }
-            }
-        }, new Database.BatchOptions());
+            }, new Database.BatchOptions());
+        } catch (VError vError) {
+            Log.w(TAG, vError);
+        }
     }
 
     @Override
     public void addTask(TaskSpec task) {
-        mCollection.put(UUID.randomUUID().toString(), task);
+        try {
+            mCollection.put(UUID.randomUUID().toString(), task);
+        } catch (VError vError) {
+            Log.w(TAG, vError);
+        }
     }
 
     @Override
     public void updateTask(Task task) {
-        mCollection.put(task.key, task.toSpec());
+        try {
+            mCollection.put(task.key, task.toSpec());
+        } catch (VError vError) {
+            Log.w(TAG, vError);
+        }
     }
 
     @Override
     public void deleteTask(String key) {
-        mCollection.delete(key);
+        try {
+            mCollection.delete(key);
+        } catch (VError vError) {
+            Log.w(TAG, vError);
+        }
     }
 
     @Override
     public void setShowDone(boolean showDone) {
-        mSettings.put(SHOW_DONE_KEY, showDone);
+        try {
+            sSettings.put(SHOW_DONE_KEY, showDone);
+        } catch (VError vError) {
+            Log.w(TAG, vError);
+        }
     }
 
     @Override
     public void close() {
         removeTodoListListener();
-        super.close();
+    }
+
+    public void shareTodoList(Set<String> aliases) {
+        List<User> users = new ArrayList<User>();
+        for (String alias : aliases) {
+            users.add(new User(alias));
+        }
+        try {
+            mCollection.getSyncgroup().inviteUsers(users, AccessList.AccessLevel.READ_WRITE);
+        } catch (VError vError) {
+            Log.w(TAG, "Could not share to: " + users.toString(), vError);
+        }
     }
 }
diff --git a/app/src/syncbase2/java/io/v/todos/sharing/ContactAdapter.java b/app/src/syncbase2/java/io/v/todos/sharing/ContactAdapter.java
new file mode 100644
index 0000000..2bab5c9
--- /dev/null
+++ b/app/src/syncbase2/java/io/v/todos/sharing/ContactAdapter.java
@@ -0,0 +1,311 @@
+// 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.support.annotation.StringRes;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import io.v.todos.R;
+
+public class ContactAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+    public static final int
+            VIEW_TYPE_SUBHEADER = 0,
+            VIEW_TYPE_CONTACT = 1;
+
+    public interface ContactTouchListener {
+        void onContactTouch(RecyclerView.ViewHolder viewHolder);
+    }
+
+    private static class SubheaderViewHolder extends RecyclerView.ViewHolder {
+        public final TextView category;
+
+        public SubheaderViewHolder(ViewGroup parent) {
+            super(LayoutInflater.from(parent.getContext())
+                    .inflate(R.layout.sharing_subheader, parent, false));
+            category = (TextView) itemView.findViewById(R.id.category);
+        }
+    }
+
+    private static class ContactViewHolder extends RecyclerView.ViewHolder {
+        public final TextView name;
+
+        public ContactViewHolder(ViewGroup parent) {
+            super(LayoutInflater.from(parent.getContext())
+                    .inflate(R.layout.sharing_entry, parent, false));
+            name = (TextView) itemView.findViewById(R.id.name);
+        }
+    }
+
+    // TODO(rosswang): Save expanded state, and/or manage more intelligently.
+    // TODO(rosswang): Potentially show condensed avatars in collapsed state.
+    private static class Sublist {
+        @StringRes
+        public final int header;
+        public final List<String> items = new ArrayList<>();
+
+        public Sublist(@StringRes int header) {
+            this.header = header;
+        }
+
+        public int getEffectiveSize() {
+            return 1 + items.size();
+        }
+    }
+
+    private final Sublist
+            mSharingAlready = new Sublist(R.string.sharing_already),
+            mSharingPossible = new Sublist(R.string.sharing_possible);
+
+    private final Sublist[] mSections = new Sublist[]{
+            mSharingAlready,
+            mSharingPossible
+    };
+
+    private Collection<String> mSharedAlready;
+    private final Set<String> mSharesAdded, mSharesRemoved, mSharesRecent;
+    private final Multiset<String> mDiscoCounter = HashMultiset.create();
+    private ContactTouchListener mContactTouchListener;
+
+    public ContactAdapter(Collection<String> sharedAlready, Set<String> sharesAdded,
+                          Set<String> sharesRemoved, Set<String> sharesRecent) {
+        mSharesAdded = sharesAdded;
+        mSharesRemoved = sharesRemoved;
+        mSharesRecent = sharesRecent;
+
+        Set<String> recentUnaccountedFor = new HashSet<>(sharesRecent);
+        recentUnaccountedFor.removeAll(sharedAlready);
+        recentUnaccountedFor.removeAll(sharesAdded);
+        recentUnaccountedFor.removeAll(sharesRemoved);
+        mSharingPossible.items.addAll(recentUnaccountedFor);
+        mSharingPossible.items.addAll(sharesRemoved);
+        Collections.sort(mSharingPossible.items);
+
+        setSharedAlreadyData(sharedAlready);
+    }
+
+    private void setSharedAlreadyData(Collection<String> sharedAlready) {
+        mSharedAlready = sharedAlready;
+        mSharingAlready.items.clear();
+        mSharingAlready.items.addAll(sharedAlready);
+        mSharingAlready.items.removeAll(mSharesRemoved);
+        mSharingAlready.items.addAll(mSharesAdded);
+        Collections.sort(mSharingAlready.items);
+    }
+
+    public void filterDeltas() {
+        mSharesAdded.removeAll(mSharedAlready);
+        mSharesRemoved.retainAll(mSharedAlready);
+    }
+
+    private static class SublistEntry {
+        public final Sublist sublist;
+        public final int itemPosition;
+        public final String item;
+
+        public SublistEntry(Sublist sublist, int itemPosition, String item) {
+            this.sublist = sublist;
+            this.itemPosition = itemPosition;
+            this.item = item;
+        }
+    }
+
+    private SublistEntry getSublistItemPosition(int position) {
+        for (Sublist section : mSections) {
+            if (position == 0) {
+                return new SublistEntry(section, -1, null);
+            }
+            position--;
+            if (position < section.items.size()) {
+                return new SublistEntry(section, position, section.items.get(position));
+            }
+            position -= section.items.size();
+        }
+        throw new IndexOutOfBoundsException("No sublist at position " + position);
+    }
+
+    /**
+     * Inverse of {@link #getSublistItemPosition(int)}.
+     */
+    private int getViewPosition(Sublist section, int itemPosition) {
+        int offset = 1;
+        for (Sublist cursor: mSections) {
+            if (cursor == section) {
+                return offset + itemPosition;
+            } else {
+                offset += cursor.getEffectiveSize();
+            }
+        }
+        throw new NoSuchElementException("Section is not in list");
+    }
+
+    @Override
+    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        if (viewType == VIEW_TYPE_SUBHEADER) {
+            return new SubheaderViewHolder(parent);
+        } else {
+            final ContactViewHolder cvh = new ContactViewHolder(parent);
+            cvh.itemView.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mContactTouchListener.onContactTouch(cvh);
+                }
+            });
+            return cvh;
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
+        final SublistEntry entry = getSublistItemPosition(position);
+        if (entry.item == null) {
+            final SubheaderViewHolder svh = (SubheaderViewHolder) holder;
+            svh.category.setText(entry.sublist.header);
+        } else {
+            final ContactViewHolder cvh = (ContactViewHolder) holder;
+            cvh.name.setText(entry.item);
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        int count = 0;
+        for (Sublist section : mSections) {
+            count += section.getEffectiveSize();
+        }
+        return count;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        return getSublistItemPosition(position).item == null ?
+                VIEW_TYPE_SUBHEADER : VIEW_TYPE_CONTACT;
+    }
+
+    public void setContactTouchListener(ContactTouchListener listener) {
+        mContactTouchListener = listener;
+    }
+
+    public void onNearbyDeviceLost(String email) {
+        // short-circuit note; remove must happen
+        if (mDiscoCounter.remove(email, 1) > 1 ||
+                Collections.binarySearch(mSharingAlready.items, email) >= 0) {
+            return;
+        }
+        int i = Collections.binarySearch(mSharingPossible.items, email);
+        if (i >= 0) {
+            mSharingPossible.items.remove(i);
+            notifyItemRemoved(getViewPosition(mSharingPossible, i));
+        }
+    }
+
+    public void onNearbyDeviceDiscovered(String email) {
+        // short-circuit note; add must happen
+        if (mDiscoCounter.add(email, 1) > 0 ||
+                Collections.binarySearch(mSharingAlready.items, email) >= 0) {
+            return;
+        }
+        int i = Collections.binarySearch(mSharingPossible.items, email);
+        if (i < 0) {
+            i = ~i;
+            mSharingPossible.items.add(i, email);
+            notifyItemInserted(getViewPosition(mSharingPossible, i));
+        }
+    }
+
+    private int shareWithPossible(int i) {
+        String email = mSharingPossible.items.remove(i);
+        // Animate movement by moving the item.
+        int j = ~Collections.binarySearch(mSharingAlready.items, email);
+        int oldPosition = getViewPosition(mSharingPossible, i);
+        int newPosition = getViewPosition(mSharingAlready, j);
+        mSharingAlready.items.add(j, email);
+        notifyItemMoved(oldPosition, newPosition);
+        registerShare(email);
+        return newPosition;
+    }
+
+    private void unshare(int i) {
+        String email = mSharingAlready.items.remove(i);
+        int j = ~Collections.binarySearch(mSharingPossible.items, email);
+        mSharingPossible.items.add(j, email);
+        registerUnshare(email);
+
+        // animate movement
+        int oldPosition = getViewPosition(mSharingAlready, i);
+        int newPosition = getViewPosition(mSharingPossible, j);
+        notifyItemMoved(oldPosition, newPosition);
+    }
+
+    private int insertShare(String email) {
+        int i = Collections.binarySearch(mSharingAlready.items, email);
+        if (i >= 0) {
+            return getViewPosition(mSharingAlready, i);
+        } else {
+            i = ~i;
+            mSharingAlready.items.add(i, email);
+            i = getViewPosition(mSharingAlready, i);
+            notifyItemInserted(i);
+            registerShare(email);
+            return i;
+        }
+    }
+
+    private void registerShare(String email) {
+        mSharesRecent.add(email);
+        if (!mSharesRemoved.remove(email)) {
+            mSharesAdded.add(email);
+        }
+    }
+
+    private void registerUnshare(String email) {
+        if (!mSharesAdded.remove(email)) {
+            mSharesRemoved.add(email);
+        }
+    }
+
+    /**
+     * @return the position of the e-mail in the adapter
+     */
+    public int onCustomShare(String email) {
+        int i = Collections.binarySearch(mSharingPossible.items, email);
+        if (i >= 0) {
+            return shareWithPossible(i);
+        } else {
+            return insertShare(email);
+        }
+    }
+
+    public void setSharedTo(Collection<String> sharedAlready) {
+        setSharedAlreadyData(sharedAlready);
+        // TODO(rosswang): list differ
+
+        notifyDataSetChanged();
+    }
+
+    // TODO(rosswang): this is a hacky abstraction
+    public void toggleContact(int position) {
+        SublistEntry entry = getSublistItemPosition(position);
+        if (entry.sublist == mSharingAlready) {
+            unshare(entry.itemPosition);
+        } else {
+            shareWithPossible(entry.itemPosition);
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/syncbase2/java/io/v/todos/sharing/NeighborhoodFragment.java b/app/src/syncbase2/java/io/v/todos/sharing/NeighborhoodFragment.java
new file mode 100644
index 0000000..bb2ff3b
--- /dev/null
+++ b/app/src/syncbase2/java/io/v/todos/sharing/NeighborhoodFragment.java
@@ -0,0 +1,135 @@
+package io.v.todos.sharing;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.SwitchCompat;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.CompoundButton;
+import android.widget.Toast;
+
+import io.v.syncbase.Syncbase;
+import io.v.syncbase.core.VError;
+import io.v.todos.R;
+import io.v.todos.persistence.syncbase.SyncbasePersistence;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Advertisement;
+
+/**
+ * A fragment encapsulating menu options and functionality related to list sharing.
+ */
+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 static SharedPreferences sPrefs;
+
+    // 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 void initSharePresence(Context androidContext) {
+        sPrefs = sPrefs != null ? sPrefs : PreferenceManager.getDefaultSharedPreferences(
+                androidContext);
+
+        sPrefs.registerOnSharedPreferenceChangeListener(sSharedPrefListener);
+        updateSharePresence();
+    }
+
+    private static void updateSharePresence() {
+        if (shouldAdvertise()) {
+            try {
+                Syncbase.advertiseLoggedInUserInNeighborhood();
+            } catch (VError vError) {
+                Log.w(FRAGMENT_TAG, "Failed to advertise logged in user", vError);
+            }
+        } else {
+            Syncbase.stopAdvertisingLoggedInUserInNeighborhood();
+        }
+    }
+
+    private static void setAdvertiseNeighborhood(boolean value) {
+        sPrefs.edit().putBoolean(PREF_ADVERTISE_NEIGHBORHOOD, value).apply();
+    }
+
+    private static boolean shouldAdvertise() {
+        return sPrefs.getBoolean(PREF_ADVERTISE_NEIGHBORHOOD, true);
+    }
+
+    private SwitchCompat mSwitch;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        inflater.inflate(R.menu.neighborhood, menu);
+        MenuItem menuItem = menu.findItem(R.id.advertise_neighborhood);
+        mSwitch = (SwitchCompat) menuItem.getActionView().
+                findViewById(R.id.neighborhood_switch);
+        mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+            @Override
+            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                setAdvertiseNeighborhood(isChecked);
+            }
+        });
+        sPrefs = sPrefs != null ? sPrefs : PreferenceManager.getDefaultSharedPreferences
+                (getActivity());
+        sPrefs.registerOnSharedPreferenceChangeListener(this);
+        updateAdvertiseNeighborhoodChecked();
+    }
+
+    @Override
+    public void onDestroyOptionsMenu() {
+        sPrefs.unregisterOnSharedPreferenceChangeListener(this);
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+        if (PREF_ADVERTISE_NEIGHBORHOOD.equals(key)) {
+            updateAdvertiseNeighborhoodChecked();
+        }
+    }
+
+    private static Boolean sLastToastCheck; // Used to track the last toasted value.
+
+    private void updateAdvertiseNeighborhoodChecked() {
+        boolean value = shouldAdvertise();
+        mSwitch.setChecked(value);
+        mSwitch.setThumbResource(value ?
+                R.drawable.sharing_activated_0_5x :
+                R.drawable.sharing_deactivated_0_5x);
+
+        // If this was a change, then toast information to the user.
+        if (sLastToastCheck == null || sLastToastCheck != value) {
+            sLastToastCheck = value;
+            Toast.makeText(getActivity(), value ?
+                    R.string.presence_on : R.string.presence_off, Toast.LENGTH_SHORT).show();
+        }
+    }
+}
diff --git a/app/src/syncbase2/java/io/v/todos/sharing/ShareListDialogFragment.java b/app/src/syncbase2/java/io/v/todos/sharing/ShareListDialogFragment.java
new file mode 100644
index 0000000..e4550e1
--- /dev/null
+++ b/app/src/syncbase2/java/io/v/todos/sharing/ShareListDialogFragment.java
@@ -0,0 +1,182 @@
+// 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.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+import io.v.syncbase.Syncbase;
+import io.v.syncbase.User;
+import io.v.todos.R;
+
+/**
+ * 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
+        implements ContactAdapter.ContactTouchListener {
+    public static final String FRAGMENT_TAG = ShareListDialogFragment.class.getSimpleName();
+
+    public static ShareListDialogFragment find(FragmentManager fragmentManager) {
+        return (ShareListDialogFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
+    }
+
+    private RecyclerView mContacts;
+    private Set<String> mRemoved, mAdded, mRecent;
+
+    private static final String
+            REMOVED_KEY = "removedShares",
+            ADDED_KEY = "addedShares",
+            RECENT_KEY = "recentShares";
+
+    private ContactAdapter mAdapter;
+
+    private ShareListMenuFragment getParent() {
+        return ((ShareListMenuFragment) getParentFragment());
+    }
+
+    @Override
+    public void onDestroyView() {
+        rmAdapter(mAdapter);
+        super.onDestroyView();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        outState.putStringArrayList(REMOVED_KEY, new ArrayList<>(mRemoved));
+        outState.putStringArrayList(ADDED_KEY, new ArrayList<>(mAdded));
+        outState.putStringArrayList(RECENT_KEY, new ArrayList<>(mRecent));
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        View view = getActivity().getLayoutInflater().inflate(R.layout.sharing, null);
+
+        if (savedInstanceState == null) {
+            mRemoved = new HashSet<>();
+            mAdded = new HashSet<>();
+            mRecent = new HashSet<>();
+        } else {
+            mRemoved = new HashSet<>(savedInstanceState.getStringArrayList(REMOVED_KEY));
+            mAdded = new HashSet<>(savedInstanceState.getStringArrayList(ADDED_KEY));
+            mRecent = new HashSet<>(savedInstanceState.getStringArrayList(RECENT_KEY));
+        }
+
+        mAdapter = new ContactAdapter(getParent().getSharedTo(), mAdded, mRemoved, mRecent);
+        mAdapter.setContactTouchListener(this);
+        mContacts = (RecyclerView) view.findViewById(R.id.recycler);
+        mContacts.setAdapter(mAdapter);
+        addAdapter(mAdapter);
+
+        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();
+                    mAdapter.onCustomShare(email);
+                    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) {
+                        mAdapter.filterDeltas();
+                        getParent().persistence.shareTodoList(mAdded);
+                        // TODO(alexfandrianto/rosswang): removal
+                    }
+                })
+                .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                    }
+                })
+                .create();
+    }
+
+    public void onSharedToChanged() {
+        mContacts.post(new Runnable() {
+            @Override
+            public void run() {
+                mAdapter.setSharedTo(getParent().getSharedTo());
+            }
+        });
+    }
+
+    @Override
+    public void onContactTouch(RecyclerView.ViewHolder viewHolder) {
+        mAdapter.toggleContact(viewHolder.getAdapterPosition());
+    }
+
+    // Keep track of the Users we have found who are nearby.
+    private static Set<User> mUsers = new HashSet<>();
+    private static Set<ContactAdapter> mAdapters = new HashSet<>();
+
+    public static synchronized void addUser(User user) {
+        mUsers.add(user);
+        for(ContactAdapter adapter : mAdapters) {
+            adapter.onNearbyDeviceDiscovered(user.getAlias());
+        }
+    }
+    public static synchronized void rmUser(User user) {
+        mUsers.remove(user);
+        for(ContactAdapter adapter : mAdapters) {
+            adapter.onNearbyDeviceLost(user.getAlias());
+        }
+    }
+    public static synchronized void addAdapter(ContactAdapter adapter) {
+        mAdapters.add(adapter);
+        for(User user : mUsers) {
+            adapter.onNearbyDeviceDiscovered(user.getAlias());
+        }
+    }
+    public static synchronized void rmAdapter(ContactAdapter adapter) {
+        mAdapters.remove(adapter);
+    }
+
+    public static void initScan() {
+        Syncbase.ScanNeighborhoodForUsersCallback mScanCb = new Syncbase.ScanNeighborhoodForUsersCallback() {
+            @Override
+            public void onFound(User user) {
+                addUser(user);
+            }
+
+            @Override
+            public void onLost(User user) {
+                rmUser(user);
+            }
+
+            @Override
+            public void onError(Throwable e) {
+                Log.w(FRAGMENT_TAG, e);
+            }
+        };
+        Syncbase.addScanForUsersInNeighborhood(mScanCb);
+    }
+}
diff --git a/app/src/syncbase2/java/io/v/todos/sharing/ShareListMenuFragment.java b/app/src/syncbase2/java/io/v/todos/sharing/ShareListMenuFragment.java
new file mode 100644
index 0000000..aa3889f
--- /dev/null
+++ b/app/src/syncbase2/java/io/v/todos/sharing/ShareListMenuFragment.java
@@ -0,0 +1,85 @@
+// 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<>();
+    private Menu mMenu;
+    private boolean mShouldHide;
+
+    public void setSharedTo(List<String> sharedTo) {
+        mSharedTo = sharedTo;
+
+        ShareListDialogFragment dialog = ShareListDialogFragment.find(getChildFragmentManager());
+        if (dialog != null) {
+            dialog.onSharedToChanged();
+        }
+    }
+
+    public List<String> getSharedTo() {
+        return mSharedTo;
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+    }
+
+    public void hideShareMenuItem() {
+        mShouldHide = true;
+        if (mMenu != null) {
+            mMenu.findItem(R.id.action_share).setVisible(false);
+        }
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        mMenu = menu;
+        inflater.inflate(R.menu.menu_share, menu);
+        if (mShouldHide) {
+            // In case the menu was added after the hide flag was set, hide the menu item now.
+            hideShareMenuItem();
+        }
+    }
+
+    @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/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_off_white_24dp.png b/app/src/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_off_white_24dp.png
deleted file mode 100644
index 6e61f95..0000000
--- a/app/src/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_off_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_on_white_24dp.png b/app/src/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_on_white_24dp.png
deleted file mode 100644
index 6c0314a..0000000
--- a/app/src/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_on_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_off_white_24dp.png b/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_off_white_24dp.png
deleted file mode 100644
index ab03ab5..0000000
--- a/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_off_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_on_white_24dp.png b/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_on_white_24dp.png
deleted file mode 100644
index 8dec6e0..0000000
--- a/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_on_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_off_white_24dp.png b/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_off_white_24dp.png
deleted file mode 100644
index e21ea70..0000000
--- a/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_off_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_on_white_24dp.png b/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_on_white_24dp.png
deleted file mode 100644
index cb07640..0000000
--- a/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_on_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_off_white_24dp.png b/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_off_white_24dp.png
deleted file mode 100644
index 8735faa..0000000
--- a/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_off_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_on_white_24dp.png b/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_on_white_24dp.png
deleted file mode 100644
index 9e9560c..0000000
--- a/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_on_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_off_white_24dp.png b/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_off_white_24dp.png
deleted file mode 100644
index e28418e..0000000
--- a/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_off_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_on_white_24dp.png b/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_on_white_24dp.png
deleted file mode 100644
index f992b16..0000000
--- a/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_on_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable/sharing_activated_0_5x.png b/app/src/syncbase2/res/drawable/sharing_activated_0_5x.png
new file mode 100644
index 0000000..9d52b8d
--- /dev/null
+++ b/app/src/syncbase2/res/drawable/sharing_activated_0_5x.png
Binary files differ
diff --git a/app/src/syncbase2/res/drawable/sharing_deactivated_0_5x.png b/app/src/syncbase2/res/drawable/sharing_deactivated_0_5x.png
new file mode 100644
index 0000000..7fad11c
--- /dev/null
+++ b/app/src/syncbase2/res/drawable/sharing_deactivated_0_5x.png
Binary files differ
diff --git a/app/src/syncbase2/res/layout/neighborhood.xml b/app/src/syncbase2/res/layout/neighborhood.xml
new file mode 100644
index 0000000..b63875f
--- /dev/null
+++ b/app/src/syncbase2/res/layout/neighborhood.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:orientation="horizontal"
+                android:layout_width="fill_parent"
+                android:layout_height="match_parent">
+
+    <android.support.v7.widget.SwitchCompat
+        android:id="@+id/neighborhood_switch"
+        android:layout_gravity="center_vertical"
+        android:layout_width="wrap_content"
+        android:layout_height="fill_parent"
+        android:theme="@style/SwitchCompat" />
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/app/src/syncbase2/res/menu/neighborhood.xml b/app/src/syncbase2/res/menu/neighborhood.xml
index b7d77d8..3ae819a 100644
--- a/app/src/syncbase2/res/menu/neighborhood.xml
+++ b/app/src/syncbase2/res/menu/neighborhood.xml
@@ -3,8 +3,7 @@
       xmlns:app="http://schemas.android.com/apk/res-auto">
     <item android:id="@+id/advertise_neighborhood"
           android:orderInCategory="103"
-          android:checkable="true"
-          android:icon="@drawable/ic_advertise_neighborhood_off_white_24dp"
+          android:actionLayout="@layout/neighborhood"
           android:title="@string/share_location"
           android:showAsAction="always"/>
 </menu>
\ No newline at end of file
diff --git a/app/src/syncbase2/res/values/strings.xml b/app/src/syncbase2/res/values/strings.xml
index b0e644b..f9c200f 100644
--- a/app/src/syncbase2/res/values/strings.xml
+++ b/app/src/syncbase2/res/values/strings.xml
@@ -1,6 +1,9 @@
 <resources>
     <string name="app_name">Syncbase Todos</string>
     <string name="share_location">Share Presence</string>
+    <!-- For Sharing Presence Toasts -->
+    <string name="presence_on">Sharing Presence: ON</string>
+    <string name="presence_off">Sharing Presence: OFF</string>
     <!-- For Sharing Menu -->
     <string name="sharing_already">Shared With</string>
     <string name="sharing_possible">Available</string>