java/syncbase: Test Advertise+Scan. Refactor Android Helpers

I was changing a small thing about Advertise/Scan in Neighborhood.
However, this lead me to discover that the tests were not longer
compiling.

So I refactored the Android Helpers to the side (to avoid
importing things that the tests did not understand).

In addition to the new tests (and old ones) passing, dice roller was
updated to confirm that the Android Helper changes are okay.

Change-Id: I7822c9d9ea4b9ddaf99bedc694c3688904456c50
diff --git a/projects/dice_roller/app/build.gradle b/projects/dice_roller/app/build.gradle
index d73c49a..14af5bd 100644
--- a/projects/dice_roller/app/build.gradle
+++ b/projects/dice_roller/app/build.gradle
@@ -32,5 +32,5 @@
     compile fileTree(dir: 'libs', include: ['*.jar'])
     testCompile 'junit:junit:4.12'
     compile 'com.android.support:appcompat-v7:23.4.0'
-    compile 'io.v:syncbase:0.1.7'
+    compile 'io.v:syncbase:0.1.8'
 }
diff --git a/projects/dice_roller/app/src/main/java/v/io/diceroller/MainActivity.java b/projects/dice_roller/app/src/main/java/v/io/diceroller/MainActivity.java
index 9d3b61c..fc8540c 100644
--- a/projects/dice_roller/app/src/main/java/v/io/diceroller/MainActivity.java
+++ b/projects/dice_roller/app/src/main/java/v/io/diceroller/MainActivity.java
@@ -4,7 +4,6 @@
 
 package v.io.diceroller;
 
-import android.content.Context;
 import android.os.Bundle;
 import android.support.v7.app.AppCompatActivity;
 import android.util.Log;
@@ -12,15 +11,14 @@
 import android.widget.Button;
 import android.widget.TextView;
 
-import java.util.ArrayList;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Random;
 
 import io.v.syncbase.Collection;
 import io.v.syncbase.Database;
 import io.v.syncbase.Syncbase;
 import io.v.syncbase.WatchChange;
+import io.v.syncbase.android.SyncbaseAndroid;
 import io.v.syncbase.exception.SyncbaseException;
 
 public class MainActivity extends AppCompatActivity {
@@ -38,16 +36,16 @@
         setContentView(R.layout.activity_main);
 
         try {
-            String rootDir = getDir("syncbase", Context.MODE_PRIVATE).getAbsolutePath();
-            Syncbase.Options options =
-                    Syncbase.Options.cloudBuilder(rootDir, CLOUD_NAME, CLOUD_ADMIN)
-                            .setMountPoint(MOUNT_POINT).build();
+            Syncbase.Options options = Syncbase.Options.cloudBuilder(
+                    SyncbaseAndroid.defaultRootDir(this), CLOUD_NAME, CLOUD_ADMIN)
+                    .setMountPoint(MOUNT_POINT)
+                    .build();
             Syncbase.init(options);
         } catch (SyncbaseException e) {
             Log.e(TAG, e.toString());
         }
 
-        Syncbase.loginAndroid(this, new LoginCallback());
+        SyncbaseAndroid.login(this, new LoginCallback());
     }
 
     @Override
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncbase.java b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
index c955244..89d27f8 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncbase.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
@@ -4,8 +4,6 @@
 
 package io.v.syncbase;
 
-import android.app.Activity;
-import android.app.FragmentTransaction;
 import android.os.Handler;
 import android.os.Looper;
 
@@ -18,11 +16,9 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 
-import io.v.syncbase.android.LoginFragment;
 import io.v.syncbase.core.NeighborhoodPeer;
 import io.v.syncbase.core.Permissions;
 import io.v.syncbase.core.Service;
