Initial commit

Change-Id: If67f482af79eb58050361d41a5ac7338bed98a2b
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d8b8329
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+*.iml
+/.DS_Store
+/.gradle
+/.idea
+/build
+/captures
+/local.properties
diff --git a/bakutoolkit/.gitignore b/bakutoolkit/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/bakutoolkit/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/bakutoolkit/build.gradle b/bakutoolkit/build.gradle
new file mode 100644
index 0000000..492104f
--- /dev/null
+++ b/bakutoolkit/build.gradle
@@ -0,0 +1,70 @@
+apply plugin: 'com.android.library'
+/*
+You might have to download JDK8 and set JAVA8_HOME (or set the jdk to Java 8 via Project Structure).
+For detailed instructions, see https://github.com/evant/gradle-retrolambda
+ */
+apply plugin: 'me.tatarka.retrolambda'
+
+android {
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    compileSdkVersion 21
+    buildToolsVersion "21.1.2"
+
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 21
+        versionCode 1
+        versionName "1.0"
+    }
+}
+
+dependencies {
+    provided(
+            /* If the application wishes to use support libraries, it should include them as compile
+            dependencies in its own build.gradle. */
+            'com.android.support:appcompat-v7:23.0.1',
+
+            'org.glassfish:javax.annotation:10.0-b28',
+            /*
+            https://projectlombok.org/setup/android.html
+            Follow Android Studio instructions at the bottom of the page to install the Lombok
+            Plugin.
+             */
+            'org.projectlombok:lombok:1.16.6',
+            'org.slf4j:slf4j-api:1.7.12'
+    )
+
+
+    testCompile(
+            'org.mockito:mockito-core:1.10.19',
+            'org.powermock:powermock-module-junit4:1.6.3',
+            'org.slf4j:slf4j-simple:1.7.12'
+    )
+
+    testCompile('org.powermock:powermock-api-mockito:1.6.3') {
+        exclude module: 'mockito-all'
+    }
+
+    compile(
+            //'com.android.support:appcompat-v7:23.1.0',
+            'com.jakewharton.rxbinding:rxbinding:0.3.0',
+            'io.reactivex:rxandroid:1.0.1',
+            'io.reactivex:rxjava:1.0.16',
+            'io.reactivex:rxjava-async-util:0.21.0',
+            'io.v:vanadium:0.1',
+            'io.v:vanadium-android:0.1',
+            'net.sourceforge.streamsupport:streamsupport:1.3.2'
+    )
+    compile fileTree(dir: 'libs', include: ['*.jar'])
+
+    /*
+    Word of caution: for local unit tests, Android log statements fail with
+    UnsatisfiedLinkError (hence the slf4j-simple implementation for testCompile).
+
+    Applications should include a suitable runtime binding, such as
+    apk ('org.slf4j:slf4j-android:1.7.12')
+    */
+}
diff --git a/bakutoolkit/proguard-rules.pro b/bakutoolkit/proguard-rules.pro
new file mode 100644
index 0000000..25805d2
--- /dev/null
+++ b/bakutoolkit/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/local/google/home/rosswang/.android-sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
diff --git a/bakutoolkit/src/androidTest/java/io/v/baku/toolkit/ApplicationTest.java b/bakutoolkit/src/androidTest/java/io/v/baku/toolkit/ApplicationTest.java
new file mode 100644
index 0000000..d9ffa3f
--- /dev/null
+++ b/bakutoolkit/src/androidTest/java/io/v/baku/toolkit/ApplicationTest.java
@@ -0,0 +1,13 @@
+package io.v.baku.toolkit;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+    public ApplicationTest() {
+        super(Application.class);
+    }
+}
\ No newline at end of file
diff --git a/bakutoolkit/src/lombok.config b/bakutoolkit/src/lombok.config
new file mode 100644
index 0000000..6c54391
--- /dev/null
+++ b/bakutoolkit/src/lombok.config
@@ -0,0 +1,5 @@
+# This works for compile but not in the IDE; use @Accessors(prefix = "m") instead.
+lombok.accessors.prefix += m
+
+# Required for Android Studio
+lombok.anyConstructor.suppressConstructorProperties = true
diff --git a/bakutoolkit/src/main/AndroidManifest.xml b/bakutoolkit/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5916f9b
--- /dev/null
+++ b/bakutoolkit/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.v.baku.toolkit">
+
+    <application
+        android:allowBackup="true"
+        android:label="@string/app_name"
+        android:supportsRtl="true">
+        <service
+            android:name="io.v.debug.SyncbaseAndroidService"
+            android:enabled="true"
+            android:exported="true" />
+
+    </application>
+
+</manifest>
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuActivity.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuActivity.java
new file mode 100644
index 0000000..1c0f34d
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuActivity.java
@@ -0,0 +1,50 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.os.Bundle;
+import android.os.PersistableBundle;
+
+import io.v.baku.toolkit.bind.SyncbaseBinding;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * A default application of {@link BakuActivityTrait} extending {@link android.app.Activity}. Most
+ * activities with distributed state should inherit from this.
+ */
+@Accessors(prefix = "m")
+@Slf4j
+public abstract class BakuActivity extends VActivity implements BakuActivityMixin {
+    @Getter
+    private BakuActivityTrait mBakuActivityTrait;
+
+    protected BakuActivityTrait createBakuActivityTrait() {
+        return new BakuActivityTrait(getVAndroidContextTrait());
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mBakuActivityTrait = createBakuActivityTrait();
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
+        super.onCreate(savedInstanceState, persistentState);
+        mBakuActivityTrait = createBakuActivityTrait();
+    }
+
+    @Override
+    protected void onDestroy() {
+        mBakuActivityTrait.close();
+        super.onDestroy();
+    }
+
+    public <T> SyncbaseBinding.Builder<T> binder() {
+        return getBakuActivityTrait().binder();
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuActivityMixin.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuActivityMixin.java
new file mode 100644
index 0000000..0362740
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuActivityMixin.java
@@ -0,0 +1,12 @@
+// Copyright 2015 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.baku.toolkit;
+
+/**
+ * An optional convenience interface for classes mixing in {@link BakuActivityTrait}.
+ */
+public interface BakuActivityMixin {
+    BakuActivityTrait getBakuActivityTrait();
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuActivityTrait.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuActivityTrait.java
new file mode 100644
index 0000000..72ab3d0
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuActivityTrait.java
@@ -0,0 +1,88 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.app.Activity;
+
+import io.v.baku.toolkit.bind.SyncbaseBinding;
+import io.v.rx.syncbase.GlobalUserSyncgroup;
+import io.v.rx.syncbase.RxDb;
+import io.v.rx.syncbase.RxSyncbase;
+import io.v.rx.syncbase.RxTable;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import rx.subscriptions.CompositeSubscription;
+
+/**
+ * Activity trait for activities with distributed UI state. By default, shared state is stored
+ * in Syncbase under <i>app.package.name</i>/db/ui.
+ * <p>
+ * Default activity extensions incorporating this mix-in are available:
+ * <ul>
+ * <li>{@link BakuActivity} (extends {@link Activity})</li>
+ * <li>{@link BakuAppCompatActivity} (extends {@link android.support.v7.app.AppCompatActivity})</li>
+ * </ul>
+ * Since Java doesn't actually support multiple inheritance, clients requiring custom inheritance
+ * hierarchies will need to wire in manually, like any of the examples above.
+ */
+@Accessors(prefix = "m")
+@Slf4j
+public class BakuActivityTrait implements AutoCloseable {
+    @Getter
+    private final VAndroidContextTrait<? extends Activity> mVAndroidContextTrait;
+
+    @Getter
+    private final RxSyncbase mSyncbase;
+    @Getter
+    private final RxDb mSyncbaseDb;
+    @Getter
+    private final RxTable mSyncbaseTable;
+    @Getter
+    private final CompositeSubscription mSubscriptions;
+
+    public BakuActivityTrait(final VAndroidContextTrait<? extends Activity> vAndroidContextTrait) {
+        mVAndroidContextTrait = vAndroidContextTrait;
+
+        mSubscriptions = new CompositeSubscription();
+        mSyncbase = new RxSyncbase(vAndroidContextTrait);
+
+        final String app = getSyncbaseAppName(),
+                db = getSyncbaseDbName(),
+                t = getSyncbaseTableName();
+        log.info("Mapping Syncbase path: {}/{}/{}", app, db, t);
+        mSyncbaseDb = mSyncbase.rxApp(app).rxDb(db);
+        mSyncbaseTable = mSyncbaseDb.rxTable(t);
+
+        GlobalUserSyncgroup.forActivity(this).join();
+    }
+
+    @Override
+    public void close() {
+        mSubscriptions.unsubscribe();
+        mSyncbase.close();
+    }
+
+    protected String getSyncbaseAppName() {
+        return mVAndroidContextTrait.getAndroidContext().getPackageName();
+    }
+
+    protected String getSyncbaseDbName() {
+        return "db";
+    }
+
+    public String getSyncbaseTableName() {
+        return "ui";
+    }
+
+    public void onSyncError(final Throwable t) {
+        mVAndroidContextTrait.getErrorReporter().onError(R.string.err_sync, t);
+    }
+
+    public <T> SyncbaseBinding.Builder<T> binder() {
+        return SyncbaseBinding.<T>builder()
+                .bakuActivity(this);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuAppCompatActivity.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuAppCompatActivity.java
new file mode 100644
index 0000000..b6ecdee
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/BakuAppCompatActivity.java
@@ -0,0 +1,51 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.os.Bundle;
+import android.os.PersistableBundle;
+
+import io.v.baku.toolkit.bind.SyncbaseBinding;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * A default application of {@link BakuActivityTrait} extending
+ * {@link android.support.v7.app.AppCompatActivity}.
+ */
+@Accessors(prefix = "m")
+@Slf4j
+public abstract class BakuAppCompatActivity
+        extends VAppCompatActivity implements BakuActivityMixin {
+    @Getter
+    private BakuActivityTrait mBakuActivityTrait;
+
+    protected BakuActivityTrait createBakuActivityTrait() {
+        return new BakuActivityTrait(getVAndroidContextTrait());
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mBakuActivityTrait = createBakuActivityTrait();
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
+        super.onCreate(savedInstanceState, persistentState);
+        mBakuActivityTrait = createBakuActivityTrait();
+    }
+
+    @Override
+    protected void onDestroy() {
+        mBakuActivityTrait.close();
+        super.onDestroy();
+    }
+
+    public <T> SyncbaseBinding.Builder<T> binder() {
+        return getBakuActivityTrait().binder();
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessedActivityTrait.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessedActivityTrait.java
new file mode 100644
index 0000000..bfbbbd5
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessedActivityTrait.java
@@ -0,0 +1,145 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.app.Activity;
+
+import io.v.android.libs.security.BlessingsManager;
+import io.v.android.v23.services.blessing.BlessingCreationException;
+import io.v.v23.security.Blessings;
+import io.v.v23.verror.VException;
+import lombok.Getter;
+import lombok.Synchronized;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import rx.Observable;
+import rx.subjects.PublishSubject;
+
+@Accessors(prefix = "m")
+@Slf4j
+public class BlessedActivityTrait implements BlessingsProvider {
+    @Getter
+    private final Activity mActivity;
+    @Getter
+    private final ErrorReporter mErrorReporter;
+
+    /**
+     * An observable that, when subscribed to, refreshes the blessing. If the account manager needs
+     * to be invoked for the refresh, the subscription will not produce results until the invocation
+     * completes. Subsequently, it will receive all blessings refreshed via
+     * {@link #refreshBlessings()} and other subscriptions to {@link #getRxBlessings()}.
+     */
+    @Getter
+    private final Observable<Blessings> mRxBlessings;
+    /**
+     * An observable for the blessings that does not refresh when subscribed to. Upon subscription,
+     * this will produce the last known blessing. It will subsequently receive all blessings
+     * refreshed via {@link #refreshBlessings()} and subscriptions to {@link #getRxBlessings()}.
+     */
+    @Getter
+    private final Observable<Blessings> mPassiveRxBlessings;
+
+    private final PublishSubject<Observable<Blessings>> mPub;
+    private Blessings mLastBlessings;
+    private Observable<Blessings> mCurrentSeek;
+    private final Object mSeekLock = new Object();
+
+    public BlessedActivityTrait(final Activity activity, final ErrorReporter errorReporter) {
+        mActivity = activity;
+        mErrorReporter = errorReporter;
+
+        mPub = PublishSubject.create();
+        mPassiveRxBlessings = Observable.switchOnNext(mPub)
+                .distinctUntilChanged()
+                .replay(1).autoConnect();
+        /* It might make more sense for b -> mLastBlessings = b to be an onNext before the above
+        replay rather than a subscription (especially if we start getting
+        OnErrorNotImplementedException or have to include a possibly redundant error reporter).
+        However, replay, even with autoConnect(0), does not offer backpressure support unless it has
+        subscribers. We can get around this by adding .onBackpressureBuffer(1), but if this turns
+        out to be a better way of doing this, we should submit an issue requesting that
+        OperatorReplay use its buffer size for backpressure. */
+        mPassiveRxBlessings.subscribe(b -> mLastBlessings = b);
+        mRxBlessings = Observable.defer(this::refreshBlessings)
+                .ignoreElements()
+                .concatWith(mPassiveRxBlessings);
+    }
+
+    @Synchronized("mSeekLock")
+    public Observable<Blessings> refreshBlessings() {
+        if (mCurrentSeek != null) {
+            return mCurrentSeek;
+        }
+
+        Blessings mgrBlessings;
+        try {
+            mgrBlessings = BlessingsManager.getBlessings(mActivity.getApplicationContext());
+        } catch (final VException e) {
+            log.warn("Could not get blessings from shared preferences", e);
+            mgrBlessings = null;
+        }
+
+        final Observable<Blessings> nextBlessings;
+
+        if (mgrBlessings == null) {
+            mCurrentSeek = nextBlessings = seekBlessings()
+                    .first() // Longer-lived providers should consider a more direct implementation
+                            // of BlessingsProvider.
+                    .onErrorResumeNext(this::handleBlessingsError)
+                    .doOnNext(this::afterSeekBlessings)
+                    .replay(1).autoConnect();
+        } else {
+            nextBlessings = Observable.just(mgrBlessings);
+        }
+        mPub.onNext(nextBlessings);
+
+        return nextBlessings;
+    }
+
+    protected Observable<Blessings> seekBlessings() {
+        final BlessingRequestFragment brf = new BlessingRequestFragment();
+        mActivity.getFragmentManager().beginTransaction()
+                .add(brf, null)
+                .commit();
+        return brf.getObservable();
+    }
+
+    protected Observable<Blessings> handleBlessingsError(final Throwable t) {
+        if (t instanceof BlessingCreationException) {
+            /* This exception can occur if a user hits "Deny" in Blessings Manager, so don't treat
+            it as an error if we have a fallback. */
+            if (mLastBlessings == null) {
+                mErrorReporter.onError(R.string.err_blessings_required, t);
+            } else {
+                log.warn("Could not create blessings", t);
+            }
+        } else if (t instanceof VException) {
+            mErrorReporter.onError(R.string.err_blessings_decode, t);
+        } else {
+            return Observable.error(t);
+        }
+
+        if (mLastBlessings == null) {
+            mActivity.finish();
+            /* Block while the app exits, as opposed to returning an error that would be reported
+            (redundantly) elsewhere. */
+            return Observable.never();
+        } else {
+            return Observable.just(mLastBlessings);
+        }
+    }
+
+    private void afterSeekBlessings(final Blessings b) {
+        try {
+            BlessingsManager.addBlessings(mActivity.getApplicationContext(), b);
+        } catch (final VException e) {
+            mErrorReporter.onError(R.string.err_blessings_store, e);
+        } finally {
+            synchronized (mSeekLock) {
+                mCurrentSeek = null;
+            }
+        }
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessingRequestFragment.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessingRequestFragment.java
new file mode 100644
index 0000000..f046e72
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessingRequestFragment.java
@@ -0,0 +1,58 @@
+package io.v.baku.toolkit;
+
+import android.app.Fragment;
+import android.content.Intent;
+import android.os.Bundle;
+
+import io.v.android.v23.services.blessing.BlessingService;
+import io.v.v23.security.Blessings;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+import rx.subjects.ReplaySubject;
+
+/**
+ * Utility fragment for seeking blessings from the Vanadium Account Manager. This fragment is
+ * short-lived, starting the account manager activity in {@link #onCreate(Bundle)} and removing
+ * itself in {@link #onActivityResult(int, int, Intent)}.
+ */
+@Accessors(prefix = "m")
+public class BlessingRequestFragment extends Fragment {
+    private static final int BLESSING_REQUEST = 0;
+
+    @Getter
+    private final ReplaySubject<Blessings> mObservable = ReplaySubject.createWithSize(1);
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final Intent intent = BlessingService.newBlessingIntent(getActivity());
+        startActivityForResult(intent, BLESSING_REQUEST);
+    }
+
+    @Override
+    public void onDestroy() {
+        mObservable.onCompleted();
+        super.onDestroy();
+    }
+
+    @Override
+    public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+
+        switch (requestCode) {
+            case BLESSING_REQUEST:
+                try {
+                    mObservable.onNext(BlessingsUtils.fromActivityResult(resultCode, data));
+                    mObservable.onCompleted();
+                } catch (final Exception e) {
+                    mObservable.onError(e);
+                }
+                getFragmentManager().beginTransaction()
+                        .remove(this)
+                        .commit();
+                break;
+            default:
+        }
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessingsProvider.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessingsProvider.java
new file mode 100644
index 0000000..340971e
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessingsProvider.java
@@ -0,0 +1,24 @@
+// Copyright 2015 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.baku.toolkit;
+
+import io.v.v23.security.Blessings;
+import rx.Observable;
+
+public interface BlessingsProvider {
+    /**
+     * @return the connect-on-subscribe observable that provides blessings. This observable should
+     * subsequently emit the same blessings as {@link #getPassiveRxBlessings()}. It might be simpler
+     * to provide only a passive observable and a refresh method, but the more common use case is to
+     * require up-to-date blessings on subscription.
+     */
+    Observable<Blessings> getRxBlessings();
+
+    /**
+     * @return the passive observable that provides blessings. This observable should emit any
+     * blessings attained through active refreshes or through {@link #getRxBlessings()}.
+     */
+    Observable<Blessings> getPassiveRxBlessings();
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessingsUtils.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessingsUtils.java
new file mode 100644
index 0000000..3567471
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/BlessingsUtils.java
@@ -0,0 +1,145 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.content.Intent;
+
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import io.v.android.v23.V;
+import io.v.android.v23.services.blessing.BlessingCreationException;
+import io.v.android.v23.services.blessing.BlessingService;
+import io.v.impl.google.naming.NamingUtil;
+import io.v.v23.context.VContext;
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.security.Blessings;
+import io.v.v23.security.VPrincipal;
+import io.v.v23.security.VSecurity;
+import io.v.v23.security.access.AccessList;
+import io.v.v23.security.access.Constants;
+import io.v.v23.security.access.Permissions;
+import io.v.v23.security.access.Tag;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+import java8.util.stream.Collectors;
+import java8.util.stream.Stream;
+import java8.util.stream.StreamSupport;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Common utilities for blessing and ACL management. In the future, we may want to factor these out
+ * of Baku into the V23 Java libraries.
+ */
+@Slf4j
+@UtilityClass
+public class BlessingsUtils {
+    public static final Pattern DEV_V_IO_USER = Pattern.compile("dev\\.v\\.io:u:([^:]+).*");
+
+    public static final AccessList OPEN_ACL = new AccessList(
+            ImmutableList.of(new BlessingPattern("...")), ImmutableList.of());
+
+    public static final ImmutableSet<Tag>
+            DATA_TAGS = ImmutableSet.of(Constants.READ, Constants.WRITE, Constants.ADMIN),
+            MOUNT_TAGS = ImmutableSet.of(Constants.READ, Constants.RESOLVE, Constants.ADMIN),
+            SYNCGROUP_TAGS = ImmutableSet.of(Constants.READ, Constants.WRITE, Constants.RESOLVE,
+                    Constants.ADMIN, Constants.DEBUG);
+
+    public static final Permissions
+            OPEN_DATA_PERMS = dataPermissions(OPEN_ACL),
+            OPEN_MOUNT_PERMS = mountPermissions(OPEN_ACL);
+
+    public static Blessings fromActivityResult(final int resultCode, final Intent data)
+            throws BlessingCreationException, VException {
+        // The Account Manager will pass us the blessings to use as an array of bytes.
+        return decodeBlessings(BlessingService.extractBlessingReply(resultCode, data));
+    }
+
+    public static Blessings decodeBlessings(final byte[] blessings) throws VException {
+        return (Blessings)VomUtil.decode(blessings, Blessings.class);
+    }
+
+    public static Set<String> getBlessingNames(final VContext ctx, final Blessings blessings) {
+        return ImmutableSet.copyOf(VSecurity.getBlessingNames(V.getPrincipal(ctx), blessings));
+    }
+
+    public static AccessList blessingsToAcl(final VContext ctx, final Blessings blessings) {
+        return new AccessList(ImmutableList.copyOf(Collections2.transform(
+                getBlessingNames(ctx, blessings),
+                s -> new BlessingPattern(s))), //method reference confuses Android Studio
+                ImmutableList.of());
+    }
+
+    public static Stream<String> blessingsToUsernameStream(final VContext ctx,
+                                                           final Blessings blessings) {
+        return StreamSupport.stream(getBlessingNames(ctx, blessings))
+                .map(DEV_V_IO_USER::matcher)
+                .filter(Matcher::matches)
+                .map(m -> m.group(1));
+    }
+
+    /**
+     * This method finds and parses all blessings of the form dev.v.io/u/.... This is different from
+     * the method at https://v.io/tutorials/java/android.html, which can return additional
+     * extensions ("/android").
+     */
+    public static Set<String> blessingsToUsernames(final VContext ctx, final Blessings blessings) {
+        return blessingsToUsernameStream(ctx, blessings).collect(Collectors.toSet());
+    }
+
+    public static String userMount(final String username) {
+        return NamingUtil.join("users", username);
+    }
+
+    public static Stream<String> blessingsToUserMountStream(final VContext ctx, final Blessings blessings) {
+        return blessingsToUsernameStream(ctx, blessings)
+                .map(BlessingsUtils::userMount);
+    }
+
+    public static Set<String> blessingsToUserMounts(final VContext ctx, final Blessings blessings) {
+        return blessingsToUserMountStream(ctx, blessings).collect(Collectors.toSet());
+    }
+
+    public static Permissions homogeneousPermissions(final Set<Tag> tags, final AccessList acl) {
+        return new Permissions(Maps.toMap(Collections2.transform(tags, Tag::getValue), x -> acl));
+    }
+
+    public static Permissions dataPermissions(final AccessList acl) {
+        return homogeneousPermissions(DATA_TAGS, acl);
+    }
+
+    public static Permissions mountPermissions(final AccessList acl) {
+        return homogeneousPermissions(MOUNT_TAGS, acl);
+    }
+
+    public static Permissions syncgroupPermissions(final AccessList acl) {
+        return homogeneousPermissions(SYNCGROUP_TAGS, acl);
+    }
+
+    /**
+     * Standard blessing handling for Vanadium applications:
+     * <ul>
+     *     <li>Provide the given blessings when anybody connects to us.</li>
+     *     <li>Provide these blessings when we connect to other services (for example, when we talk
+     *     to the mounttable).</li>
+     *     <li>Trust these blessings and all the "parent" blessings.</li>
+     * </ul>
+     */
+    public static void assumeBlessings(final VContext vContext, final Blessings blessings)
+            throws VException {
+        log.info("Assuming blessings: " + blessings);
+        final VPrincipal principal = V.getPrincipal(vContext);
+        principal.blessingStore().setDefaultBlessings(blessings);
+        principal.blessingStore().set(blessings, new BlessingPattern("..."));
+        VSecurity.addToRoots(principal, blessings);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugFragment.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugFragment.java
new file mode 100644
index 0000000..ac42765
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugFragment.java
@@ -0,0 +1,42 @@
+package io.v.baku.toolkit;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class DebugFragment extends Fragment {
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        inflater.inflate(R.menu.debug, menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Can't use a switch statement since IDs are not constant in Android libary modules.
+        final int id = item.getItemId();
+        if (id == R.id.clear_app_data) {
+            DebugUtils.clearAppData(getActivity());
+            return true;
+        } else if (id == R.id.kill_process) {
+            DebugUtils.killProcess(getActivity());
+            return true;
+        } else if (id == R.id.logging) {
+            new DebugLogDialogFragment().show(getFragmentManager(), null);
+            return true;
+        } else {
+            return super.onOptionsItemSelected(item);
+        }
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugLogDialogFragment.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugLogDialogFragment.java
new file mode 100644
index 0000000..6e587ab
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugLogDialogFragment.java
@@ -0,0 +1,68 @@
+package io.v.baku.toolkit;
+
+
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+/**
+ * A {@link DialogFragment} surfacing logging options via a {@link DebugLogPreferenceFragment},
+ * Reset and Restart action buttons, and logcat via a {@link LogCatFragment}.
+ */
+public class DebugLogDialogFragment extends DialogFragment {
+    private static final String DEBUG_LOG_PF_TAG = "DebugLogPreferences";
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final Dialog dialog = super.onCreateDialog(savedInstanceState);
+        dialog.setTitle(R.string.logging);
+        return dialog;
+    }
+
+    private void wireInEventHandlers(final View view) {
+        final Button reset = (Button) view.findViewById(R.id.debug_log_reset);
+        if (reset != null) {
+            reset.setOnClickListener(v -> ((DebugLogPreferenceFragment) getChildFragmentManager()
+                    .findFragmentByTag(DEBUG_LOG_PF_TAG)).reset());
+        }
+
+        final Button restart = (Button) view.findViewById(R.id.debug_log_restart);
+        if (restart != null) {
+            restart.setOnClickListener(v -> DebugUtils.restartProcess(getActivity()));
+        }
+    }
+
+    @Override
+    public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+                             final Bundle savedInstanceState) {
+        final View view = inflater.inflate(R.layout.dialog_debug_log, container);
+        wireInEventHandlers(view);
+        return view;
+    }
+
+    private void wireInSubFragments(final Bundle savedInstanceState) {
+        final FragmentTransaction ft = getChildFragmentManager().beginTransaction();
+
+        if (getView().findViewById(R.id.pref_logging_container) != null &&
+                getChildFragmentManager().findFragmentByTag(DEBUG_LOG_PF_TAG) == null) {
+            ft.replace(R.id.pref_logging_container,
+                    new DebugLogPreferenceFragment(),
+                    DEBUG_LOG_PF_TAG);
+        }
+        if (savedInstanceState == null) {
+            ft.replace(R.id.logcat_container, new LogCatFragment());
+        }
+        ft.commit();
+    }
+
+    @Override
+    public void onActivityCreated(final Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        wireInSubFragments(savedInstanceState);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugLogPreferenceFragment.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugLogPreferenceFragment.java
new file mode 100644
index 0000000..49e2876
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugLogPreferenceFragment.java
@@ -0,0 +1,83 @@
+package io.v.baku.toolkit;
+
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import io.v.v23.OptionDefs;
+
+/**
+ * A {@link PreferenceFragment} surfacing logging options.
+ * <ul>
+ * <li>VLEVEL - verbosity for vlog</li>
+ * <li>VMODULE - per-module log verbosities</li>
+ * </ul>
+ */
+public class DebugLogPreferenceFragment extends PreferenceFragment
+        implements SharedPreferences.OnSharedPreferenceChangeListener {
+    @Override
+    public void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        final PreferenceManager pm = getPreferenceManager();
+        pm.setSharedPreferencesName(VAndroidContextTrait.VANADIUM_OPTIONS_SHARED_PREFS);
+        pm.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
+        createPreferences();
+    }
+
+    private void createPreferences() {
+        addPreferencesFromResource(R.xml.pref_logging);
+        final SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences();
+        updateVLevelSummary(sharedPreferences, findPreference(OptionDefs.LOG_VLEVEL));
+        updateVModuleSummary(sharedPreferences, findPreference(OptionDefs.LOG_VMODULE));
+    }
+
+    @Override
+    public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+                             final Bundle savedInstanceState) {
+        final View view = super.onCreateView(inflater, container, savedInstanceState);
+        view.setBackgroundColor(getResources().getColor(android.R.color.background_light));
+
+        return view;
+    }
+
+    private static void updateVLevelSummary(final SharedPreferences sharedPreferences,
+                                            final Preference pref) {
+        pref.setSummary(VOptionPreferenceUtils.readVLevel(sharedPreferences)
+                .map(Object::toString)
+                .orElse(null));
+    }
+
+    private static void updateVModuleSummary(final SharedPreferences sharedPreferences,
+                                             final Preference pref) {
+        pref.setSummary(VOptionPreferenceUtils.readVModule(sharedPreferences).orElse(null));
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+        final Preference pref = findPreference(key);
+        if (pref == null) {
+            return;
+        }
+
+        if (OptionDefs.LOG_VLEVEL.equals(key)) {
+            updateVLevelSummary(sharedPreferences, pref);
+        } else if (OptionDefs.LOG_VMODULE.equals(key)) {
+            updateVModuleSummary(sharedPreferences, pref);
+        }
+    }
+
+    public void reset() {
+        getPreferenceManager().getSharedPreferences().edit()
+                .clear()
+                .commit();
+        // The easiest way to resync the preferences seems to be:
+        getPreferenceScreen().removeAll();
+        createPreferences();
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugUtils.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugUtils.java
new file mode 100644
index 0000000..fa2f6ac
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/DebugUtils.java
@@ -0,0 +1,88 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+
+import java.io.IOException;
+
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@UtilityClass
+public class DebugUtils {
+    /**
+     * Determines whether the top-level apk was built in debug mode. This is preferable to
+     * {@code BuildConfig.DEBUG} as that is only available per module.
+     */
+    public static boolean isApkDebug(final Context context) {
+        return (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
+    }
+
+    /**
+     * Clears all data for this app. This will force the application to close. It would be nice to
+     * be able to schedule a restart as well, but clearing app data clears any scheduled intents.
+     * <p>
+     * When debugging Vanadium applications, this is often useful to clear the app blessings cache
+     * and Syncbase data.
+     */
+    public static void clearAppData(final Context context) {
+        try {
+            Runtime.getRuntime().exec("pm clear " + context.getPackageName());
+        } catch (final IOException e) {
+            if (context instanceof VAndroidContextMixin) {
+                ((VAndroidContextMixin) context).getVAndroidContextTrait()
+                        .getErrorReporter()
+                        .onError(R.string.err_app_clear, e);
+            } else {
+                log.error("Unable to clear app data", e);
+            }
+        }
+    }
+
+    public static void stopPackageServices(final Context context) {
+        final PackageInfo pkg;
+        try {
+            pkg = context.getPackageManager().getPackageInfo(context.getPackageName(),
+                    PackageManager.GET_SERVICES);
+        } catch (final PackageManager.NameNotFoundException e) {
+            log.warn("Unable to enumerate package components", e);
+            return;
+        }
+
+        for (final ServiceInfo svc : pkg.services) {
+            context.stopService(new Intent().setClassName(context, svc.name));
+        }
+    }
+
+    /**
+     * Terminates the JVM for this app. When debugging Vanadium applications, this is useful for
+     * terminating any long-lived Android services that might be attached to the app process
+     * (e.g. Syncbase).
+     * <p>
+     * This additionally stops all services defined in the (merged) package manifest before
+     * terminating the process. Services default to {@link android.app.Service#START_STICKY}/{@link
+     * android.app.Service#START_STICKY_COMPATIBILITY}, which would result in started services
+     * auto-restarting after process termination. This could lead to unconventional initialization
+     * order, which is not something we necessarily want to exercise while debugging.
+     */
+    public static void killProcess(final Context context) {
+        stopPackageServices(context);
+        System.exit(0);
+    }
+
+    public static void restartProcess(final Context context) {
+        final Intent i = context.getPackageManager().getLaunchIntentForPackage(
+                context.getPackageName());
+        context.startActivity(i);
+        killProcess(context);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/ErrorReporter.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/ErrorReporter.java
new file mode 100644
index 0000000..adceed3
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/ErrorReporter.java
@@ -0,0 +1,51 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.content.Context;
+import android.widget.Toast;
+
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.extern.slf4j.Slf4j;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.subjects.PublishSubject;
+import rx.subjects.Subject;
+
+@Slf4j
+public class ErrorReporter {
+    @AllArgsConstructor
+    @EqualsAndHashCode
+    private static class ErrorEntry {
+        private final int summaryStringId;
+        private final Throwable error;
+    }
+
+    private final Context mContext;
+    private final Subject<ErrorEntry, ErrorEntry> mErrors;
+
+    public ErrorReporter(final Context context) {
+        mContext = context;
+
+        mErrors = PublishSubject.create();
+        mErrors.distinctUntilChanged()
+                .onBackpressureBuffer()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(e -> reportError(e.summaryStringId, e.error),
+                        t -> reportError(R.string.err_misc, t));
+    }
+
+    /**
+     * @param summaryStringId string resource ID for the error summary
+     */
+    public void onError(final int summaryStringId, final Throwable t) {
+        mErrors.onNext(new ErrorEntry(summaryStringId, t));
+    }
+
+    protected void reportError(final int summaryStringId, final Throwable t) {
+        log.error(mContext.getString(summaryStringId), t);
+        Toast.makeText(mContext, summaryStringId, Toast.LENGTH_LONG).show();
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/LogCatFragment.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/LogCatFragment.java
new file mode 100644
index 0000000..db0590e
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/LogCatFragment.java
@@ -0,0 +1,133 @@
+package io.v.baku.toolkit;
+
+import android.app.ListFragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.ArrayAdapter;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+import com.google.common.base.Throwables;
+
+import org.joda.time.Duration;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+import lombok.extern.slf4j.Slf4j;
+import rx.Observable;
+import rx.Subscription;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.schedulers.Schedulers;
+
+@Slf4j
+public class LogCatFragment extends ListFragment {
+    private static final int
+            INITIAL_BUFFER = 1024,
+            MAX_SMOOTH_SCROLL = 64;
+    private static final Duration LOGCAT_UPDATE_PERIOD = Duration.millis(100);
+
+    // http://stackoverflow.com/questions/12692103/read-logcat-programmatically-within-application
+    private static final Observable<String> RX_LOG_CAT = Observable.<String>create(subscriber -> {
+        try {
+            final Process process = Runtime.getRuntime().exec("logcat");
+            try (final BufferedReader bufferedReader =
+                         new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+                String line;
+                while (!subscriber.isUnsubscribed() && (line = bufferedReader.readLine()) != null) {
+                    subscriber.onNext(line);
+                }
+            }
+            subscriber.onCompleted();
+        } catch (final IOException e) {
+            subscriber.onError(e);
+        }
+    });
+
+    private Subscription mLogCatSubscription;
+    private boolean mFollow = true;
+    private final AbsListView.OnScrollListener mScrollListener =
+            new AbsListView.OnScrollListener() {
+                private int mPrevious;
+                @Override
+                public void onScrollStateChanged(final AbsListView view, final int scrollState) {
+                }
+
+                @Override
+                public void onScroll(final AbsListView view, final int firstVisibleItem,
+                                     final int visibleItemCount, final int totalItemCount) {
+                    final boolean atBottom = visibleItemCount == 0 ||
+                            firstVisibleItem + visibleItemCount == totalItemCount;
+                    if (firstVisibleItem < mPrevious) {
+                        mFollow = false;
+                    } else if (atBottom) {
+                        mFollow = true;
+                    }
+                    mPrevious = firstVisibleItem;
+                }
+            };
+
+    private ListAdapter createAdapter() {
+        final ArrayList<String> buffer = new ArrayList<>(INITIAL_BUFFER);
+        final ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity(),
+                android.R.layout.simple_list_item_1, android.R.id.text1, buffer);
+        mLogCatSubscription = RX_LOG_CAT
+                .buffer(LOGCAT_UPDATE_PERIOD.getMillis(), TimeUnit.MILLISECONDS)
+                .filter(e -> !e.isEmpty())
+                .subscribeOn(Schedulers.io())
+                .onBackpressureBuffer()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(
+                        s -> {
+                            buffer.addAll(s);
+                            adapter.notifyDataSetChanged();
+                            if (mFollow) {
+                                final ListView listView = getListView();
+                                if (buffer.size() - listView.getLastVisiblePosition() >
+                                        MAX_SMOOTH_SCROLL) {
+                                    listView.setSelection(buffer.size() - 1);
+                                } else {
+                                    listView.smoothScrollToPosition(buffer.size() - 1);
+                                }
+                            }
+                        },
+                        t -> {
+                            buffer.add(Throwables.getStackTraceAsString(t));
+                            log.error("Error while following logs", t);
+                        });
+        return adapter;
+    }
+
+    @Override
+    public void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setListAdapter(createAdapter());
+    }
+
+    @Override
+    public void onDestroy() {
+        mLogCatSubscription.unsubscribe();
+        super.onDestroy();
+    }
+
+    @Override
+    public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+                             final Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.logcat, container, false);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        if (mFollow) {
+            setSelection(getListAdapter().getCount() - 1);
+        }
+        getListView().setOnScrollListener(mScrollListener);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/VActivity.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/VActivity.java
new file mode 100644
index 0000000..e02f1aa
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/VActivity.java
@@ -0,0 +1,37 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+
+import lombok.Getter;
+import lombok.experimental.Accessors;
+
+/**
+ * A default application of {@link VAndroidContextTrait} extending {@link Activity}.
+ */
+@Accessors(prefix = "m")
+public abstract class VActivity extends Activity implements VAndroidContextMixin {
+    @Getter
+    private VAndroidContextTrait<Activity> mVAndroidContextTrait;
+
+    protected VAndroidContextTrait<Activity> createVActivityTrait(final Bundle savedInstanceState) {
+        return VAndroidContextTrait.withDefaults(this, savedInstanceState);
+    }
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mVAndroidContextTrait = createVActivityTrait(savedInstanceState);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
+        super.onCreate(savedInstanceState, persistentState);
+        mVAndroidContextTrait = createVActivityTrait(savedInstanceState);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/VAndroidContextMixin.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/VAndroidContextMixin.java
new file mode 100644
index 0000000..553c3cf
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/VAndroidContextMixin.java
@@ -0,0 +1,12 @@
+// Copyright 2015 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.baku.toolkit;
+
+/**
+ * An optional convenience interface for classes mixing in {@link VAndroidContextTrait}.
+ */
+public interface VAndroidContextMixin {
+    VAndroidContextTrait getVAndroidContextTrait();
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/VAndroidContextTrait.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/VAndroidContextTrait.java
new file mode 100644
index 0000000..56fb72a
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/VAndroidContextTrait.java
@@ -0,0 +1,125 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+
+import io.v.android.v23.V;
+import io.v.v23.OptionDefs;
+import io.v.v23.Options;
+import io.v.v23.context.VContext;
+import io.v.v23.security.Blessings;
+import io.v.v23.verror.VException;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Android context trait incorporating common Vanadium utilities:
+ * <ul>
+ * <li>Vanadium initialization during {@code onCreate}; context available via
+ * {@code getVContext}</li>
+ * <li>Blessings management, available via {@code getBlessingsProvider().getRxBlessings}.
+ * Upon {@code subscribe}, blessings are refreshed from the {@code BlessingsManager} or sought from
+ * the {@code BlessingsProvider} (by default, the Account Manager).</li>
+ * </ul>
+ * <p>
+ * Default activity extensions incorporating this mix-in are available:
+ * <ul>
+ * <li>{@link VActivity} (extends {@link Activity})</li>
+ * <li>{@link VAppCompatActivity} (extends {@link android.support.v7.app.AppCompatActivity})</li>
+ * </ul>
+ * Since Java doesn't actually support multiple inheritance, clients requiring custom inheritance
+ * hierarchies will need to wire in manually, like any of the examples above.
+ */
+@Accessors(prefix = "m")
+@Slf4j
+public class VAndroidContextTrait<T extends Context> {
+    public static final String VANADIUM_OPTIONS_SHARED_PREFS = "VanadiumOptions";
+
+    public static SharedPreferences getVanadiumPreferences(final Context androidContext) {
+        return androidContext.getSharedPreferences(VANADIUM_OPTIONS_SHARED_PREFS,
+                Context.MODE_PRIVATE);
+    }
+
+    @Getter
+    private final T mAndroidContext;
+    @Getter
+    private final BlessingsProvider mBlessingsProvider;
+    @Getter
+    private final ErrorReporter mErrorReporter;
+    @Getter
+    private final SharedPreferences mVanadiumPreferences;
+    @Getter
+    private final VContext mVContext;
+
+    public Options getSavedOptions() {
+        return VOptionPreferenceUtils.getOptionsFromPreferences(mVanadiumPreferences);
+    }
+
+    private VContext vinit() {
+        try {
+            return V.init(mAndroidContext, getSavedOptions());
+        } catch (final RuntimeException e) {
+            try {
+                /* V.shutdown so we might try V.init again if warranted. If we don't V.shutdown
+                first, the process can abruptly die. It seems that if this happens, Android might
+                just restart the app immediately, i.e. before we've been able to display an
+                intelligible error message. */
+                V.shutdown();
+            } catch (final RuntimeException e2) {
+                log.error("Unable to clean up failed Vanadium initialization", e2);
+                e.addSuppressed(e2);
+            }
+
+            if (mVanadiumPreferences.getAll().isEmpty()) {
+                throw e;
+            } else {
+                mErrorReporter.reportError(R.string.err_vinit_options, e);
+                // Don't actually clear/fix options here; leave that to the user
+                return V.init(mAndroidContext);
+            }
+        }
+    }
+
+    public VAndroidContextTrait(final T androidContext, final BlessingsProvider blessingsProvider,
+                                final ErrorReporter errorReporter) {
+        mAndroidContext = androidContext;
+        mBlessingsProvider = blessingsProvider;
+        mErrorReporter = errorReporter;
+
+        mVanadiumPreferences = getVanadiumPreferences(mAndroidContext);
+        mVContext = vinit();
+
+        //Any time our blessings change, we need to attach them to our principal.
+        mBlessingsProvider.getPassiveRxBlessings().subscribe(this::processBlessings,
+                t -> errorReporter.onError(R.string.err_blessings_misc, t));
+    }
+
+    protected void processBlessings(final Blessings blessings) {
+        try {
+            BlessingsUtils.assumeBlessings(mVContext, blessings);
+        } catch (final VException e) {
+            mErrorReporter.onError(R.string.err_blessings_assume, e);
+        }
+    }
+
+    public static <T extends Activity> VAndroidContextTrait<T> withDefaults(
+            final T activity, final Bundle savedInstanceState) {
+        if (DebugUtils.isApkDebug(activity) && savedInstanceState == null) {
+            log.info("Debug menu enabled");
+            activity.getFragmentManager().beginTransaction()
+                    .add(new DebugFragment(), null)
+                    .commit();
+        }
+        final ErrorReporter errorReporter = new ErrorReporter(activity);
+        final BlessingsProvider blessingsProvider =
+                new BlessedActivityTrait(activity, errorReporter);
+        return new VAndroidContextTrait<>(activity, blessingsProvider, errorReporter);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/VAppCompatActivity.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/VAppCompatActivity.java
new file mode 100644
index 0000000..4d25a40
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/VAppCompatActivity.java
@@ -0,0 +1,41 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.support.v7.app.AppCompatActivity;
+
+import lombok.Getter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * A default application of {@link VAndroidContextTrait} extending
+ * {@link android.support.v7.app.AppCompatActivity}.
+ */
+@Accessors(prefix = "m")
+@Slf4j
+public abstract class VAppCompatActivity extends AppCompatActivity implements VAndroidContextMixin {
+    @Getter
+    private VAndroidContextTrait<AppCompatActivity> mVAndroidContextTrait;
+
+    protected VAndroidContextTrait<AppCompatActivity> createVActivityTrait(
+            final Bundle savedInstanceState) {
+        return VAndroidContextTrait.withDefaults(this, savedInstanceState);
+    }
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mVAndroidContextTrait = createVActivityTrait(savedInstanceState);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
+        super.onCreate(savedInstanceState, persistentState);
+        mVAndroidContextTrait = createVActivityTrait(savedInstanceState);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/VOptionPreferenceUtils.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/VOptionPreferenceUtils.java
new file mode 100644
index 0000000..a683f48
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/VOptionPreferenceUtils.java
@@ -0,0 +1,44 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.content.SharedPreferences;
+
+import com.google.common.base.Strings;
+
+import io.v.v23.OptionDefs;
+import io.v.v23.Options;
+import java8.util.Optional;
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class VOptionPreferenceUtils {
+    public static Optional<Integer> readVLevel(final SharedPreferences prefs) {
+        final String raw = prefs.getString(OptionDefs.LOG_VLEVEL, "");
+        try {
+            return Optional.of(Integer.parseInt(raw));
+        } catch (final NumberFormatException|NullPointerException e) {
+            return Optional.empty();
+        }
+    }
+
+    public static Optional<String> readVModule(final SharedPreferences prefs) {
+        final String raw = prefs.getString(OptionDefs.LOG_VMODULE, "");
+        if (Strings.isNullOrEmpty(raw)) {
+            return Optional.empty();
+        } else {
+            return Optional.of(raw);
+        }
+    }
+
+    public static Options getOptionsFromPreferences(final SharedPreferences prefs) {
+        final Options opts = new Options();
+        /* It would be nice to copy this map naively, but Vanadium options are type-specific and
+        Android stores some numeric preferences as strings. */
+        readVLevel(prefs).ifPresent(v -> opts.set(OptionDefs.LOG_VLEVEL, v));
+        readVModule(prefs).ifPresent(v -> opts.set(OptionDefs.LOG_VMODULE, v));
+        return opts;
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/CoordinatorChain.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/CoordinatorChain.java
new file mode 100644
index 0000000..e531251
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/CoordinatorChain.java
@@ -0,0 +1,12 @@
+// Copyright 2015 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.baku.toolkit.bind;
+
+import java8.lang.FunctionalInterface;
+import java8.util.function.UnaryOperator;
+
+@FunctionalInterface
+public interface CoordinatorChain<T> extends UnaryOperator<TwoWayBinding<T>> {
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/DebouncingCoordinator.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/DebouncingCoordinator.java
new file mode 100644
index 0000000..3cb9203
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/DebouncingCoordinator.java
@@ -0,0 +1,68 @@
+// Copyright 2015 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.baku.toolkit.bind;
+
+import org.joda.time.Duration;
+
+import java.util.concurrent.TimeUnit;
+
+import io.v.rx.syncbase.WatchEvent;
+import lombok.RequiredArgsConstructor;
+import rx.Observable;
+import rx.Subscription;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.subjects.ReplaySubject;
+
+/**
+ * If input events arrive too quickly, the resulting watch updates can come back in a stutter, where
+ * older watch events arrive after the input has already been subsequently updated, reverting the
+ * view for a brief moment and messing with the cursor position.
+ * <p>
+ * A simple debounce on the uplink or downlink doesn't solve the problem because it effectively just
+ * adds a delay to the boundary condition. To prevent this, any update from the model must be
+ * throttled if there was a recent update from the view.
+ * <p>
+ * Unfortunately for rapid concurrent updates this can result in divergence which should be handled
+ * via conflict resolution or CRDT.
+ */
+@RequiredArgsConstructor
+public class DebouncingCoordinator<T> implements TwoWayBinding<T> {
+    public static final Duration DEFAULT_IO_DEBOUNCE = Duration.millis(500);
+
+    private final TwoWayBinding<T> mChild;
+    private final Duration mIoDebounce;
+
+    private final ReplaySubject<Observable<?>> mRxDebounce = ReplaySubject.createWithSize(1);
+    {
+        mRxDebounce.onNext(Observable.just(0)
+                .observeOn(AndroidSchedulers.mainThread()));
+        //We expect these timeouts to be on the main thread; see putDebounceWindow
+    }
+
+    public DebouncingCoordinator(final TwoWayBinding<T> child) {
+        this(child, DEFAULT_IO_DEBOUNCE);
+    }
+
+    private Observable<?> getDebounceWindow() {
+        return Observable.switchOnNext(mRxDebounce).first();
+    }
+
+    private void putDebounceWindow() {
+        mRxDebounce.onNext(Observable.timer(mIoDebounce.getMillis(), TimeUnit.MILLISECONDS,
+                AndroidSchedulers.mainThread()));
+        //Do timeouts on the main thread to ensure that timeouts don't clear while an input update
+        //is in progress.
+    }
+
+    @Override
+    public Observable<WatchEvent<T>> downlink() {
+        return mChild.downlink().debounce(s -> getDebounceWindow());
+    }
+
+    @Override
+    public Subscription uplink(final Observable<T> rxData) {
+        return mChild.uplink(rxData.doOnNext(d -> putDebounceWindow()));
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/SuppressWriteOnReadCoordinator.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/SuppressWriteOnReadCoordinator.java
new file mode 100644
index 0000000..f416831
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/SuppressWriteOnReadCoordinator.java
@@ -0,0 +1,37 @@
+// Copyright 2015 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.baku.toolkit.bind;
+
+import io.v.rx.syncbase.WatchEvent;
+import lombok.RequiredArgsConstructor;
+import rx.Observable;
+import rx.Subscription;
+
+/**
+ * If we don't suppress writes while responding to SB changes with Android widgets, we easily end
+ * up in update loops. To operate correctly, this coordinator must occur single-threaded with the
+ * widget binding layer.
+ */
+@RequiredArgsConstructor
+public class SuppressWriteOnReadCoordinator<T> implements TwoWayBinding<T> {
+    private final TwoWayBinding<T> mChild;
+
+    private boolean mSuppressWrites;
+
+    @Override
+    public Observable<WatchEvent<T>> downlink() {
+        final Observable<WatchEvent<T>> childDownlink = mChild.downlink();
+        return Observable.create(s -> s.add(childDownlink.subscribe(x -> {
+            mSuppressWrites = true;
+            s.onNext(x);
+            mSuppressWrites = false;
+        }, s::onError, s::onCompleted)));
+    }
+
+    @Override
+    public Subscription uplink(Observable<T> rxData) {
+        return mChild.uplink(rxData.filter(x -> !mSuppressWrites));
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/SyncbaseBinding.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/SyncbaseBinding.java
new file mode 100644
index 0000000..d66c75c
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/SyncbaseBinding.java
@@ -0,0 +1,227 @@
+// Copyright 2015 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.baku.toolkit.bind;
+
+import android.app.Activity;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.google.common.collect.Iterables;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import io.v.baku.toolkit.BakuActivityTrait;
+import io.v.rx.syncbase.RxTable;
+import rx.Subscription;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.functions.Action1;
+import rx.subscriptions.CompositeSubscription;
+
+public abstract class SyncbaseBinding {
+    /**
+     * The {@link #deleteValue(Object)} and {@link #defaultValue(Object)} options are minimally
+     * typesafe, but keeping this builder simple is preferable, and the absence of rigorous static
+     * type checking here is acceptable.
+     */
+    public static class Builder<T> {
+        private Activity mActivity;
+        private RxTable mRxTable;
+        private String mKey;
+        private boolean mExplicitDefaultValue;
+        private T mDeleteValue, mDefaultValue;
+        private final List<CoordinatorChain<T>> mCoordinators = new ArrayList<>();
+        private CompositeSubscription mSubscriptionParent;
+        private Action1<Throwable> mOnError;
+
+        public Builder<T> activity(final Activity activity) {
+            mActivity = activity;
+            return this;
+        }
+
+        public Builder<T> rxTable(final RxTable rxTable) {
+            mRxTable = rxTable;
+            return this;
+        }
+
+        public Builder<T> bakuActivity(final BakuActivityTrait trait) {
+            return activity(trait.getVAndroidContextTrait().getAndroidContext())
+                    .rxTable(trait.getSyncbaseTable())
+                    .subscriptionParent(trait.getSubscriptions())
+                    .onError(trait::onSyncError);
+        }
+
+        public Builder<T> key(final String key) {
+            mKey = key;
+            return this;
+        }
+
+        private String getKey(final Object fallback) {
+            return mKey == null ? fallback.toString() : mKey;
+        }
+
+        /**
+         * Note that although this option is generic and attempts to enforce some weak measure of
+         * type safety in normal use, type pollution may still result in a
+         * {@link ClassCastException} at build time.
+         */
+        @SuppressWarnings("unchecked")
+        public <U extends T> Builder<U> deleteValue(final U deleteValue) {
+            mDeleteValue = deleteValue;
+            return (Builder<U>) this;
+        }
+
+        private T getDefaultValue(final T fallback) {
+            return mExplicitDefaultValue ? mDefaultValue : fallback;
+        }
+
+        /**
+         * Note that although this option is generic and attempts to enforce some weak measure of
+         * type safety in normal use, type pollution may still result in a
+         * {@link ClassCastException} at build time.
+         */
+        @SuppressWarnings("unchecked")
+        public <U extends T> Builder<U> defaultValue(final U defaultValue) {
+            mDefaultValue = defaultValue;
+            mExplicitDefaultValue = true;
+            return (Builder<U>) this;
+        }
+
+        public <U extends T> Builder<U> zeroValue(final U zeroValue) {
+            return deleteValue(zeroValue).defaultValue(zeroValue);
+        }
+
+        @SafeVarargs
+        public final Builder<T> coordinators(final CoordinatorChain<T>... coordinators) {
+            mCoordinators.clear();
+            return chain(coordinators);
+        }
+
+        @SafeVarargs
+        public final Builder<T> chain(final CoordinatorChain<T>... coordinators) {
+            Collections.addAll(mCoordinators, coordinators);
+            return this;
+        }
+
+        public Builder<T> coordinators(final Iterable<CoordinatorChain<T>> coordinators) {
+            mCoordinators.clear();
+            return chain(coordinators);
+        }
+
+        public Builder<T> chain(final Iterable<CoordinatorChain<T>> coordinators) {
+            Iterables.addAll(mCoordinators, coordinators);
+            return this;
+        }
+
+        public Builder<T> subscriptionParent(final CompositeSubscription subscriptionParent) {
+            mSubscriptionParent = subscriptionParent;
+            return this;
+        }
+
+        private Subscription subscribe(final Subscription subscription) {
+            if (mSubscriptionParent != null) {
+                mSubscriptionParent.add(subscription);
+            }
+            return subscription;
+        }
+
+        public Builder<T> onError(final Action1<Throwable> onError) {
+            mOnError = onError;
+            return this;
+        }
+
+        public Builder<T> bindOneWay(final TextView textView) {
+            @SuppressWarnings("unchecked")
+            final Builder<String> t = (Builder<String>) this;
+
+            subscribe(TextViewBindingTermini.bindRead(textView,
+                    SyncbaseBindingTermini.bindRead(mRxTable, getKey(textView.getId()),
+                            String.class, t.getDefaultValue(""))
+                            .onBackpressureLatest()
+                            .observeOn(AndroidSchedulers.mainThread()),
+                    mOnError));
+
+            return this;
+        }
+
+        /**
+         * Constructs a two-way binding between a string in Syncbase and the text of a TextView.
+         * <p>
+         * Defaults:
+         * <ul>
+         * <li>{@code defaultValue}: {@code ""}</li>
+         * <li>{@code deleteValue}: {@code null}</li>
+         * <li>{@code coordinators}: {@code DebouncingCoordinator}, and ensures that there is a
+         * {@code SuppressWriteOnReadCoordinator} somewhere in the chain, injecting it right
+         * above the {@code TextView} if absent.</li>
+         * </ul>
+         * <p>
+         * The coordination policy must end its downlink on the Android main thread.
+         * TODO(rosswang): provide a Coordinator that coerces this.
+         * <p>
+         * If {@code subscriptionParent} is set, this method adds the generated binding to it.
+         * <p>
+         * TODO(rosswang): produce a SyncbaseBinding, and allow mutable bindings.
+         */
+        public Builder<T> bindTwoWay(final TextView textView) {
+            @SuppressWarnings("unchecked")
+            final Builder<String> t = (Builder<String>) this;
+
+            TwoWayBinding<String> core = SyncbaseBindingTermini.bind(mRxTable,
+                    getKey(textView.getId()), String.class, t.getDefaultValue(""),
+                    (String) mDeleteValue, mOnError);
+            boolean hasSuppressWriteOnRead = false;
+            for (final CoordinatorChain<String> c : t.mCoordinators) {
+                core = c.apply(core);
+                if (core instanceof SuppressWriteOnReadCoordinator) {
+                    hasSuppressWriteOnRead = true;
+                }
+            }
+            if (mCoordinators.isEmpty()) {
+                core = new DebouncingCoordinator<>(core);
+            }
+            if (!hasSuppressWriteOnRead) {
+                core = new SuppressWriteOnReadCoordinator<>(core);
+            }
+            subscribe(TextViewBindingTermini.bind(textView, core, mOnError));
+
+            return this;
+        }
+
+        /**
+         * Calls {@link #bindTwoWay(TextView)} if the {@link TextView} is an {@link EditText},
+         * {@link #bindOneWay(TextView)} otherwise.
+         */
+        public Builder<T> bindTo(final TextView textView) {
+            return textView instanceof EditText ? bindTwoWay(textView) : bindOneWay(textView);
+        }
+
+        /**
+         * An alias for {@link #bindTwoWay(TextView)}, which is the default for {@link EditText}
+         * widgets.
+         */
+        public Builder<T> bindTo(final EditText editText) {
+            return bindTwoWay(editText);
+        }
+
+        public Builder<T> bindTo(final View view) {
+            if (view instanceof TextView) {
+                return bindTo((TextView) view);
+            } else {
+                throw new IllegalArgumentException("No default binding for view " + view);
+            }
+        }
+
+        public Builder<T> bindTo(final int viewId) {
+            return bindTo(mActivity.findViewById(viewId));
+        }
+    }
+
+    public static <T> Builder<T> builder() {
+        return new Builder<>();
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/SyncbaseBindingTermini.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/SyncbaseBindingTermini.java
new file mode 100644
index 0000000..fcd80f9
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/SyncbaseBindingTermini.java
@@ -0,0 +1,64 @@
+// Copyright 2015 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.baku.toolkit.bind;
+
+import java.util.Objects;
+
+import io.v.rx.VFn;
+import io.v.rx.syncbase.RxTable;
+import io.v.rx.syncbase.WatchEvent;
+import io.v.v23.syncbase.nosql.Table;
+import lombok.AllArgsConstructor;
+import lombok.experimental.UtilityClass;
+import rx.Observable;
+import rx.Subscription;
+import rx.functions.Action1;
+import rx.schedulers.Schedulers;
+
+@UtilityClass
+public class SyncbaseBindingTermini {
+    @AllArgsConstructor
+    private static class WriteData<T> {
+        public final Table t;
+        public final T data;
+    }
+
+    public static <T> Observable<WatchEvent<T>> bindRead(
+            final RxTable rxTable, final String key, final Class<T> type, final T defaultValue) {
+        return rxTable.watch(key, type, defaultValue);
+    }
+
+    public static <T> Subscription bindWrite(
+            final RxTable rxTable, final Observable<T> rxData, final String key,
+            final Class<T> type, final T deleteValue, final Action1<Throwable> onError) {
+        return rxData
+                .onBackpressureLatest()
+                .observeOn(Schedulers.io())
+                .switchMap(data -> rxTable.once().map(t -> new WriteData<>(t, data)))
+                .subscribe(VFn.unchecked(w -> {
+                    if (Objects.equals(w.data, deleteValue)) {
+                        w.t.delete(rxTable.getVContext(), key);
+                    } else {
+                        w.t.put(rxTable.getVContext(), key, w.data, type);
+                    }
+                }), onError);
+    }
+
+    public static <T> TwoWayBinding<T> bind(
+            final RxTable rxTable, final String key, final Class<T> type, final T defaultValue,
+            final T deleteValue, final Action1<Throwable> onError) {
+        return new TwoWayBinding<T>() {
+            @Override
+            public Observable<WatchEvent<T>> downlink() {
+                return bindRead(rxTable, key, type, defaultValue);
+            }
+
+            @Override
+            public Subscription uplink(final Observable<T> rxData) {
+                return bindWrite(rxTable, rxData, key, type, deleteValue, onError);
+            }
+        };
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/TextViewBindingTermini.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/TextViewBindingTermini.java
new file mode 100644
index 0000000..035ddde
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/TextViewBindingTermini.java
@@ -0,0 +1,43 @@
+// Copyright 2015 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.baku.toolkit.bind;
+
+import android.widget.TextView;
+
+import com.jakewharton.rxbinding.widget.RxTextView;
+
+import java.util.Objects;
+
+import io.v.rx.syncbase.WatchEvent;
+import lombok.experimental.UtilityClass;
+import rx.Observable;
+import rx.Subscription;
+import rx.functions.Action1;
+import rx.subscriptions.CompositeSubscription;
+
+@UtilityClass
+public class TextViewBindingTermini {
+    public static Subscription bindRead(final TextView textView,
+                                        final Observable<WatchEvent<String>> downlink,
+                                        final Action1<Throwable> onError) {
+        return downlink
+                .map(WatchEvent::getValue)
+                .filter(s -> !Objects.equals(s, Objects.toString(textView.getText(), null)))
+                .subscribe(textView::setTextKeepState, onError);
+    }
+
+    public static Observable<String> bindWrite(final TextView textView) {
+        return RxTextView.afterTextChangeEvents(textView)
+                .skip(1) //don't put the initial content
+                .map(e -> Objects.toString(e.editable(), null));
+    }
+
+    public static Subscription bind(final TextView textView, final TwoWayBinding<String> binding,
+                                    final Action1<Throwable> onError) {
+        return new CompositeSubscription(
+                bindRead(textView, binding.downlink(), onError),
+                binding.uplink(bindWrite(textView)));
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/TwoWayBinding.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/TwoWayBinding.java
new file mode 100644
index 0000000..4e827fa
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/TwoWayBinding.java
@@ -0,0 +1,28 @@
+// Copyright 2015 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.baku.toolkit.bind;
+
+import io.v.rx.syncbase.WatchEvent;
+import rx.Observable;
+import rx.Subscription;
+
+/**
+ * {@code TwoWayBinding}s are nested from the Syncbase layer innermost to the UI layer outermost.
+ * The downlink is the data flow from Syncbase to the UI, and the uplink is the data flow from the
+ * UI to Syncbase.
+ */
+public interface TwoWayBinding<T> {
+    /**
+     * This method should be called at most once per instance, and the observable should have at
+     * most one subscriber.
+     */
+    Observable<WatchEvent<T>> downlink();
+
+    /**
+     * This method should be called at most once per instance, and the observable should have at
+     * most one subscriber.
+     */
+    Subscription uplink(Observable<T> rxData);
+}
diff --git a/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/package-info.java b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/package-info.java
new file mode 100644
index 0000000..51e9879
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/baku/toolkit/bind/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Constructs:
+ *
+ * Binding - maps one or more controls/properties to a Syncbase row.
+ * Coordination policy - integrates uplinks and downlinks.
+ */
+package io.v.baku.toolkit.bind;
\ No newline at end of file
diff --git a/bakutoolkit/src/main/java/io/v/debug/SyncbaseAndroidService.java b/bakutoolkit/src/main/java/io/v/debug/SyncbaseAndroidService.java
new file mode 100644
index 0000000..c89899d
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/debug/SyncbaseAndroidService.java
@@ -0,0 +1,122 @@
+// Copyright 2015 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.debug;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import org.joda.time.Duration;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+import io.v.android.v23.V;
+import io.v.baku.toolkit.BlessingsUtils;
+import io.v.impl.google.services.syncbase.SyncbaseServer;
+import io.v.rx.VFn;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.Server;
+import io.v.v23.verror.VException;
+import lombok.Value;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import rx.Observable;
+import rx.Subscription;
+import rx.schedulers.Schedulers;
+import rx.util.async.Async;
+
+/**
+ * Syncbase Android service in lieu of GMS Core Syncbase. Exposes Syncbase as a simplistic bound
+ * service, relying on Vanadium RPC in lieu of IPC, with open permissions, returning the local
+ * Vanadium name in {@code Binder.getName}.
+ */
+@Slf4j
+public class SyncbaseAndroidService extends Service {
+    private static final Duration
+            STOP_TIMEOUT = Duration.standardSeconds(5),
+            KEEP_ALIVE = Duration.standardMinutes(5);
+
+    @Accessors(prefix = "m")
+    @Value
+    public static class BindResult {
+        Server mServer;
+        String mName;
+    }
+
+    private VContext mVContext;
+    private Observable<BindResult> mObservable;
+    private Subscription mKill;
+
+    public class Binder extends android.os.Binder {
+        private Binder() {
+        }
+
+        public Observable<BindResult> getObservable() {
+            return mObservable;
+        }
+    }
+
+    @Override
+    public void onCreate() {
+        mVContext = V.init(this);
+        // If we don't proxy, it seems we can't even mount.
+        try {
+            mVContext = V.withListenSpec(mVContext, V.getListenSpec(mVContext).withProxy("proxy"));
+        } catch (final VException e) {
+            log.warn("Unable to set up Vanadium proxy for Syncbase", e);
+        }
+
+        mObservable = Async.fromCallable(this::startServer, Schedulers.io())
+                .replay(1).autoConnect(0); //cache last result; connect immediately
+    }
+
+    @Override
+    public void onDestroy() {
+        try {
+            mObservable.doOnNext(VFn.unchecked(b -> {
+                log.info("Stopping Syncbase");
+                b.mServer.stop();
+            }))
+                    .timeout(STOP_TIMEOUT.getMillis(), TimeUnit.MILLISECONDS)
+                    .toBlocking()
+                    .single();
+            log.info("Syncbase is over");
+            System.exit(0);
+        } catch (final RuntimeException e) {
+            log.error("Failed to shut down Syncbase", e);
+            System.exit(1);
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new Binder();
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        mKill = Observable.timer(KEEP_ALIVE.getMillis(), TimeUnit.MILLISECONDS)
+                .subscribe(x -> stopSelf());
+        return true;
+    }
+
+    @Override
+    public void onRebind(Intent intent) {
+        mKill.unsubscribe();
+    }
+
+    private BindResult startServer() throws SyncbaseServer.StartException {
+        final File storageRoot = new File(getFilesDir(), "syncbase");
+        storageRoot.mkdirs();
+
+        log.info("Starting Syncbase");
+        final VContext sbCtx = SyncbaseServer.withNewServer(mVContext, new SyncbaseServer.Params()
+                .withPermissions(BlessingsUtils.OPEN_DATA_PERMS)
+                .withStorageRootDir(storageRoot.getAbsolutePath()));
+        final Server server = V.getServer(sbCtx);
+        return new BindResult(server, "/" + server.getStatus().getEndpoints()[0]);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/debug/SyncbaseClient.java b/bakutoolkit/src/main/java/io/v/debug/SyncbaseClient.java
new file mode 100644
index 0000000..7f89454
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/debug/SyncbaseClient.java
@@ -0,0 +1,133 @@
+// Copyright 2015 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.debug;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+
+import io.v.v23.rpc.Server;
+import io.v.v23.security.Blessings;
+import io.v.v23.syncbase.Syncbase;
+import io.v.v23.syncbase.SyncbaseService;
+import lombok.RequiredArgsConstructor;
+import lombok.Synchronized;
+import lombok.Value;
+import lombok.experimental.Accessors;
+import rx.Observable;
+import rx.Observer;
+import rx.Subscription;
+import rx.subjects.ReplaySubject;
+
+/**
+ * Syncbase Vanadium client for the {@link SyncbaseAndroidService} bound service, wrapped in an
+ * {@link Observable}.
+ */
+@Accessors(prefix = "m")
+public class SyncbaseClient implements AutoCloseable {
+    public static class BindException extends Exception {
+        public BindException(final String message) {
+            super(message);
+        }
+    }
+
+    @Value
+    public static class ServerClient {
+        Server mServer;
+        SyncbaseService mClient;
+    }
+
+    @RequiredArgsConstructor
+    private static class SyncbaseServiceConnection implements ServiceConnection {
+        private final Observer<ServerClient> mObserver;
+        private Subscription lastSubscription;
+
+        @Override
+        public void onServiceConnected(final ComponentName componentName,
+                                       final IBinder iBinder) {
+            final SyncbaseAndroidService.Binder binder = (SyncbaseAndroidService.Binder) iBinder;
+            lastSubscription = binder.getObservable()
+                    .map(b -> new ServerClient(b.getServer(), Syncbase.newService(b.getName())))
+                    .subscribe(mObserver);
+        }
+
+        @Override
+        public void onServiceDisconnected(final ComponentName componentName) {
+            lastSubscription.unsubscribe();
+            mObserver.onNext(null);
+        }
+    }
+
+    private final Context mAndroidContext;
+    private final Observable<ServerClient> mObservable;
+    private final SyncbaseServiceConnection mConnection;
+    private boolean mBound;
+
+    public Observable<Server> getRxServer() {
+        return mObservable.map(ServerClient::getServer);
+    }
+
+    public Observable<SyncbaseService> getRxClient() {
+        return mObservable.map(ServerClient::getClient);
+    }
+
+    @Synchronized
+    private void startService(final Context androidContext, final Intent intent) {
+        if (androidContext.startService(intent) == null) {
+            mConnection.mObserver.onError(
+                    new BindException("Could not locate Syncbase Android service"));
+        }
+        mBound = androidContext.bindService(
+                intent, mConnection, Context.BIND_AUTO_CREATE);
+        if (!mBound) {
+            mConnection.mObserver.onError(
+                    new BindException("Failed to bind to Syncbase Android service"));
+        }
+    }
+
+    /**
+     * Starts/connects to {@link SyncbaseAndroidService} and returns an {@code Observable} which
+     * receives an {@code onNext} whenever the Android service client connects. Subscriptions share
+     * a connection and initially receive the active instance if still connected.
+     *
+     * @param rxBlessings an optional observable of blessings. If provided, the Syncbase service
+     *                    will not be started until blessings are available.
+     *                    TODO(rosswang): this should either handle blessings changes or not care.
+     */
+    public SyncbaseClient(final Context androidContext, final Observable<Blessings> rxBlessings) {
+        mAndroidContext = androidContext;
+
+        /*
+        SyncbaseAndroidService is included in the io.v.baku.toolkit manifest, but since Android
+        libraries are linked statically, we'll need to resolve it via the end application package
+        (through androidContext).
+         */
+        final Intent intent = new Intent(androidContext, SyncbaseAndroidService.class);
+        final ReplaySubject<ServerClient> rpl = ReplaySubject.createWithSize(1);
+        mConnection = new SyncbaseServiceConnection(rpl);
+
+        if (rxBlessings == null) {
+            startService(androidContext, intent);
+        } else {
+            rxBlessings.first()
+                    .subscribe(s -> {
+                        startService(androidContext, intent);
+                    }, rpl::onError);
+        }
+
+        mObservable = rpl.filter(s -> s != null);
+    }
+
+    @Override
+    @Synchronized
+    public void close() {
+        if (mBound) {
+            mAndroidContext.unbindService(mConnection);
+            mBound = false;
+        }
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/MountEvent.java b/bakutoolkit/src/main/java/io/v/rx/MountEvent.java
new file mode 100644
index 0000000..2af85aa
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/MountEvent.java
@@ -0,0 +1,45 @@
+// Copyright 2015 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.rx;
+
+import org.joda.time.DateTime;
+
+import io.v.v23.verror.VException;
+import java8.util.Optional;
+import lombok.Value;
+import lombok.experimental.Accessors;
+
+@Accessors(prefix = "m")
+@Value
+public class MountEvent {
+    public static MountEvent forAddNameSuccess(final String name) {
+        return new MountEvent(true, name, Optional.empty(), DateTime.now(), Optional.empty());
+    }
+
+    public static MountEvent forAddNameFailure(final String name, final VException e) {
+        return new MountEvent(true, name, Optional.empty(), DateTime.now(), Optional.of(e));
+    }
+
+    public static MountEvent forStatus(final boolean isMount, final String name,
+                                       final String server, final DateTime timestamp,
+                                       final VException error) {
+        return new MountEvent(isMount, name, Optional.of(server), timestamp,
+                Optional.ofNullable(error));
+    }
+
+    public static int compareByTimestamp(final MountEvent a, final MountEvent b) {
+        return a.getTimestamp().compareTo(b.getTimestamp());
+    }
+
+    boolean mMount;
+    String mName;
+    Optional<String> mServer;
+    DateTime mTimestamp;
+    Optional<VException> mError;
+
+    public boolean isSuccessfulMount() {
+        return mMount && mServer.isPresent() && !mError.isPresent();
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/RxMountState.java b/bakutoolkit/src/main/java/io/v/rx/RxMountState.java
new file mode 100644
index 0000000..f720047
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/RxMountState.java
@@ -0,0 +1,57 @@
+// Copyright 2015 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.rx;
+
+import org.joda.time.Duration;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import io.v.v23.rpc.MountStatus;
+import io.v.v23.rpc.MountStatusKey;
+import io.v.v23.rpc.MountStatusValue;
+import io.v.v23.rpc.Server;
+import java8.util.J8Arrays;
+import java8.util.stream.Collectors;
+import java8.util.stream.Stream;
+import lombok.experimental.UtilityClass;
+import rx.Observable;
+import rx.schedulers.Schedulers;
+
+@UtilityClass
+public class RxMountState {
+    /**
+     * Millisecond interval for mount status polling.
+     */
+    public static final Duration DEFAULT_POLLING_INTERVAL = Duration.standardSeconds(1);
+
+    public static Observable<Stream<MountStatus>> poll(final Server s, final Duration interval) {
+        return Observable.interval(0, interval.getMillis(), TimeUnit.MILLISECONDS,
+                Schedulers.io())
+                .map(i -> J8Arrays.stream(s.getStatus().getMounts()));
+    }
+
+    public static Observable<Stream<MountStatus>> poll(final Server s) {
+        return poll(s, DEFAULT_POLLING_INTERVAL);
+    }
+
+    public static Map<MountStatusKey, MountStatusValue> index(
+            final Stream<MountStatus> state) {
+        return state.collect(Collectors.toMap(
+                MountStatusKey::fromMountStatus,
+                MountStatusValue::fromMountStatus));
+    }
+
+    public static Map<MountStatusKey, MountStatusValue> index(
+            final MountStatus[] state) {
+        return index(J8Arrays.stream(state));
+    }
+
+    public static Observable<Map<MountStatusKey, MountStatusValue>> index(
+            final Observable<Stream<MountStatus>> rx) {
+        return rx.map(RxMountState::index)
+                .distinctUntilChanged();
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/RxNamespace.java b/bakutoolkit/src/main/java/io/v/rx/RxNamespace.java
new file mode 100644
index 0000000..4fbc11d
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/RxNamespace.java
@@ -0,0 +1,131 @@
+// Copyright 2015 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.rx;
+
+import org.joda.time.DateTime;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.v.v23.rpc.MountStatus;
+import io.v.v23.rpc.MountStatusKey;
+import io.v.v23.rpc.MountStatusValue;
+import io.v.v23.rpc.Server;
+import io.v.v23.verror.VException;
+import java8.util.Maps;
+import lombok.AllArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.UtilityClass;
+import rx.Observable;
+
+@UtilityClass
+public class RxNamespace {
+    @RequiredArgsConstructor
+    @AllArgsConstructor
+    private static class MountRecord {
+        /*
+        TODO(rosswang): https://github.com/vanadium/issues/issues/826
+        Right now, mount status defaults timestamps for events that have never happened to be some
+        time in 1754. As a hack, initialize these to epoch/1970 (or any time after the default).
+         */
+        public DateTime lastMount = new DateTime(0),
+                lastUnmount = new DateTime(0);
+
+        public static MountRecord fromStatus(final MountStatus status) {
+            return new MountRecord(status.getLastMount(), status.getLastUnmount());
+        }
+    }
+
+    private static Observable<MountEvent> processStatus(
+            final Map<MountStatusKey, MountRecord> mountRecords,
+            final MountStatusKey k, final MountStatusValue v) {
+
+        final List<MountEvent> newEvents = new ArrayList<>(2);
+        synchronized (mountRecords) {
+            final MountRecord record = Maps.computeIfAbsent(mountRecords, k,
+                    kk -> new MountRecord());
+            if (v.getLastMount() != null && v.getLastMount().isAfter(record.lastMount)) {
+                newEvents.add(MountEvent.forStatus(true, k.getName(), k.getServer(),
+                        v.getLastMount(), v.getLastMountError()));
+            }
+            if (v.getLastUnmount() != null && v.getLastUnmount().isAfter(record.lastUnmount)) {
+                newEvents.add(MountEvent.forStatus(false, k.getName(), k.getServer(),
+                        v.getLastUnmount(), v.getLastUnmountError()));
+            }
+            record.lastMount = v.getLastMount();
+            record.lastUnmount = v.getLastUnmount();
+        }
+        Collections.sort(newEvents, MountEvent::compareByTimestamp);
+        return Observable.from(newEvents);
+    }
+
+    private static boolean isAlreadyMounted(final MountStatus status) {
+        //There are also ambiguous cases; err on the side of false.
+        return status.getLastMount().isAfter(status.getLastUnmount()) &&
+                status.getLastMountError() == null;
+    }
+
+    /**
+     * @return an {@code Observable} of {@code MountEvent}s. Events including servers come from
+     * polling, may be mount or unmount events, and may or may not include an error. Events with
+     * {@code isMount = true} are from {@link Server#addName(String)} and may or may not include an
+     * error. If a server is already mounted, a backdated mount event is included.
+     */
+    public static Observable<MountEvent> mount(final Observable<Server> rxServer,
+                                                          final String name) {
+        return Observable.switchOnNext(
+                rxServer.map(server -> {
+                    /*
+                    A note on thread safety. The initial status scan occurs on one thread, before
+                    any concurrency contention. Subsequent modifications happen on the Rx io
+                    scheduler, which by may be on different threads for each poll. Thus, not only
+                    does the map need to be thread-safe, but also each mount record. The simplest
+                    way to ensure this is just to lock map get/create and record access on a mutex,
+                    which might as well be the map itself. This does perform some unnecessary
+                    synchronization, but it all occurs on a worker thread on a low-fidelity loop, so
+                    it's not worth a more sophisticated lock.
+
+                    A fully correct minimal lock would include a thread-safe map and a read/write
+                    lock for each record.
+                     */
+                    final Map<MountStatusKey, MountRecord> mountRecords = new HashMap<>();
+                    final MountStatus[] mounts = server.getStatus().getMounts();
+                    final List<MountEvent> alreadyMounted = new ArrayList<>(mounts.length);
+                    for (final MountStatus status : mounts) {
+                        if (status.getName().equals(name)) {
+                            mountRecords.put(MountStatusKey.fromMountStatus(status),
+                                    MountRecord.fromStatus(status));
+                            if (isAlreadyMounted(status)) {
+                                alreadyMounted.add(MountEvent.forStatus(true, status.getName(),
+                                        status.getServer(), status.getLastMount(),
+                                        status.getLastMountError() /* null */));
+                            }
+                        }
+                    }
+                    if (alreadyMounted.isEmpty()) {
+                        try {
+                            server.addName(name);
+                        } catch (final VException e) {
+                            return Observable.just(MountEvent.forAddNameFailure(name, e));
+                        }
+                        final MountEvent initial = MountEvent.forAddNameSuccess(name);
+
+                        return RxMountState.index(
+                                RxMountState.poll(server).map(state -> state.filter(
+                                        status -> status.getName().equals(name))))
+                                .flatMapIterable(Map::entrySet)
+                                .concatMap(e -> processStatus(mountRecords,
+                                        e.getKey(), e.getValue()))
+                                .startWith(initial);
+                    } else {
+                        return Observable.from(alreadyMounted);
+                    }
+                }))
+                .takeWhile(MountEvent::isMount);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/RxVIterable.java b/bakutoolkit/src/main/java/io/v/rx/RxVIterable.java
new file mode 100644
index 0000000..b6e256b
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/RxVIterable.java
@@ -0,0 +1,33 @@
+// Copyright 2015 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.rx;
+
+import io.v.v23.VIterable;
+import io.v.v23.verror.VException;
+import lombok.experimental.UtilityClass;
+import rx.Observable;
+
+@UtilityClass
+public class RxVIterable {
+    /**
+     * Wraps a {@link VIterable} in an observable that produces the same elements and checks for
+     * an error from the {@code VIterable} at the end if present. This is a thin wrapper that does
+     * not ensure that the {@code VIterable} is only iterated over once, so the returned
+     * {@link Observable} should be subscribed to at most once. If multiple subscriptions are
+     * needed, consider a connectable observable operator, such as {@link Observable#publish()},
+     * {@link Observable#replay()}, {@link Observable#share()}, or {@link Observable#cache()}.
+     * However, if using a replay/cache, be cognizant of buffer growth.
+     *
+     * @return an observable wrapping the {@link VIterable}. This observable should only be
+     * subscribed to once, as we can only iterate over the underlying stream once.
+     */
+    public static <T> Observable<T> wrap(final VIterable<T> vi) {
+        return Observable.from(vi)
+                .concatWith(Observable.defer(() -> {
+                    final VException e = vi.error();
+                    return e == null ? Observable.empty() : Observable.error(e);
+                }));
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/UncheckedVException.java b/bakutoolkit/src/main/java/io/v/rx/UncheckedVException.java
new file mode 100644
index 0000000..f289df7
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/UncheckedVException.java
@@ -0,0 +1,42 @@
+// Copyright 2015 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.rx;
+
+import com.google.common.collect.Iterables;
+
+import java.util.Arrays;
+
+import io.v.v23.verror.VException;
+
+/**
+ * This wrapper for {@link VException} facilitates its use with lambdas and RxJava. Where this is
+ * used, it is expected that alternate error handling mechanisms are in place.
+ */
+public class UncheckedVException extends RuntimeException {
+    public UncheckedVException(final VException cause) {
+        super(cause);
+    }
+
+    @Override
+    public VException getCause() {
+        return (VException)super.getCause();
+    }
+
+    public boolean isIdIn(final Iterable<VException.IDAction> ids) {
+        return Iterables.any(ids, id -> id.getID().equals(getCause().getID()));
+    }
+
+    public boolean isIdIn(final VException.IDAction... ids) {
+        return isIdIn(Arrays.asList(ids));
+    }
+
+    public static boolean isIdIn(final Throwable t, final Iterable<VException.IDAction> ids) {
+        return t instanceof UncheckedVException && ((UncheckedVException)t).isIdIn(ids);
+    }
+
+    public static boolean isIdIn(final Throwable t, final VException.IDAction... ids) {
+        return isIdIn(t, Arrays.asList(ids));
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/VFn.java b/bakutoolkit/src/main/java/io/v/rx/VFn.java
new file mode 100644
index 0000000..834656b
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/VFn.java
@@ -0,0 +1,62 @@
+// Copyright 2015 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.rx;
+
+import io.v.v23.verror.VException;
+import java8.lang.FunctionalInterface;
+import lombok.experimental.UtilityClass;
+import rx.functions.Action0;
+import rx.functions.Action1;
+import rx.functions.Func1;
+
+@UtilityClass
+public class VFn {
+    @FunctionalInterface
+    public interface VAction {
+        void call() throws VException;
+    }
+
+    @FunctionalInterface
+    public interface VAction1<T> {
+        void call(T arg) throws VException;
+    }
+
+    @FunctionalInterface
+    public interface VFunc1<T, R> {
+        R call(T arg) throws VException;
+    }
+
+    public static void doUnchecked(final VAction v) {
+        try {
+            v.call();
+        } catch (final VException e) {
+            throw new UncheckedVException(e);
+        }
+    }
+
+    public static Action0 unchecked(final VAction v) {
+        return () -> doUnchecked(v);
+    }
+
+    public static <T> Action1<T> unchecked(final VAction1<? super T> v) {
+        return t -> {
+            try {
+                v.call(t);
+            } catch (final VException e) {
+                throw new UncheckedVException(e);
+            }
+        };
+    }
+
+    public static <T, R> Func1<T, R> unchecked(final VFunc1<? super T, ? extends R> v) {
+        return t -> {
+            try {
+                return v.call(t);
+            } catch (final VException e) {
+                throw new UncheckedVException(e);
+            }
+        };
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/Creatable.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/Creatable.java
new file mode 100644
index 0000000..c919650
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/Creatable.java
@@ -0,0 +1,15 @@
+// Copyright 2015 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.rx.syncbase;
+
+import io.v.v23.context.VContext;
+import io.v.v23.security.access.Permissions;
+import io.v.v23.verror.VException;
+import java8.lang.FunctionalInterface;
+
+@FunctionalInterface
+interface Creatable {
+    void create(VContext vContext, Permissions permissions) throws VException;
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/ExistenceAware.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/ExistenceAware.java
new file mode 100644
index 0000000..9bb2f9d
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/ExistenceAware.java
@@ -0,0 +1,14 @@
+// Copyright 2015 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.rx.syncbase;
+
+import io.v.v23.context.VContext;
+import io.v.v23.verror.VException;
+import java8.lang.FunctionalInterface;
+
+@FunctionalInterface
+interface ExistenceAware {
+    boolean exists(VContext vContext) throws VException;
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/GlobalUserSyncgroup.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/GlobalUserSyncgroup.java
new file mode 100644
index 0000000..651d98a
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/GlobalUserSyncgroup.java
@@ -0,0 +1,217 @@
+// Copyright 2015 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.rx.syncbase;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import io.v.baku.toolkit.BakuActivityMixin;
+import io.v.baku.toolkit.BakuActivityTrait;
+import io.v.baku.toolkit.BlessingsUtils;
+import io.v.baku.toolkit.R;
+import io.v.baku.toolkit.VAndroidContextMixin;
+import io.v.baku.toolkit.VAndroidContextTrait;
+import io.v.v23.context.VContext;
+import io.v.v23.security.Blessings;
+import io.v.v23.security.access.AccessList;
+import io.v.v23.security.access.Permissions;
+import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
+import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
+import io.v.v23.services.syncbase.nosql.TableRow;
+import io.v.v23.syncbase.nosql.Database;
+import io.v.v23.syncbase.nosql.Syncgroup;
+import io.v.v23.verror.NoExistException;
+import io.v.v23.verror.VException;
+import java8.util.function.Function;
+import java8.util.stream.Collectors;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import rx.Observable;
+import rx.Subscription;
+import rx.functions.Action2;
+import rx.schedulers.Schedulers;
+
+@Accessors(prefix = "m")
+@AllArgsConstructor
+@Builder(builderClassName = "Builder")
+@Slf4j
+public class GlobalUserSyncgroup {
+    public static final String
+            DEFAULT_SYNCGROUP_HOST_NAME = "usersync",
+            DEFAULT_SYNCGROUP_SUFFIX = "user",
+            DEFAULT_RENDEZVOUS_MOUNT = "sgmt";
+    public static final SyncgroupMemberInfo
+            DEFAULT_SYNCGROUP_MEMBER_INFO = new SyncgroupMemberInfo();
+
+    public static GlobalUserSyncgroup forActivity(final BakuActivityTrait t) {
+        return builder().bakuActivityTrait(t).build();
+    }
+
+    public static GlobalUserSyncgroup forActivity(final BakuActivityMixin m) {
+        return forActivity(m.getBakuActivityTrait());
+    }
+
+    /*
+    As of Lombok IntelliJ 0.9.6, @Builder exhibits a few bugs interacting with @Accessors (Gradle
+    build is fine).
+
+    https://github.com/mplushnikov/lombok-intellij-plugin/issues/151
+     */
+    public static class Builder {
+        private String sgSuffix = DEFAULT_SYNCGROUP_SUFFIX;
+        private Function<String, String> descriptionForUsername = u -> "User syncgroup for " + u;
+        private Function<AccessList, Permissions> permissionsForUserAcl =
+                BlessingsUtils::syncgroupPermissions;
+        private List<TableRow> prefixes = new ArrayList<>();
+        private SyncgroupMemberInfo memberInfo = DEFAULT_SYNCGROUP_MEMBER_INFO;
+
+        /**
+         * This is an additive setter to {@link #prefixes(List)}.
+         */
+        public Builder prefix(final TableRow prefix) {
+            prefixes.add(prefix);
+            return this;
+        }
+
+        /**
+         * This is an additive setter to {@link #prefixes(List)}.
+         */
+        public Builder prefix(final String tableName, final String rowPrefix) {
+            return prefix(new TableRow(tableName, rowPrefix));
+        }
+
+        /**
+         * This is an additive setter to {@link #prefixes(List)}.
+         */
+        public Builder prefix(final String tableName) {
+            return prefix(tableName, "");
+        }
+
+        /**
+         * This is a composite setter for:
+         * <ul>
+         * <li>{@code vContext}</li>
+         * <li>{@code rxBlessings}</li>
+         * <li>{@code syncHostLevel}</li> (as a new
+         * {@link UserAppSyncHost#UserAppSyncHost(Context)})
+         * <li>{@code onError}</li>
+         * </ul>
+         * and should be called prior to any overrides for those fields.
+         */
+        public Builder vActivityTrait(final VAndroidContextTrait<?> t) {
+            return vContext(t.getVContext())
+                    .rxBlessings(t.getBlessingsProvider().getRxBlessings())
+                    .syncHostLevel(new UserAppSyncHost(t.getAndroidContext()))
+                    .onError(t.getErrorReporter()::onError);
+        }
+
+        /**
+         * In addition to those fields in {@link #vActivityTrait(VAndroidContextTrait)}, this
+         * additionally sets:
+         * <ul>
+         * <li>{@code syncbase}</li>
+         * <li>{@code db}</li>
+         * <li>and adds to {@code prefixes}</li>
+         * </ul>
+         */
+        public Builder bakuActivityTrait(final BakuActivityTrait t) {
+            return vActivityTrait(t.getVAndroidContextTrait())
+                    .syncbase(t.getSyncbase())
+                    .db(t.getSyncbaseDb())
+                    .prefix(t.getSyncbaseTableName());
+        }
+
+        /**
+         * A convenience setter for {@link #bakuActivityTrait(BakuActivityTrait)} via
+         * {@link VAndroidContextMixin}.
+         */
+        public Builder activity(final BakuActivityMixin activity) {
+            return bakuActivityTrait(activity.getBakuActivityTrait());
+        }
+    }
+
+    private final VContext mVContext;
+    private final Observable<Blessings> mRxBlessings;
+    private final SyncHostLevel mSyncHostLevel;
+    private final String mSgSuffix;
+    private final RxSyncbase mSyncbase;
+    private final RxDb mDb;
+    private final Function<String, String> mDescriptionForUsername;
+    private final Function<AccessList, Permissions> mPermissionsForUserAcl;
+    private final List<TableRow> mPrefixes;
+    private final SyncgroupMemberInfo mMemberInfo;
+    /**
+     * @see io.v.baku.toolkit.ErrorReporter#onError(int, Throwable)
+     */
+    private final Action2<Integer, Throwable> mOnError;
+
+    private SyncgroupSpec createSpec(final String username, final AccessList userAcl) {
+        return new SyncgroupSpec(mDescriptionForUsername.apply(username),
+                mPermissionsForUserAcl.apply(userAcl), mPrefixes,
+                mSyncHostLevel.getRendezvousTableNames(username), false);
+    }
+
+    private Observable<Object> createOrJoinSyncgroup(final Database db, final String sgName,
+                                                     final SyncgroupSpec spec) {
+        return Observable.create(s -> {
+            final Syncgroup sg = db.getSyncgroup(sgName);
+            try {
+                sg.join(mVContext, mMemberInfo);
+                log.info("Joined syncgroup " + sgName);
+            } catch (final NoExistException e) {
+                try {
+                    sg.create(mVContext, spec, mMemberInfo);
+                    log.info("Created syncgroup " + sgName);
+                } catch (final VException e2) {
+                    s.onError(e2);
+                    return;
+                }
+            } catch (final VException e) {
+                s.onError(e);
+                return;
+            }
+            s.onNext(null);
+            s.onCompleted();
+        });
+    }
+
+    private Observable<Object> createOrJoinSyncgroup(final String username, final AccessList acl) {
+        final String sgHost = mSyncHostLevel.getSyncgroupHostName(username);
+        final String sgName = RxSyncbase.syncgroupName(sgHost, mSgSuffix);
+        final SyncgroupSpec spec = createSpec(username, acl);
+
+        final Observable<Object> mount = SgHostUtil.ensureSyncgroupHost(
+                mVContext, mSyncbase.getRxServer(), sgHost).share();
+
+        return mDb.getObservable()
+                .observeOn(Schedulers.io())
+                .switchMap(db -> Observable.merge(mount.first().ignoreElements().concatWith(
+                        createOrJoinSyncgroup(db, sgName, spec)), mount));
+    }
+
+    public Subscription join() {
+        return Observable.switchOnNext(mRxBlessings
+                .map(b -> {
+                    final AccessList acl = BlessingsUtils.blessingsToAcl(mVContext, b);
+                    final List<Observable<?>> createOrJoins =
+                            BlessingsUtils.blessingsToUsernameStream(mVContext, b)
+                                    .distinct()
+                                    .map(u -> createOrJoinSyncgroup(u, acl))
+                                    .collect(Collectors.toList());
+                    if (createOrJoins.isEmpty()) {
+                        throw new NoSuchElementException("GlobalUserSyncgroup requires a " +
+                                "username; no username blessings found. Blessings: " + b);
+                    }
+                    return Observable.merge(createOrJoins);
+                }))
+                .subscribe(x -> {
+                }, t -> mOnError.call(R.string.err_syncgroup_join, t));
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/RxApp.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/RxApp.java
new file mode 100644
index 0000000..ab81a69
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/RxApp.java
@@ -0,0 +1,43 @@
+// Copyright 2015 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.rx.syncbase;
+
+import io.v.rx.VFn;
+import io.v.v23.context.VContext;
+import io.v.v23.syncbase.SyncbaseApp;
+import io.v.v23.syncbase.SyncbaseService;
+import io.v.v23.verror.VException;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+import rx.Observable;
+
+@Accessors(prefix = "m")
+@Getter
+public class RxApp extends RxEntity<SyncbaseApp, SyncbaseService> {
+    private final VContext mVContext;
+    private final String mName;
+    private final RxSyncbase mRxSyncbase;
+
+    private final Observable<SyncbaseApp> mObservable;
+
+    public RxApp(final String name, final RxSyncbase rxSb) {
+        mVContext = rxSb.getVContext();
+        mName = name;
+        mRxSyncbase = rxSb;
+
+        mObservable = rxSb.getRxClient().map(VFn.unchecked(this::mapFrom));
+    }
+
+    @Override
+    public SyncbaseApp mapFrom(final SyncbaseService sb) throws VException {
+        final SyncbaseApp app = sb.getApp(mName);
+        SyncbaseEntity.compose(app::exists, app::create).ensureExists(mVContext, null);
+        return app;
+    }
+
+    public RxDb rxDb(final String name) {
+        return new RxDb(name, this);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/RxDb.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/RxDb.java
new file mode 100644
index 0000000..97ca825
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/RxDb.java
@@ -0,0 +1,43 @@
+// Copyright 2015 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.rx.syncbase;
+
+import io.v.rx.VFn;
+import io.v.v23.context.VContext;
+import io.v.v23.syncbase.SyncbaseApp;
+import io.v.v23.syncbase.nosql.Database;
+import io.v.v23.verror.VException;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+import rx.Observable;
+
+@Accessors(prefix = "m")
+@Getter
+public class RxDb extends RxEntity<Database, SyncbaseApp> {
+    private final VContext mVContext;
+    private final String mName;
+    private final RxApp mRxApp;
+
+    private final Observable<Database> mObservable;
+
+    public RxDb(final String name, final RxApp rxApp) {
+        mVContext = rxApp.getVContext();
+        mName = name;
+        mRxApp = rxApp;
+
+        mObservable = rxApp.getObservable().map(VFn.unchecked(this::mapFrom));
+    }
+
+    @Override
+    public Database mapFrom(final SyncbaseApp app) throws VException {
+        final Database db = app.getNoSqlDatabase(mName, null);
+        SyncbaseEntity.compose(db::exists, db::create).ensureExists(mVContext, null);
+        return db;
+    }
+
+    public RxTable rxTable(final String name) {
+        return new RxTable(name, this);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/RxEntity.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/RxEntity.java
new file mode 100644
index 0000000..937ad53
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/RxEntity.java
@@ -0,0 +1,22 @@
+// Copyright 2015 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.rx.syncbase;
+
+import io.v.v23.verror.VException;
+import rx.Observable;
+
+public abstract class RxEntity<T, P> {
+    public abstract String getName();
+    public abstract Observable<T> getObservable();
+    public abstract T mapFrom(P parent) throws VException;
+
+    /**
+     * This is a shortcut for {@code getObservable().first()}, to reduce the likelihood of
+     * forgetting to filter by {@code first}. Most commands should be run in this way.
+     */
+    public Observable<T> once() {
+        return getObservable().first();
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/RxSyncbase.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/RxSyncbase.java
new file mode 100644
index 0000000..e441be4
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/RxSyncbase.java
@@ -0,0 +1,62 @@
+// Copyright 2015 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.rx.syncbase;
+
+import android.content.Context;
+
+import io.v.baku.toolkit.VAndroidContextTrait;
+import io.v.debug.SyncbaseClient;
+import io.v.impl.google.naming.NamingUtil;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.Server;
+import io.v.v23.security.Blessings;
+import io.v.v23.syncbase.SyncbaseService;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+import rx.Observable;
+
+/**
+ * Models a binding to a Syncbase Android service as an {@code Observable} of
+ * {@link SyncbaseService}s. The binding will be asynchronously made and then potentially
+ * periodically lost and regained, so modeling further operations as subscriptions works well.
+ */
+@Accessors(prefix = "m")
+@AllArgsConstructor
+public class RxSyncbase implements AutoCloseable {
+    public static String syncgroupName(final String sgHost, final String sgSuffix) {
+        return NamingUtil.join(sgHost, "%%sync", sgSuffix);
+    }
+
+    @Getter private final VContext mVContext;
+    private final SyncbaseClient mClient;
+
+    public Observable<Server> getRxServer() {
+        return mClient.getRxServer();
+    }
+
+    public Observable<SyncbaseService> getRxClient() {
+        return mClient.getRxClient();
+    }
+
+    public RxSyncbase(final Context androidContext, final VContext ctx,
+                      final Observable<Blessings> rxBlessings) {
+        mVContext = ctx;
+        mClient = new SyncbaseClient(androidContext, rxBlessings);
+    }
+
+    public RxSyncbase(final VAndroidContextTrait trait) {
+        this(trait.getAndroidContext(), trait.getVContext(),
+                trait.getBlessingsProvider().getRxBlessings());
+    }
+
+    public void close() {
+        mClient.close();
+    }
+
+    public RxApp rxApp(final String name) {
+        return new RxApp(name, this);
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/RxTable.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/RxTable.java
new file mode 100644
index 0000000..60aa58b
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/RxTable.java
@@ -0,0 +1,151 @@
+// Copyright 2015 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.rx.syncbase;
+
+import io.v.rx.RxVIterable;
+import io.v.rx.VFn;
+import io.v.v23.VIterable;
+import io.v.v23.context.CancelableVContext;
+import io.v.v23.context.VContext;
+import io.v.v23.services.syncbase.nosql.BatchOptions;
+import io.v.v23.services.watch.ResumeMarker;
+import io.v.v23.syncbase.nosql.BatchDatabase;
+import io.v.v23.syncbase.nosql.Database;
+import io.v.v23.syncbase.nosql.DatabaseCore;
+import io.v.v23.syncbase.nosql.Table;
+import io.v.v23.syncbase.nosql.WatchChange;
+import io.v.v23.verror.NoExistException;
+import io.v.v23.verror.VException;
+import lombok.Getter;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import rx.Observable;
+import rx.Subscriber;
+import rx.schedulers.Schedulers;
+import rx.subscriptions.Subscriptions;
+
+@Accessors(prefix = "m")
+@Getter
+@Slf4j
+public class RxTable extends RxEntity<Table, DatabaseCore> {
+    private final VContext mVContext;
+    private final String mName;
+    private final RxDb mRxDb;
+
+    private final Observable<Table> mObservable;
+
+    public RxTable(final String name, final RxDb rxDb) {
+        mVContext = rxDb.getVContext();
+        mName = name;
+        mRxDb = rxDb;
+
+        mObservable = rxDb.getObservable().map(VFn.unchecked(this::mapFrom));
+    }
+
+    @Override
+    public Table mapFrom(final DatabaseCore db) throws VException {
+        final Table t = db.getTable(mName);
+        SyncbaseEntity.compose(t::exists, t::create).ensureExists(mVContext, null);
+        return t;
+    }
+
+    private <T> T getInitial(final BatchDatabase db, final String tableName, final String key,
+                             final Class<T> type, final T defaultValue) throws VException {
+        try {
+            @SuppressWarnings("unchecked")
+            final T fromGet = (T) db.getTable(tableName).get(
+                    mVContext, key, type);
+            return fromGet;
+        } catch (final NoExistException e) {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Wraps a prefix watch stream in a key-specific observable. It remains to be seen whether it
+     * will be better to feature-request an exact-match watch API from Syncbase or consolidate all
+     * watches into one stream. Exact-match presents a cleaner API boundary but results in more
+     * underlying streams, whereas consolidating at the library level will usually be more efficient
+     * unless large portions of data won't need to be watched, and also it opens up questions of
+     * whether we should computationally optimize the prefix query.
+     *
+     * @return an observable wrapping the watch stream. This observable should only be subscribed to
+     * once, as we can only iterate over the underlying stream once.
+     */
+    private static <T> Observable<WatchEvent<T>> observeWatchStream(
+            final VIterable<WatchChange> s, final String key, final T defaultValue) {
+        return RxVIterable.wrap(s)
+                .filter(c -> c.getRowName().equals(key))
+                /*
+                About the Vfn.unchecked, on error, the wrapping replay will disconnect, calling
+                cancellation (see cancelOnDisconnect). The possible source of VException here is VOM
+                decoding.
+                 */
+                .map(VFn.unchecked(c -> {
+                    return WatchEvent.fromWatchChange(c, defaultValue);
+                }))
+                .distinctUntilChanged();
+    }
+
+    private void cancelContextOnDisconnect(final Subscriber<?> subscriber,
+                                           final CancelableVContext cancelable,
+                                           final String key) {
+        subscriber.add(Subscriptions.create(() -> {
+            log.debug("Cancelling watch on {}: {}", mName, key);
+            cancelable.cancel();
+        }));
+    }
+
+    private <T> void subscribeWatch(final Subscriber<? super WatchEvent<T>> subscriber,
+                                    final Database db, final String key, final Class<T> type,
+                                    final T defaultValue) throws VException {
+        /*
+        Watch will not work properly unless the table exists (sync will not create the table),
+        and table creation must happen outside the batch.
+        https://github.com/vanadium/issues/issues/857
+        */
+        mapFrom(db);
+
+        final BatchDatabase batch = db.beginBatch(mVContext, new BatchOptions("", true));
+        final T initial = getInitial(batch, mName, key, type, defaultValue);
+        final ResumeMarker r = batch.getResumeMarker(mVContext);
+        subscriber.onNext(new WatchEvent<>(initial, r, false));
+        batch.abort(mVContext);
+
+        final CancelableVContext cancelable = mVContext.withCancel();
+        final VIterable<WatchChange> s = db.watch(cancelable, mName, key, r);
+        log.debug("Watching {}: {}", mName, key);
+        cancelContextOnDisconnect(subscriber, cancelable, key);
+        observeWatchStream(s, key, defaultValue).subscribe(subscriber);
+    }
+
+    /**
+     * Watches a specific Syncbase row for changes.
+     *
+     * TODO(rosswang): Cache this by args.
+     */
+    public <T> Observable<WatchEvent<T>> watch(final String key, final Class<T> type,
+                                               final T defaultValue) {
+        return Observable.<WatchEvent<T>>create(subscriber -> mRxDb.getObservable()
+                .observeOn(Schedulers.io())
+                .subscribe(
+                        VFn.unchecked(db -> {
+                            /*
+                            Could be an expression lambda, but that confuses both RetroLambda and
+                            AndroidStudio.
+                            */
+                            subscribeWatch(subscriber, db, key, type, defaultValue);
+                        }),
+                        subscriber::onError
+                        //onComplete is connected by subscribeWatch/observeWatchStream.subscribe
+                ))
+                /*
+                Don't create new watch streams for subsequent subscribers, but do cancel the stream
+                if no subscribers are listening (and restart if new subscriptions happen).
+                 */
+                .replay(1)
+                .refCount();
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/SgHostUtil.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/SgHostUtil.java
new file mode 100644
index 0000000..112697f
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/SgHostUtil.java
@@ -0,0 +1,106 @@
+// Copyright 2015 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.rx.syncbase;
+
+import org.joda.time.Duration;
+
+import io.v.rx.MountEvent;
+import io.v.rx.RxNamespace;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.Server;
+import io.v.v23.syncbase.Syncbase;
+import io.v.v23.verror.TimeoutException;
+import io.v.v23.verror.VException;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+import rx.Observable;
+import rx.schedulers.Schedulers;
+
+/**
+ * This utility class is a short-term solution until a better solution for distributed syncgroup
+ * hosting is available.
+ */
+@Slf4j
+@UtilityClass
+public class SgHostUtil {
+    public static final Duration SYNCBASE_PING_TIMEOUT = Duration.standardSeconds(5);
+
+    /**
+     * This method blocks while it pings Syncbase at the given name.
+     *
+     * @throws InterruptedException if the thread is interrupted while waiting for the ping
+     *                              response. The default implementation does not throw this.
+     *                              TODO(rosswang): pick this out from the VException, if possible.
+     */
+    public static boolean isSyncbaseOnline(final VContext vContext, final String name)
+            throws InterruptedException {
+        final VContext pingContext = vContext.withTimeout(SYNCBASE_PING_TIMEOUT);
+        try {
+            /* It would be nice if there were a more straightforward ping. We can't just query the
+            mount table because the server might not have shut down cleanly. */
+            Syncbase.newService(name).getApp("ping").exists(pingContext);
+            return true;
+        } catch (final TimeoutException e) {
+            return false;
+        } catch (final VException e) {
+            log.error("Unexpected error while attempting to ping Syncgroup host at " + name, e);
+            return false;
+        }
+    }
+
+    /**
+     * @return {@code true} iff the mount event represents a successful mount.
+     */
+    private static boolean processMountEvent(final MountEvent e) {
+        e.getServer().ifPresentOrElse(
+                s -> e.getError().ifPresentOrElse(
+                        err -> log.error(String.format("Could not %s local Syncbase instance %s " +
+                                        "as syncgroup host %s",
+                                e.isMount() ? "mount" : "unmount", s, e.getName()), err),
+                        () -> log.info("{} local Syncbase instance {} as syncgroup host {}",
+                                e.isMount() ? "Mounted" : "Unmounted", s, e.getName())
+                ),
+                () -> e.getError().ifPresentOrElse(
+                        err -> log.error("Could not mount local Syncbase instance as syncgroup " +
+                                "host " + e.getName(), err),
+                        () -> log.info("Mounting local Syncbase instance as syncgroup host " +
+                                e.getName()))
+        );
+        return e.isSuccessfulMount();
+    }
+
+    private static Observable<MountEvent> mountSgHost(final Observable<Server> rxServer,
+                                                      final String name) {
+        return RxNamespace.mount(rxServer, name)
+                .filter(SgHostUtil::processMountEvent)
+                .retry((i, t) -> {
+                    log.error("Error maintaining mount of local Syncbase instance as " +
+                            "syncgroup host " + name, t);
+                    return t instanceof Exception;
+                })
+                .replay().refCount();
+    }
+
+    /**
+     * @return an observable that emits an item when a Syncbase instance is known to be hosted at
+     * the given name. The mount is updated for any new server instances until this observable has
+     * been unsubscribed.
+     */
+    public static Observable<Object> ensureSyncgroupHost(
+            final VContext vContext, final Observable<Server> rxServer, final String name) {
+        return rxServer.observeOn(Schedulers.io())
+        .switchMap(s -> {
+            try {
+                if (isSyncbaseOnline(vContext, name)) {
+                    return Observable.just(0);
+                } else {
+                    return mountSgHost(rxServer, name);
+                }
+            } catch (final InterruptedException e) {
+                return Observable.error(e);
+            }
+        });
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/SyncHostLevel.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/SyncHostLevel.java
new file mode 100644
index 0000000..260609e
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/SyncHostLevel.java
@@ -0,0 +1,14 @@
+// Copyright 2015 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.rx.syncbase;
+
+import java.util.List;
+
+public interface SyncHostLevel {
+    String DEFAULT_SG_HOST_SUFFIX = "sghost", DEFAULT_RENDEZVOUS_SUFFIX = "sgmt";
+
+    String getSyncgroupHostName(String username);
+    List<String> getRendezvousTableNames(String username);
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/SyncbaseEntity.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/SyncbaseEntity.java
new file mode 100644
index 0000000..be7a43e
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/SyncbaseEntity.java
@@ -0,0 +1,40 @@
+// Copyright 2015 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.rx.syncbase;
+
+import io.v.v23.context.VContext;
+import io.v.v23.security.access.Permissions;
+import io.v.v23.verror.ExistException;
+import io.v.v23.verror.VException;
+
+abstract class SyncbaseEntity implements ExistenceAware, Creatable {
+    public static SyncbaseEntity compose(final ExistenceAware fnExists, final Creatable fnCreate) {
+        return new SyncbaseEntity() {
+            @Override
+            public void create(VContext vContext, Permissions permissions) throws VException {
+                fnCreate.create(vContext, permissions);
+            }
+
+            @Override
+            public boolean exists(VContext vContext) throws VException {
+                return fnExists.exists(vContext);
+            }
+        };
+    }
+
+    /**
+     * Utility for Syncbase entities with lazy creation semantics. It would be great if this were
+     * instead factored into a V23 interface and utility.
+     */
+    public void ensureExists(final VContext vContext, final Permissions permissions)
+            throws VException {
+        if (!exists(vContext)) {
+            try {
+                create(vContext, permissions);
+            } catch (final ExistException ignore) {
+            }
+        }
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/UserAppSyncHost.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/UserAppSyncHost.java
new file mode 100644
index 0000000..13bd471
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/UserAppSyncHost.java
@@ -0,0 +1,34 @@
+// Copyright 2015 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.rx.syncbase;
+
+import android.content.Context;
+
+import java.util.Arrays;
+import java.util.List;
+
+import io.v.baku.toolkit.BlessingsUtils;
+import io.v.impl.google.naming.NamingUtil;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public class UserAppSyncHost implements SyncHostLevel {
+    private final String mAppName, mSgHostSuffix, mRendezvousSuffix;
+
+    public UserAppSyncHost(final Context androidContext) {
+        this(androidContext.getPackageName(), DEFAULT_SG_HOST_SUFFIX, DEFAULT_RENDEZVOUS_SUFFIX);
+    }
+
+    @Override
+    public String getSyncgroupHostName(final String username) {
+        return NamingUtil.join(BlessingsUtils.userMount(username), mAppName, mSgHostSuffix);
+    }
+
+    @Override
+    public List<String> getRendezvousTableNames(String username) {
+        return Arrays.asList(NamingUtil.join(
+                BlessingsUtils.userMount(username), mAppName, mRendezvousSuffix));
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/rx/syncbase/WatchEvent.java b/bakutoolkit/src/main/java/io/v/rx/syncbase/WatchEvent.java
new file mode 100644
index 0000000..9a9a3a5
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/rx/syncbase/WatchEvent.java
@@ -0,0 +1,37 @@
+// Copyright 2015 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.rx.syncbase;
+
+import io.v.v23.services.watch.ResumeMarker;
+import io.v.v23.syncbase.nosql.ChangeType;
+import io.v.v23.syncbase.nosql.WatchChange;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+import lombok.Value;
+import lombok.experimental.Accessors;
+
+@Accessors(prefix = "m")
+@Value
+public class WatchEvent<T> {
+    T mValue;
+    ResumeMarker mResumeMarker;
+    boolean mFromSync;
+
+    @SuppressWarnings("unchecked")
+    private static <T> T getWatchValue(final WatchChange change, final T defaultValue)
+            throws VException {
+        if (change.getChangeType() == ChangeType.DELETE_CHANGE) {
+            return defaultValue;
+        } else {
+            return (T) VomUtil.decode(change.getVomValue());
+        }
+    }
+
+    public static <T> WatchEvent<T> fromWatchChange(final WatchChange c, final T defaultValue)
+            throws VException {
+        return new WatchEvent<>(getWatchValue(c, defaultValue),
+                c.getResumeMarker(), c.isFromSync());
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/v23/rpc/MountStatusKey.java b/bakutoolkit/src/main/java/io/v/v23/rpc/MountStatusKey.java
new file mode 100644
index 0000000..589fb53
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/v23/rpc/MountStatusKey.java
@@ -0,0 +1,18 @@
+// Copyright 2015 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.v23.rpc;
+
+import lombok.Value;
+import lombok.experimental.Accessors;
+
+@Accessors(prefix = "m")
+@Value
+public class MountStatusKey {
+    String mName, mServer;
+
+    public static MountStatusKey fromMountStatus(final MountStatus m) {
+        return new MountStatusKey(m.getName(), m.getServer());
+    }
+}
diff --git a/bakutoolkit/src/main/java/io/v/v23/rpc/MountStatusValue.java b/bakutoolkit/src/main/java/io/v/v23/rpc/MountStatusValue.java
new file mode 100644
index 0000000..9d31d55
--- /dev/null
+++ b/bakutoolkit/src/main/java/io/v/v23/rpc/MountStatusValue.java
@@ -0,0 +1,27 @@
+// Copyright 2015 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.v23.rpc;
+
+import org.joda.time.DateTime;
+import org.joda.time.Duration;
+
+import io.v.v23.verror.VException;
+import lombok.Value;
+import lombok.experimental.Accessors;
+
+@Accessors(prefix = "m")
+@Value
+public class MountStatusValue {
+    DateTime mLastMount;
+    VException mLastMountError;
+    Duration mTtl;
+    DateTime mLastUnmount;
+    VException mLastUnmountError;
+
+    public static MountStatusValue fromMountStatus(final MountStatus m) {
+        return new MountStatusValue(m.getLastMount(), m.getLastMountError(), m.getTTL(),
+                m.getLastUnmount(), m.getLastUnmountError());
+    }
+}
diff --git a/bakutoolkit/src/main/res/layout-land/dialog_debug_log.xml b/bakutoolkit/src/main/res/layout-land/dialog_debug_log.xml
new file mode 100644
index 0000000..e36fd28
--- /dev/null
+++ b/bakutoolkit/src/main/res/layout-land/dialog_debug_log.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <FrameLayout
+        android:id="@+id/logcat_container"
+        android:layout_width="500dp"
+        android:layout_height="match_parent" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/bakutoolkit/src/main/res/layout/dialog_debug_log.xml b/bakutoolkit/src/main/res/layout/dialog_debug_log.xml
new file mode 100644
index 0000000..98ca2c6
--- /dev/null
+++ b/bakutoolkit/src/main/res/layout/dialog_debug_log.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <FrameLayout
+        android:id="@+id/pref_logging_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/debug_log_reset"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="@string/reset" />
+
+        <Button
+            android:id="@+id/debug_log_restart"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="@string/restart" />
+
+    </LinearLayout>
+
+    <FrameLayout
+        android:id="@+id/logcat_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+</LinearLayout>
\ No newline at end of file
diff --git a/bakutoolkit/src/main/res/layout/logcat.xml b/bakutoolkit/src/main/res/layout/logcat.xml
new file mode 100644
index 0000000..fd594fe
--- /dev/null
+++ b/bakutoolkit/src/main/res/layout/logcat.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <ListView
+        android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@android:color/background_light" />
+</LinearLayout>
\ No newline at end of file
diff --git a/bakutoolkit/src/main/res/menu/debug.xml b/bakutoolkit/src/main/res/menu/debug.xml
new file mode 100644
index 0000000..4eeaf62
--- /dev/null
+++ b/bakutoolkit/src/main/res/menu/debug.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/debug"
+          android:title="@string/debug"
+          android:orderInCategory="999">
+        <menu>
+            <item android:id="@+id/logging"
+                  android:title="@string/logging"/>
+            <item android:id="@+id/clear_app_data"
+                  android:title="@string/clear_app_data"/>
+            <item android:id="@+id/kill_process"
+                  android:title="@string/kill_process"/>
+        </menu>
+    </item>
+</menu>
\ No newline at end of file
diff --git a/bakutoolkit/src/main/res/values/errors.xml b/bakutoolkit/src/main/res/values/errors.xml
new file mode 100644
index 0000000..b0e7d99
--- /dev/null
+++ b/bakutoolkit/src/main/res/values/errors.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="err_app_clear">Unable to clear app data</string>
+    <string name="err_blessings_assume">Unable to assume blessings</string>
+    <string name="err_blessings_decode">Unable to decode blessings</string>
+    <string name="err_blessings_misc">Unexpected error while attaining blessings</string>
+    <string name="err_blessings_required">Blessings required</string>
+    <string name="err_blessings_store">Unable to store blessings</string>
+    <string name="err_sync">Unable to sync state</string>
+    <string name="err_syncgroup_join">Unable to join syncgroup</string>
+    <string name="err_misc">Unknown error</string>
+    <string name="err_vinit_options">Invalid Vanadium options</string>
+</resources>
\ No newline at end of file
diff --git a/bakutoolkit/src/main/res/values/strings.xml b/bakutoolkit/src/main/res/values/strings.xml
new file mode 100644
index 0000000..48a0718
--- /dev/null
+++ b/bakutoolkit/src/main/res/values/strings.xml
@@ -0,0 +1,12 @@
+<resources>
+    <string name="app_name">Baku Toolkit</string>
+    <string name="clear_app_data">Clear app data</string>
+    <string name="debug">Debug</string>
+    <string name="kill_process">Kill process</string>
+    <string name="logging">Logging</string>
+    <string name="vlevel">V23 log verbosity</string>
+    <string name="vmodule">V23 module log verbosity</string>
+
+    <string name="reset">Reset</string>
+    <string name="restart">Restart app</string>
+</resources>
diff --git a/bakutoolkit/src/main/res/xml/pref_logging.xml b/bakutoolkit/src/main/res/xml/pref_logging.xml
new file mode 100644
index 0000000..74580c2
--- /dev/null
+++ b/bakutoolkit/src/main/res/xml/pref_logging.xml
@@ -0,0 +1,20 @@
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+   <EditTextPreference
+        android:id="@+id/pref_vlevel"
+        android:key="io.v.v23.LOG_VLEVEL"
+        android:title="@string/vlevel"
+        android:selectAllOnFocus="true"
+        android:inputType="number"
+        android:singleLine="true"
+        android:maxLines="1" />
+
+    <EditTextPreference
+        android:id="@+id/pref_vmodule"
+        android:key="io.v.v23.LOG_VMODULE"
+        android:title="@string/vmodule"
+        android:selectAllOnFocus="false"
+        android:singleLine="true"
+        android:maxLines="1" />
+
+</PreferenceScreen>
diff --git a/bakutoolkit/src/test/java/io/v/baku/toolkit/BlessedActivityTraitTest.java b/bakutoolkit/src/test/java/io/v/baku/toolkit/BlessedActivityTraitTest.java
new file mode 100644
index 0000000..cf9d92c
--- /dev/null
+++ b/bakutoolkit/src/test/java/io/v/baku/toolkit/BlessedActivityTraitTest.java
@@ -0,0 +1,151 @@
+// Copyright 2015 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.baku.toolkit;
+
+import android.app.Activity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.android.libs.security.BlessingsManager;
+import io.v.v23.security.Blessings;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import rx.Observable;
+import rx.functions.Action1;
+import rx.subjects.PublishSubject;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({Blessings.class, BlessingsManager.class})
+public class BlessedActivityTraitTest {
+    private static class MockBlessedActivityTrait extends BlessedActivityTrait {
+        @RequiredArgsConstructor
+        @ToString
+        private static class ErrorEntry {
+            public final String summary;
+            public final Throwable t;
+        }
+
+        private final List<ErrorEntry> mErrors = new ArrayList<>();
+        private final PublishSubject<Blessings> mBlessingsSubject = PublishSubject.create();
+
+        public MockBlessedActivityTrait(final Activity activity,
+                                        final ErrorReporter errorReporter) {
+            super(activity, errorReporter);
+        }
+
+        @Override
+        protected Observable<Blessings> seekBlessings() {
+            return mBlessingsSubject;
+        }
+
+        public void offerBlessings(final Blessings blessings) {
+            mBlessingsSubject.onNext(blessings);
+        }
+    }
+
+    @Before
+    public void setUp() {
+        mockStatic(BlessingsManager.class);
+    }
+
+    @Test
+    public void testColdPassive() {
+        // would NPE if it tries to do anytyhing
+        new MockBlessedActivityTrait(null, null)
+                .getPassiveRxBlessings()
+                .subscribe(b -> fail("Unexpected blessings " + b));
+    }
+
+    @Test
+    public void testBlessingsFromManager() throws Exception {
+        final Activity activity = mock(Activity.class);
+        @SuppressWarnings("unchecked")
+        final Action1<Blessings>
+                cold = mock(Action1.class),
+                hot = mock(Action1.class);
+        final Blessings
+                b1 = PowerMockito.mock(Blessings.class),
+                b2 = PowerMockito.mock(Blessings.class);
+
+        PowerMockito.when(BlessingsManager.getBlessings(any())).thenReturn(b1, b2);
+
+        final MockBlessedActivityTrait t = new MockBlessedActivityTrait(activity, null);
+        t.getPassiveRxBlessings().subscribe(cold);
+
+        t.getRxBlessings().subscribe(hot);
+        verify(hot).call(b1);
+        verify(cold).call(b1);
+        verify(hot, never()).call(b2);
+
+        t.refreshBlessings();
+        verify(hot).call(b2);
+        verify(cold).call(b2);
+    }
+
+    @Test
+    public void testBlessingsFromProvider() throws Exception {
+        final Activity activity = mock(Activity.class);
+        @SuppressWarnings("unchecked")
+        final Action1<Blessings> s = mock(Action1.class);
+        final Blessings
+                b1 = PowerMockito.mock(Blessings.class),
+                b2 = PowerMockito.mock(Blessings.class);
+
+        final MockBlessedActivityTrait t = new MockBlessedActivityTrait(activity, null);
+        t.getRxBlessings().subscribe(s);
+        verify(s, never()).call(any());
+
+        t.offerBlessings(b1);
+        verify(s).call(b1);
+
+        t.refreshBlessings();
+        // The mock BlessingsManager will default to null, so it will seek blessings again.
+        t.offerBlessings(b2);
+        verify(s).call(b2);
+    }
+
+    /**
+     * Verifies that if a new subscriber needs to seek blessings, the new subscriber does not
+     * receive blessings until the seek completes (does not receive old blessings), and that the old
+     * subscriber is refreshed as well.
+     */
+    @Test
+    public void testDeferOnNewSubscriber() throws Exception {
+        final Activity activity = mock(Activity.class);
+        @SuppressWarnings("unchecked")
+        final Action1<Blessings>
+                s1 = mock(Action1.class),
+                s2 = mock(Action1.class);
+        final Blessings
+                b1 = PowerMockito.mock(Blessings.class),
+                b2 = PowerMockito.mock(Blessings.class);
+
+        final MockBlessedActivityTrait t = new MockBlessedActivityTrait(activity, null);
+        t.getRxBlessings().subscribe(s1);
+        t.offerBlessings(b1);
+
+        t.getRxBlessings().subscribe(s2);
+        verify(s2, never()).call(any());
+
+        t.offerBlessings(b2);
+        verify(s1).call(b2);
+        verify(s2).call(b2);
+    }
+}
diff --git a/bakutoolkit/src/test/java/io/v/baku/toolkit/bind/SyncbaseBindingTerminiTest.java b/bakutoolkit/src/test/java/io/v/baku/toolkit/bind/SyncbaseBindingTerminiTest.java
new file mode 100644
index 0000000..ee2d659
--- /dev/null
+++ b/bakutoolkit/src/test/java/io/v/baku/toolkit/bind/SyncbaseBindingTerminiTest.java
@@ -0,0 +1,61 @@
+// Copyright 2015 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.baku.toolkit.bind;
+
+import org.junit.Test;
+
+import io.v.rx.RxTestCase;
+import io.v.rx.syncbase.RxTable;
+import io.v.v23.syncbase.nosql.Table;
+import rx.subjects.PublishSubject;
+import rx.subjects.ReplaySubject;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class SyncbaseBindingTerminiTest extends RxTestCase {
+    @Test
+    public void testSequencing() throws Exception {
+        final ReplaySubject<Table> mockTables = ReplaySubject.createWithSize(1);
+        final RxTable rxTable = mock(RxTable.class);
+        when(rxTable.getObservable()).thenReturn(mockTables);
+        when(rxTable.once()).thenCallRealMethod();
+
+        final PublishSubject<Integer> rxData = PublishSubject.create();
+
+        final String key = "key";
+        SyncbaseBindingTermini.bindWrite(rxTable, rxData, key, Integer.class, null,
+                this::catchAsync);
+
+        rxData.onNext(1);
+        Thread.sleep(BLOCKING_DELAY_MS);
+
+        rxData.onNext(2);
+        Thread.sleep(BLOCKING_DELAY_MS);
+
+        final Table t1 = mock(Table.class);
+        mockTables.onNext(t1);
+        Thread.sleep(BLOCKING_DELAY_MS);
+        verify(t1).put(null, key, 2, Integer.class);
+
+        rxData.onNext(3);
+        Thread.sleep(BLOCKING_DELAY_MS);
+        verify(t1).put(null, key, 3, Integer.class);
+
+        verifyNoMoreInteractions(t1);
+
+        final Table t2 = mock(Table.class);
+        mockTables.onNext(t2);
+        Thread.sleep(BLOCKING_DELAY_MS);
+        verifyZeroInteractions(t2);
+
+        rxData.onNext(4);
+        Thread.sleep(BLOCKING_DELAY_MS);
+        verify(t2).put(null, key, 4, Integer.class);
+    }
+}
diff --git a/bakutoolkit/src/test/java/io/v/rx/RxNamespaceTest.java b/bakutoolkit/src/test/java/io/v/rx/RxNamespaceTest.java
new file mode 100644
index 0000000..abeb807
--- /dev/null
+++ b/bakutoolkit/src/test/java/io/v/rx/RxNamespaceTest.java
@@ -0,0 +1,166 @@
+// Copyright 2015 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.rx;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeConstants;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import io.v.v23.rpc.MountStatus;
+import io.v.v23.rpc.Server;
+import io.v.v23.rpc.ServerStatus;
+import java8.util.function.Predicate;
+import lombok.experimental.Accessors;
+import rx.Observable;
+import rx.Subscriber;
+import rx.subjects.ReplaySubject;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class RxNamespaceTest extends RxTestCase {
+    /**
+     * There is a hack in use in {@link RxNamespace} that compares any timestamp against an
+     * arbitrary value after 1754, currently 1970. For this test, use 2000 as the time in the past.
+     */
+    private static final DateTime
+            THE_PAST = new DateTime(2000, DateTimeConstants.JANUARY, 1, 0, 0),
+            THE_DISTANT_PAST = new DateTime(1990, DateTimeConstants.JANUARY, 1, 0, 0);
+    private static final long STATUS_POLLING_DELAY_MS = verificationDelay(
+            RxMountState.DEFAULT_POLLING_INTERVAL);
+
+    @Accessors(prefix = "m")
+    private class TestSubscriber extends Subscriber<MountEvent> {
+        public boolean mayComplete;
+        /**
+         * This is a queue only as an implementation detail; we do not actually verify order here.
+         */
+        public final Collection<Predicate<MountEvent>> expectedEvents =
+                new ConcurrentLinkedQueue<>();
+
+        @Override
+        public void onCompleted() {
+            try {
+                assertTrue("Unexpectedly unsubscribed", mayComplete);
+            } catch (final Throwable t) {
+                catchAsync(t);
+            }
+        }
+
+        @Override
+        public void onError(final Throwable t) {
+            catchAsync(t);
+        }
+
+        @Override
+        public void onNext(final MountEvent e) {
+            for (final Iterator<Predicate<MountEvent>> tests = expectedEvents.iterator();
+                 tests.hasNext();) {
+                if (tests.next().test(e)) {
+                    tests.remove();
+                    return;
+                }
+            }
+            //all-else
+            fail("Unexpected mount event: " + e);
+        }
+
+        public void assertNoLeftoverExpectations() {
+            //It would be nice to print these, but lambdas don't stringize into anything meaningful.
+            assertTrue("Leftover expectations", expectedEvents.isEmpty());
+        }
+    }
+
+    private static ServerStatus mockStatus(final MountStatus... ms) {
+        return new ServerStatus(null, false, ms, null, null);
+    }
+
+    private static boolean isInitialMountAttemptStart(final MountEvent e) {
+        return e.isMount() && !e.getServer().isPresent() && !e.getError().isPresent();
+    }
+
+    private final Server mServer = mock(Server.class);
+    private final ReplaySubject<Server> mRxServer = ReplaySubject.create();
+    private final TestSubscriber mSubscriber = new TestSubscriber();
+
+    @Before
+    public void setUp() {
+        mRxServer.onNext(mServer);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mSubscriber.assertNoLeftoverExpectations();
+    }
+
+    private void attachSubscriber(final Observable<MountEvent> mount) {
+        mount.share().subscribe(mSubscriber);
+    }
+
+    @Test
+    public void testAlreadyMounted() {
+        when(mServer.getStatus()).thenReturn(mockStatus(new MountStatus("foo", "bar",
+                THE_PAST, null, null, THE_DISTANT_PAST, null)));
+
+        mSubscriber.expectedEvents.add(MountEvent::isSuccessfulMount);
+
+        attachSubscriber(RxNamespace.mount(mRxServer, "foo"));
+    }
+
+    @Test
+    public void testExistingOtherNames() {
+        when(mServer.getStatus()).thenReturn(mockStatus(new MountStatus("baz", "bar",
+                THE_PAST, null, null, THE_DISTANT_PAST, null)));
+
+        mSubscriber.expectedEvents.add(RxNamespaceTest::isInitialMountAttemptStart);
+
+        attachSubscriber(RxNamespace.mount(mRxServer, "foo"));
+    }
+
+    private void mountWithOldStatus() {
+        when(mServer.getStatus()).thenReturn(mockStatus(new MountStatus("foo", "bar",
+                THE_DISTANT_PAST, null, null, THE_PAST, null)));
+
+        mSubscriber.expectedEvents.add(RxNamespaceTest::isInitialMountAttemptStart);
+
+        attachSubscriber(RxNamespace.mount(mRxServer, "foo"));
+    }
+
+    @Test
+    public void testMountWithOlderEvents() throws InterruptedException {
+        mountWithOldStatus();
+
+        Thread.sleep(STATUS_POLLING_DELAY_MS);
+        assertNoAsyncErrors();
+    }
+
+    @Test
+    public void testUnsubscribeOnUnmount() throws InterruptedException {
+        mountWithOldStatus();
+        Thread.sleep(BLOCKING_DELAY_MS);
+        assertNoAsyncErrors();
+
+        mSubscriber.mayComplete = true;
+        when(mServer.getStatus()).thenReturn(mockStatus(new MountStatus("foo", "bar",
+                THE_DISTANT_PAST, null, null, DateTime.now(), null)));
+        Thread.sleep(STATUS_POLLING_DELAY_MS);
+        assertTrue("Mount should unsubscribe after unmount", mSubscriber.isUnsubscribed());
+
+        when(mServer.getStatus()).then(i -> {
+            fail("Polling should stop after an unmount");
+            return null;
+        });
+        Thread.sleep(STATUS_POLLING_DELAY_MS);
+        assertNoAsyncErrors();
+    }
+}
diff --git a/bakutoolkit/src/test/java/io/v/rx/RxTestCase.java b/bakutoolkit/src/test/java/io/v/rx/RxTestCase.java
new file mode 100644
index 0000000..d01a542
--- /dev/null
+++ b/bakutoolkit/src/test/java/io/v/rx/RxTestCase.java
@@ -0,0 +1,45 @@
+// Copyright 2015 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.rx;
+
+import com.google.common.base.Throwables;
+
+import org.joda.time.Duration;
+import org.junit.After;
+
+import java.util.Collection;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import java8.util.stream.Collectors;
+import java8.util.stream.StreamSupport;
+
+import static org.junit.Assert.fail;
+
+public abstract class RxTestCase {
+    public static final long BLOCKING_DELAY_MS = 250;
+
+    public static long verificationDelay(final Duration nominal) {
+        return 2 * nominal.getMillis();
+    }
+
+    private final Collection<Throwable> mErrors = new ConcurrentLinkedQueue<>();
+
+
+    public void catchAsync(final Throwable t) {
+        mErrors.add(t);
+    }
+
+    /**
+     * Tests should call this where it make sense and to fail early if possible.
+     */
+    @After
+    public void assertNoAsyncErrors() {
+        if (!mErrors.isEmpty()) {
+            fail(StreamSupport.stream(mErrors)
+                    .map(Throwables::getStackTraceAsString)
+                    .collect(Collectors.joining("\n")));
+        }
+    }
+}
diff --git a/bakutoolkit/src/test/java/io/v/rx/syncbase/GlobalUserSyncgroupTest.java b/bakutoolkit/src/test/java/io/v/rx/syncbase/GlobalUserSyncgroupTest.java
new file mode 100644
index 0000000..565f7d5
--- /dev/null
+++ b/bakutoolkit/src/test/java/io/v/rx/syncbase/GlobalUserSyncgroupTest.java
@@ -0,0 +1,169 @@
+package io.v.rx.syncbase;// Copyright 2015 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.
+
+import com.google.common.base.Stopwatch;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.v.baku.toolkit.BlessingsUtils;
+import io.v.debug.SyncbaseClient;
+import io.v.rx.RxMountState;
+import io.v.rx.RxTestCase;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.MountStatus;
+import io.v.v23.rpc.Server;
+import io.v.v23.rpc.ServerStatus;
+import io.v.v23.security.Blessings;
+import io.v.v23.syncbase.SyncbaseApp;
+import io.v.v23.syncbase.SyncbaseService;
+import io.v.v23.syncbase.nosql.Database;
+import io.v.v23.syncbase.nosql.Syncgroup;
+import java8.util.stream.RefStreams;
+import rx.Subscription;
+import rx.subjects.ReplaySubject;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(PowerMockRunner.class)
+@SuppressStaticInitializationFor("io.v.baku.toolkit.BlessingsUtils")
+@PrepareForTest({BlessingsUtils.class, SgHostUtil.class})
+public class GlobalUserSyncgroupTest extends RxTestCase {
+    private static final long STATUS_POLLING_DELAY_MS = verificationDelay(
+            RxMountState.DEFAULT_POLLING_INTERVAL);
+
+    private final VContext mVContext = mock(VContext.class);
+
+    private final ReplaySubject<Blessings> mRxBlessings = ReplaySubject.create();
+    private final ReplaySubject<Server> mRxServer = ReplaySubject.create();
+    private final ReplaySubject<SyncbaseService> mRxClient = ReplaySubject.create();
+
+    private final Server mServer = mock(Server.class);
+    private final SyncbaseService mClient = mock(SyncbaseService.class);
+    private final Database mDb = mock(Database.class);
+    private final Syncgroup mSg = mock(Syncgroup.class);
+
+    private RxSyncbase mSb;
+
+    @Before
+    public void setUp() throws Exception {
+        final SyncbaseClient sbClient = mock(SyncbaseClient.class);
+        final SyncbaseApp app = mock(SyncbaseApp.class);
+
+        when(sbClient.getRxServer()).thenReturn(mRxServer);
+        when(sbClient.getRxClient()).thenReturn(mRxClient);
+
+        mRxServer.onNext(mServer);
+        mRxClient.onNext(mClient);
+
+        when(mClient.getApp("app")).thenReturn(app);
+        when(app.getNoSqlDatabase(eq("db"), any())).thenReturn(mDb);
+
+        mSb = new RxSyncbase(null, sbClient);
+
+        PowerMockito.spy(SgHostUtil.class);
+    }
+
+    private Subscription joinMockSyncgroup() throws Exception {
+        when(mDb.getSyncgroup(RxSyncbase.syncgroupName("users/foo@bar.com/app/sghost", "sg")))
+                .thenReturn(mSg);
+        final Stopwatch t = Stopwatch.createStarted();
+
+        final Subscription subscription = GlobalUserSyncgroup.builder()
+                .vContext(mVContext)
+                .rxBlessings(mRxBlessings)
+                .syncHostLevel(new UserAppSyncHost("app", "sghost", "sgmt"))
+                .sgSuffix("sg")
+                .syncbase(mSb)
+                .db(mSb.rxApp("app").rxDb("db"))
+                .onError((m, e) -> catchAsync(e))
+                .build()
+                .join();
+
+        PowerMockito.spy(BlessingsUtils.class);
+        PowerMockito.doReturn(RefStreams.of("foo@bar.com"))
+                .when(BlessingsUtils.class, "blessingsToUsernameStream", any(), any());
+        PowerMockito.doReturn(null)
+                .when(BlessingsUtils.class, "blessingsToAcl", any(), any());
+        PowerMockito.doReturn(null)
+                .when(BlessingsUtils.class, "homogeneousPermissions", any(), any());
+
+        long elapsed = t.elapsed(TimeUnit.MILLISECONDS);
+        if (elapsed > BLOCKING_DELAY_MS) {
+            fail("GlobalUserSyncgroup.join should not block; took " + elapsed + " ms (threshold " +
+                    BLOCKING_DELAY_MS + " ms)");
+        }
+        return subscription;
+    }
+
+    @Test
+    public void testJoinAlreadyMounted() throws Exception {
+        PowerMockito.doReturn(true)
+                .when(SgHostUtil.class, "isSyncbaseOnline", any(), any());
+        joinMockSyncgroup();
+        mRxBlessings.onNext(null);
+        Thread.sleep(STATUS_POLLING_DELAY_MS +
+                SgHostUtil.SYNCBASE_PING_TIMEOUT.getMillis());
+        verify(mServer, never()).addName(any());
+        verify(mServer, never()).getStatus();
+        verify(mSg).join(any(), any());
+    }
+
+    @Test
+    public void testJoinMountLifecycle() throws Exception {
+        PowerMockito.doReturn(false)
+                .when(SgHostUtil.class, "isSyncbaseOnline", any(), any());
+        final Subscription subscription = joinMockSyncgroup();
+
+        final AtomicInteger statusPolls = new AtomicInteger();
+        final ServerStatus serverStatus = mock(ServerStatus.class);
+        when(serverStatus.getMounts()).then(i -> {
+            statusPolls.incrementAndGet();
+            return new MountStatus[0];
+        });
+        when(mServer.getStatus()).thenReturn(serverStatus);
+
+        Thread.sleep(STATUS_POLLING_DELAY_MS);
+        verify(mServer, never()).addName(any());
+        assertEquals("Polling should not start until blessings are resolved", 0, statusPolls.get());
+
+        mRxBlessings.onNext(null);
+        //Verify 3 polls + initial to ensure polling loop is working.
+        Thread.sleep(4 * RxMountState.DEFAULT_POLLING_INTERVAL.getMillis() +
+                SgHostUtil.SYNCBASE_PING_TIMEOUT.getMillis());
+        verify(mServer).addName("users/foo@bar.com/app/sghost");
+        final int nPolls = statusPolls.get();
+        if (nPolls < 4) {
+            fail("Polling should start and continue after blessings are resolved (" + nPolls +
+                    "/4-5 expected polls)");
+        }
+
+        try {
+            subscription.unsubscribe();
+        } catch (final IllegalArgumentException expected) {
+            // https://github.com/ReactiveX/RxJava/pull/3167
+            // This should be fixed in the next version of RxJava.
+        }
+        Thread.sleep(BLOCKING_DELAY_MS);
+        statusPolls.set(0);
+        Thread.sleep(STATUS_POLLING_DELAY_MS);
+        assertEquals("Polling should stop after Syncgroup join is unsubscribed",
+                0, statusPolls.get());
+    }
+}
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..7975321
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,22 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        jcenter()
+        mavenCentral()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:1.3.0'
+        classpath 'me.tatarka:gradle-retrolambda:3.2.3'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+        mavenCentral()
+    }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..1d3591c
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..de00d04
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Oct 15 16:37:26 PDT 2015
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windowz variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+if "%@eval[2+2]" == "4" goto 4NT_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+goto execute

+

+:4NT_args

+@rem Get arguments from the 4NT Shell from JP Software

+set CMD_LINE_ARGS=%$

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..f11ede4
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':bakutoolkit'