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'