@@ -45,14 +41,16 @@
  */
 
 /**
- * Syncbase is a storage system for developers that makes it easy to synchronize app data between
- * devices. It works even when devices are not connected to the Internet.
+ * A storage system for developers that makes it easy to synchronize app data between devices.
+ * It works even when devices are not connected to the Internet.
  *
  * <p>Methods of classes in this package may throw an exception that is a subclass of
  * SyncbaseException.  See details of those subclasses to determine whether there are conditions
  * the calling code should handle.</p>
  */
 public class Syncbase {
+    private Syncbase() {}
+
     /**
      * Options for opening a database.
      */
@@ -69,7 +67,8 @@
         final String mCloudAdmin;
 
         Options(Options.Builder builder) {
-            mCallbackExecutor = builder.mExecutor;
+            mCallbackExecutor = builder.mExecutor != null
+                    ? builder.mExecutor : UiThreadExecutor.INSTANCE;
             mRootDir = builder.mRootDir;
             mMountPoints = builder.mMountPoints;
             mDisableSyncgroupPublishing = !builder.mUsesCloud;
@@ -109,7 +108,7 @@
             private final String mCloudName;
             private final String mCloudAdmin;
 
-            private Executor mExecutor = UiThreadExecutor.INSTANCE;
+            private Executor mExecutor;
             private final List<String> mMountPoints = new ArrayList<>();
             private boolean mTestLogin;
             private int mLogLevel;
@@ -253,6 +252,13 @@
     }
 
     /**
+     * Runs the callback on the callback executor.
+     */
+    public static void executeCallback(Runnable runnable) {
+        sOpts.mCallbackExecutor.execute(runnable);
+    }
+
+    /**
      * Returns a Database object. Return null if the user is not currently logged in.
      */
     public static Database database() throws SyncbaseException {
@@ -311,38 +317,6 @@
     }
 
     /**
-     * Logs in the user on Android.
-     * If the user is already logged in, it runs the success callback on the executor. Otherwise,
-     * the user selects an account through an account picker flow and is logged into Syncbase. The
-     * callback's success or failure cases are called accordingly.
-     * Note: This default account flow is currently restricted to Google accounts.
-     *
-     * @param activity The Android activity where login will occur.
-     * @param cb       The callback to call when the login was done.
-     */
-    public static void loginAndroid(Activity activity, final LoginCallback cb) {
-        if (isLoggedIn()) {
-            sOpts.mCallbackExecutor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    cb.onSuccess();
-                }
-            });
-            return;
-        }
-        FragmentTransaction transaction = activity.getFragmentManager().beginTransaction();
-        LoginFragment fragment = new LoginFragment();
-        fragment.setTokenReceiver(new LoginFragment.TokenReceiver() {
-            @Override
-            public void receiveToken(String token) {
-                Syncbase.login(token, User.PROVIDER_GOOGLE, cb);
-            }
-        });
-        transaction.add(fragment, UUID.randomUUID().toString());
-        transaction.commit();  // This will invoke the fragment's onCreate() immediately.
-    }
-
-    /**
      * Logs in the user associated with the given OAuth token and provider and starts Syncbase;
      * creates default database if needed; performs create-or-join for "userdata" syncgroup if
      * needed. The passed callback is called on the current thread.
@@ -520,8 +494,13 @@
     /**
      * Advertises the logged in user's presence to those around them.
      */
-    public static void advertiseLoggedInUserInNeighborhood() throws VError {
-        Neighborhood.StartAdvertising(new ArrayList<String>());
+    public static void advertiseLoggedInUserInNeighborhood() throws SyncbaseException {
+        try {
+            Neighborhood.StartAdvertising(new ArrayList<String>());
+        } catch(VError e) {
+            chainThrow("advertising user in neighborhood", e);
+            throw new AssertionError("never happens");
+        }
     }
 
     /**
@@ -529,12 +508,17 @@
      *
      * @param usersWhoCanSee The set of users who are allowed to find this user.
      */
-    public static void advertiseLoggedInUserInNeighborhood(Iterable<User> usersWhoCanSee) throws VError {
+    public static void advertiseLoggedInUserInNeighborhood(Iterable<User> usersWhoCanSee) throws SyncbaseException {
         List<String> visibility = new ArrayList<String>();
         for (User user : usersWhoCanSee) {
             visibility.add(Syncbase.getBlessingStringFromAlias(user.getAlias()));
         }
-        Neighborhood.StartAdvertising(visibility);
+        try {
+            Neighborhood.StartAdvertising(visibility);
+        } catch(VError e) {
+            chainThrow("advertising user in neighborhood", e);
+            throw new AssertionError("never happens");
+        }
     }
 
     /**
@@ -562,9 +546,7 @@
 
     static String getPersonalBlessingString() throws SyncbaseException {
         try {
-
             return Blessings.UserBlessingFromContext();
-
         } catch(VError e) {
             chainThrow("getting certificates from context", e);
             throw new AssertionError("never happens");
diff --git a/syncbase/src/main/java/io/v/syncbase/android/SyncbaseAndroid.java b/syncbase/src/main/java/io/v/syncbase/android/SyncbaseAndroid.java
new file mode 100644
index 0000000..e5cbb1f
--- /dev/null
+++ b/syncbase/src/main/java/io/v/syncbase/android/SyncbaseAndroid.java
@@ -0,0 +1,66 @@
+// 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.syncbase.android;
+
+import android.app.Activity;
+import android.app.FragmentTransaction;
+import android.content.Context;
+
+import java.util.UUID;
+
+import io.v.syncbase.Syncbase;
+import io.v.syncbase.User;
+
+/**
+ * Contains helper methods for initializing Syncbase on Android.
+ */
+public final class SyncbaseAndroid {
+    private SyncbaseAndroid() {}
+
+    /**
+     * Logs in the user on Android.
+     * If the user is already logged in, it runs the success callback on the executor. Otherwise,
+     * the user selects an account through an account picker flow and is logged into Syncbase. The
+     * callback's success or failure cases are called accordingly.
+     * Note: This default account flow is currently restricted to Google accounts.
+     *
+     * @param activity The Android activity where login will occur.
+     * @param callback The callback to call when the login was done.
+     */
+    public static void login(Activity activity, final Syncbase.LoginCallback callback) {
+        if (Syncbase.isLoggedIn()) {
+            Syncbase.executeCallback(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onSuccess();
+                }
+            });
+            return;
+        }
+        FragmentTransaction transaction = activity.getFragmentManager().beginTransaction();
+        LoginFragment fragment = new LoginFragment();
+        fragment.setTokenReceiver(new LoginFragment.TokenReceiver() {
+            @Override
+            public void receiveToken(String token) {
+                Syncbase.login(token, User.PROVIDER_GOOGLE, callback);
+            }
+        });
+        transaction.add(fragment, UUID.randomUUID().toString());
+        transaction.commit(); // This will invoke the fragment's onCreate() immediately.
+    }
+
+    /**
+     * Computes a default location to store Syncbase data.
+     *
+     * @param context The Android context
+     */
+    public static String defaultRootDir(Context context) {
+        return context.getDir("syncbase", Context.MODE_PRIVATE).getAbsolutePath();
+    }
+
+    // TODO(alexfandrianto): Add options builders for Android. We would like to specify the default
+    // root dir as well as the mount point if possible. A good default mount point could be computed
+    // off the app id; unfortunately, we won't know that until after we've logged in.
+}
diff --git a/syncbase/src/main/java/io/v/syncbase/exception/Exceptions.java b/syncbase/src/main/java/io/v/syncbase/exception/Exceptions.java
index 5c08a38..d8a7423 100644
--- a/syncbase/src/main/java/io/v/syncbase/exception/Exceptions.java
+++ b/syncbase/src/main/java/io/v/syncbase/exception/Exceptions.java
@@ -94,6 +94,7 @@
             case "v.io/v23/services/syncbase.InferAppBlessingFailed":
             case "v.io/v23/services/syncbase.InferUserBlessingFailed":
             case "v.io/v23/services/syncbase.InferDefaultPermsFailed":
+            case "v.io/v23/syncbase/util.FoundNoConventionalBlessings":
                 throw new SyncbaseSecurityException(fullMessage, cause);
 
             default:
diff --git a/syncbase/src/main/java/io/v/syncbase/exception/SyncbaseSecurityException.java b/syncbase/src/main/java/io/v/syncbase/exception/SyncbaseSecurityException.java
index f4a0984..a60f253 100644
--- a/syncbase/src/main/java/io/v/syncbase/exception/SyncbaseSecurityException.java
+++ b/syncbase/src/main/java/io/v/syncbase/exception/SyncbaseSecurityException.java
@@ -7,7 +7,8 @@
 /**
  * Thrown in response to various inconsistencies in permissions, or unsupported authentication
  * provider, or Vanadium errors: NoAccess, NotTrusted, NoExistOrNoAccess, UnauthorizedCreateId,
- * InferAppBlessingFailed, InferUserBlessingFailed, or InferDefaultPermsFailed.
+ * InferAppBlessingFailed, InferUserBlessingFailed, InferDefaultPermsFailed, or
+ * FoundNoConventionalBlessings.
  */
 public class SyncbaseSecurityException extends SyncbaseException {
     SyncbaseSecurityException(String message, Exception cause) {
diff --git a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
index 5c53115..66a63a6 100644
--- a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
@@ -17,7 +17,9 @@
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 import io.v.syncbase.core.Permissions;
 import io.v.syncbase.core.VError;
@@ -488,4 +490,86 @@
             }
         });
     }
+
+    @Test
+    public void testAdvertiseInNeighborhood() throws Exception {
+        TestUtil.createDatabase(); // This is meant to force the test login.
+
+        assertFalse(Syncbase.isAdvertisingLoggedInUserInNeighborhood());
+        Syncbase.advertiseLoggedInUserInNeighborhood();
+        assertTrue(Syncbase.isAdvertisingLoggedInUserInNeighborhood());
+        Syncbase.stopAdvertisingLoggedInUserInNeighborhood();
+        assertFalse(Syncbase.isAdvertisingLoggedInUserInNeighborhood());
+    }
+
+    @Test
+    public void testAdvertiseInNeighborhoodNotLoggedIn() {
+        try {
+            Syncbase.advertiseLoggedInUserInNeighborhood();
+            fail("should throw because the user isn't logged in");
+        } catch (SyncbaseException e) {
+            // We expect the advertise attempt to throw.
+        }
+    }
+
+    @Test
+    public void testScanInNeighborhood() throws Exception {
+        TestUtil.createDatabase(); // This is meant to force the test login.
+
+        Syncbase.ScanNeighborhoodForUsersCallback cb =
+                new Syncbase.ScanNeighborhoodForUsersCallback() {
+            @Override
+            public void onFound(User user) {}
+
+            @Override
+            public void onLost(User user) {}
+
+            @Override
+            public void onError(Throwable e) {
+                fail("should not throw an error");
+            }
+        };
+        // Test that this doesn't error. A more thorough test would check that the callback fires,
+        // but that would require multiple devices, so it has not been done here.
+        Syncbase.removeAllScansForUsersInNeighborhood();
+        Syncbase.addScanForUsersInNeighborhood(cb);
+        Syncbase.removeScanForUsersInNeighborhood(cb);
+        Syncbase.removeAllScansForUsersInNeighborhood();
+
+        try {
+            SettableFuture.create().get(1, TimeUnit.SECONDS);
+        } catch (TimeoutException e) {
+            // It's okay for this to time out. We just need to ensure the error callback has
+            // time to be called.
+        }
+    }
+
+    @Test
+    public void testScanInNeighborhoodNotLoggedIn() {
+        final SettableFuture<Boolean> errored = SettableFuture.create();
+        Syncbase.ScanNeighborhoodForUsersCallback cb =
+                new Syncbase.ScanNeighborhoodForUsersCallback() {
+                    @Override
+                    public void onFound(User user) {}
+
+                    @Override
+                    public void onLost(User user) {}
+
+                    @Override
+                    public void onError(Throwable e) {
+                        // This is expected to be called.
+                        assertNotNull("should error", e);
+                        errored.set(true);
+                    }
+                };
+        // Force an error because the user was not logged in.
+        Syncbase.addScanForUsersInNeighborhood(cb);
+
+        // Give the callback time to be called.
+        try {
+            assertTrue(errored.get(1, TimeUnit.SECONDS));
+        } catch (InterruptedException | TimeoutException | ExecutionException e) {
+            fail("should have been able to get successfully");
+        }
+    }
 }