Merge changes from topic 'baku-old'

* changes:
  TBR: Renaming SyncbaseXAdapter -> RxXAdapter
  More support for using ID list bindings
diff --git a/.gitignore b/.gitignore
index e5bb932..9ab0193 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
 /.jiri
-#TODO(nlacasse): Get rid of .v23 below once v23->jiri transition is complete.
+
 # Gradle dirs
 build/
 /buildSrc/schema
@@ -16,9 +16,6 @@
 # vdl files
 /src
 
-# Vanadium
-/.v23
-
 # Generated source
 generated-src/
-android-lib/src/main/jniLibs/
\ No newline at end of file
+android-lib/src/main/jniLibs/
diff --git a/android-lib/.gitignore b/android-lib/.gitignore
index e8fdac8..b8bd3b4 100644
--- a/android-lib/.gitignore
+++ b/android-lib/.gitignore
@@ -4,9 +4,6 @@
 /bin/
 /vdl_bin/
 
-# Vanadium
-/.v23
-
 # Gradle
 .gradle/
 build/
diff --git a/android-lib/build.gradle b/android-lib/build.gradle
index 1f0511b..e96a85f 100644
--- a/android-lib/build.gradle
+++ b/android-lib/build.gradle
@@ -8,6 +8,7 @@
         classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4'
         classpath 'com.android.tools.build:gradle:1.2.3'
         classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0'
+        classpath 'io.v:gradle-plugin:1.4'
     }
 }
 
@@ -16,10 +17,11 @@
 apply plugin: 'maven-publish'
 apply plugin: 'wrapper'
 apply plugin: 'com.jfrog.bintray'
+apply plugin: 'io.v.vdl'
 
 // You should change this after releasing a new version of the library. See the
 // list of published versions at https://repo1.maven.org/maven2/io/v/vanadium-android.
-def releaseVersion = '1.5'
+def releaseVersion = '1.7'
 
 android {
     buildToolsVersion '23.0.1'
@@ -46,6 +48,7 @@
     compile project(':lib')
     compile 'com.android.support:support-v4:23.0.1'
     compile 'com.android.support:appcompat-v7:23.0.1'
+    compile "com.google.android.gms:play-services:8.3.0"
     androidTestCompile 'junit:junit:4.12'
     androidTestCompile 'com.google.truth:truth:0.25'
     // This dependency exists only to work around an issue in the sdkmanager plugin v0.12.0. This
@@ -56,6 +59,10 @@
     }
 }
 
+vdl {
+    inputPaths += 'src/main/java'
+}
+
 public static isDarwin() {
     return getOS().contains("os x")
 }
diff --git a/android-lib/src/main/AndroidManifest.xml b/android-lib/src/main/AndroidManifest.xml
index 800e17f..e83fd05 100644
--- a/android-lib/src/main/AndroidManifest.xml
+++ b/android-lib/src/main/AndroidManifest.xml
@@ -14,10 +14,45 @@
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
     <uses-permission android:name="android.permission.BLUETOOTH" />
 
+    <uses-permission android:name="android.permission.NFC" />
+
     <application>
-        <activity
-            android:name="io.v.android.impl.google.services.blessing.BlessingActivity"
+        <activity android:name="io.v.android.impl.google.services.blessing.BlessingActivity"
             android:excludeFromRecents="true"/>
+        <receiver
+            android:name="com.google.android.gms.gcm.GcmReceiver"
+            android:exported="true"
+            android:permission="com.google.android.c2dm.permission.SEND" >
+            <intent-filter>
+                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+                <category android:name="io.v.android" />
+            </intent-filter>
+        </receiver>
+        <service
+            android:name="io.v.android.impl.google.services.gcm.GcmTokenRefreshListenerService"
+            android:exported="false" >
+            <intent-filter>
+                <action android:name="com.google.android.gms.iid.InstanceID" />
+            </intent-filter>
+        </service>
+        <service
+            android:name="io.v.android.impl.google.services.gcm.GcmReceiveListenerService"
+            android:exported="false" >
+        </service>
+        <service
+            android:name="io.v.android.impl.google.services.gcm.GcmRegistrationService"
+            android:exported="false" >
+        </service>
+        <activity android:name="io.v.android.impl.google.services.beam.BeamActivity"
+            android:excludeFromRecents="true">
+            <intent-filter>
+                <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="vnd.android.nfc"
+                    android:host="ext"
+                    android:pathPrefix="/io.v.android.vbeam:vbs"/>
+            </intent-filter>
+        </activity>
     </application>
 
 </manifest>
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/beam/BeamActivity.java b/android-lib/src/main/java/io/v/android/impl/google/services/beam/BeamActivity.java
new file mode 100644
index 0000000..2f973f5
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/beam/BeamActivity.java
@@ -0,0 +1,107 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.impl.google.services.beam;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.nfc.NdefMessage;
+import android.nfc.NfcAdapter;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.app.ActivityCompat;
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.joda.time.Duration;
+
+import io.v.android.v23.V;
+import io.v.android.v23.VBeam;
+import io.v.v23.OptionDefs;
+import io.v.v23.Options;
+import io.v.v23.context.VContext;
+import io.v.v23.security.VSecurity;
+
+/**
+ * Handles the NDEF discovered intent on the receiver phone.
+ * It contacts the VBeam server on the sending phone, then retrieves and starts the shared intent.
+ */
+public class BeamActivity extends Activity {
+    private static final String TAG = "BeamActivity";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        NdefMessage msgs[] = null;
+        Intent intent = getIntent();
+
+        if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
+            Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
+            if (rawMsgs != null) {
+                msgs = new NdefMessage[rawMsgs.length];
+                for (int i = 0; i < rawMsgs.length; i++) {
+                    msgs[i] = (NdefMessage) rawMsgs[i];
+                }
+            }
+        }
+        if (msgs == null) {
+            Log.d(TAG, "No ndef messages");
+            finish();
+            return;
+        }
+        VBeamManager.Data data = null;
+        for (NdefMessage m : msgs) {
+            data = VBeamManager.decodeMessage(m);
+            if (data != null)
+                break;
+        }
+        if (data == null) {
+            Log.w(TAG, "Unable to deserialize data");
+            finish();
+            return;
+        }
+        Log.d(TAG, "connecting to " + data.name);
+        VContext ctx = V.init(this).withTimeout(Duration.standardSeconds(2));
+        Options opts = new Options();
+
+        opts.set(OptionDefs.SERVER_AUTHORIZER, VSecurity.newPublicKeyAuthorizer(data.key));
+        IntentBeamerClient client = IntentBeamerClientFactory.getIntentBeamerClient(data.name);
+        ListenableFuture<IntentBeamerClient.GetIntentOut> out =
+                client.getIntent(ctx, data.secret, opts);
+        Futures.addCallback(out, new FutureCallback<IntentBeamerClient.GetIntentOut>() {
+            @Override
+            public void onSuccess(IntentBeamerClient.GetIntentOut result) {
+                try {
+                    Log.d(TAG, "got intent " + result.intentUri);
+                    int flags = 0;
+                    if (result.intentUri.startsWith("intent:")) {
+                        flags = Intent.URI_INTENT_SCHEME;
+                    } else {
+                        flags = Intent.URI_ANDROID_APP_SCHEME;
+                    }
+                    Intent resultIntent = Intent.parseUri(result.intentUri, flags);
+                    resultIntent.putExtra(VBeamManager.EXTRA_VBEAM_PAYLOAD, result.payload);
+                    startActivity(resultIntent);
+                    finish();
+                } catch (Throwable t) {
+                    t.printStackTrace();
+                }
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                t.printStackTrace();
+                finish();
+            }
+        });
+    }
+}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamManager.java b/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamManager.java
new file mode 100644
index 0000000..d6c008e
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamManager.java
@@ -0,0 +1,144 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.impl.google.services.beam;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcEvent;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.security.interfaces.ECPublicKey;
+
+import io.v.android.v23.VBeam;
+import io.v.v23.V;
+import io.v.v23.context.VContext;
+import io.v.v23.naming.Endpoint;
+import io.v.v23.security.VSecurity;
+import io.v.v23.verror.VException;
+
+/**
+ * Internal implementation of {@link VBeam} for an activity.
+ */
+public class VBeamManager implements NfcAdapter.CreateNdefMessageCallback {
+
+    static final String EXTERNAL_DOMAIN = "io.v.android.vbeam";
+    static final String EXTERNAL_TYPE = "vbs";
+    private static final String EXTERNAL_PATH = "/" + EXTERNAL_DOMAIN + ":" + EXTERNAL_TYPE;
+    private static final String TAG = "VBeamManager";
+    private final VBeamServer server;
+    private final VContext context;
+    private final String packageName;
+
+    /**
+     * Key for the payload bytes in a VBeam intent.
+     */
+    public static final String EXTRA_VBEAM_PAYLOAD = "io.v.intent.vbeam.payload";
+
+    /**
+     * Starts the VBeam server and registers the activity to send NDEF messages.
+     * @param context
+     * @param activity
+     * @param creator
+     * @throws VException
+     */
+    public VBeamManager(VContext context, Activity activity, VBeam.IntentCreator creator)
+            throws VException {
+        this.server = new VBeamServer(creator);
+        this.packageName = activity.getApplicationContext().getPackageName();
+        this.context = V.withNewServer(
+                context, "", this.server, VSecurity.newAllowEveryoneAuthorizer());
+    }
+
+    @Override
+    public NdefMessage createNdefMessage(NfcEvent event) {
+        try {
+            String requestID = server.newRequest();
+
+            byte[] payload;
+            try {
+                Data data = new Data();
+                data.secret = requestID;
+                data.key = V.getPrincipal(context).publicKey();
+                data.name = getEndpointName();
+                ByteArrayOutputStream bos = new ByteArrayOutputStream();
+                ObjectOutputStream oos = new ObjectOutputStream(bos);
+                oos.writeObject(data);
+                oos.flush();
+                payload = bos.toByteArray();
+                oos.close();
+                bos.close();
+            } catch (IOException e) {
+                Log.e(TAG, "unable to encode ndef message", e);
+                return null;
+            }
+            return new NdefMessage(
+                    new NdefRecord[]{
+                            NdefRecord.createExternal(EXTERNAL_DOMAIN, EXTERNAL_TYPE, payload),
+                            NdefRecord.createApplicationRecord(packageName)
+                    }
+            );
+        } catch (Throwable t) {
+            Log.e(TAG, "createNdefMessage failed", t);
+            throw t;
+        }
+    }
+
+    static Data decodeMessage(NdefMessage m) {
+        for (NdefRecord r : m.getRecords()) {
+            Uri uri = r.toUri();
+            if (uri == null || !EXTERNAL_PATH.equals(uri.getPath())) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    StringBuilder description = new StringBuilder("ignoring ");
+                    if (uri == null) {
+                        description.append(r);
+                    } else {
+                        description.append(uri.getPath());
+                        description.append(" != ");
+                        description.append(EXTERNAL_PATH);
+                    }
+                    Log.d(TAG, description.toString());
+                }
+                continue;
+            }
+            byte[] payload = r.getPayload();
+            if (payload != null) {
+                ByteArrayInputStream bis = new ByteArrayInputStream(payload);
+                ObjectInputStream ois = null;
+                try {
+                    ois = new ObjectInputStream(bis);
+                    Object d = ois.readObject();
+                    if (d != null && d instanceof Data) {
+                        return (Data)d;
+                    }
+                } catch (IOException e) {
+                    Log.e(TAG, "decodeMessage", e);
+                } catch (ClassNotFoundException e) {
+                    Log.e(TAG, "decodeMessage", e);
+                }
+            }
+        }
+        return null;
+    }
+
+    private String getEndpointName() {
+        Endpoint[] endpoints = V.getServer(context).getStatus().getEndpoints();
+        return endpoints[0].name();
+    }
+
+    static class Data implements Serializable {
+        String name;
+        String secret;
+        ECPublicKey key;
+    }
+}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamServer.java b/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamServer.java
new file mode 100644
index 0000000..3ace363
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamServer.java
@@ -0,0 +1,84 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.impl.google.services.beam;
+
+import android.util.Pair;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import io.v.android.v23.VBeam;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.ServerCall;
+import io.v.v23.verror.VException;
+
+class VBeamServer implements IntentBeamerServer {
+    private static final long EXPIRATION_MS = 60000;
+
+    private final Map<String, Long> requestMap = new LinkedHashMap<>();
+    private final VBeam.IntentCreator callback;
+
+    public VBeamServer(VBeam.IntentCreator creator) throws VException {
+        this.callback = creator;
+    }
+
+    @Override
+    public synchronized ListenableFuture<GetIntentOut> getIntent(
+            VContext ctx, ServerCall call, String secret) {
+        evictStaleRequests();
+        if (requestMap.remove(secret) != null) {
+            try {
+                return Futures.transform(
+                        callback.createIntent(ctx, call),
+                        new AsyncFunction<Pair<String, byte[]>, GetIntentOut>() {
+
+                        @Override
+                        public ListenableFuture<GetIntentOut> apply(Pair<String, byte[]> input)
+                                throws Exception {
+                            return convertIntent(input);
+                        }
+                });
+            } catch (Throwable t) {
+                return Futures.immediateFailedFuture(t);
+            }
+        }
+        return Futures.immediateFailedFuture(new VException("Bad request"));
+    }
+
+    private static ListenableFuture<GetIntentOut> convertIntent(Pair<String, byte[]> intent) {
+        GetIntentOut out = new GetIntentOut();
+        out.intentUri = intent.first;
+        out.payload = intent.second;
+        return Futures.immediateFuture(out);
+    }
+
+    private synchronized void evictStaleRequests() {
+        long now = System.currentTimeMillis();
+        for (Iterator<Long> it = requestMap.values().iterator(); it.hasNext();) {
+            long l = it.next();
+            if (l > now || l < now - EXPIRATION_MS) {
+                it.remove();
+            } else {
+                // LinkedHashMap is sorted by insertion order, so oldest entry is first.
+                break;
+            }
+        }
+    }
+
+    synchronized String newRequest() {
+        evictStaleRequests();
+        String secret = UUID.randomUUID().toString();
+        synchronized(requestMap) {
+            requestMap.put(secret, System.currentTimeMillis());
+        }
+        return secret;
+    }
+}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/beam/beamer.vdl b/android-lib/src/main/java/io/v/android/impl/google/services/beam/beamer.vdl
new file mode 100644
index 0000000..a61f8cd
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/beam/beamer.vdl
@@ -0,0 +1,9 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package beam
+
+type IntentBeamer interface {
+  GetIntent(secret string) (intentUri string, payload []byte | error)
+}
\ No newline at end of file
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmReceiveListenerService.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmReceiveListenerService.java
new file mode 100644
index 0000000..2b2844b
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmReceiveListenerService.java
@@ -0,0 +1,28 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.impl.google.services.gcm;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import com.google.android.gms.gcm.GcmListenerService;
+
+import io.v.v23.verror.VException;
+
+/**
+ * Listens for GCM messages and wakes up services upon their receipt.
+ */
+public class GcmReceiveListenerService extends GcmListenerService {
+    private static final String TAG = "GcmRecvListenerService";
+
+    @Override
+    public void onMessageReceived(String from, Bundle data) {
+        try {
+            Util.wakeupServices(this, false);
+        } catch (VException e) {
+            Log.e(TAG, "Couldn't wakeup services.", e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmRegistrationService.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmRegistrationService.java
new file mode 100644
index 0000000..45936b7
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmRegistrationService.java
@@ -0,0 +1,53 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.impl.google.services.gcm;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import com.google.android.gms.gcm.GoogleCloudMessaging;
+import com.google.android.gms.iid.InstanceID;
+
+import java.io.IOException;
+
+import io.v.v23.verror.VException;
+
+/**
+ * Communicates with GCM servers to obtain a new registration token and then starts
+ * all app services that have registered themselves as wake-able.
+ */
+public class GcmRegistrationService extends IntentService {
+    private static final String TAG = "GcmRegistrationService";
+    public static final String GCM_TOKEN_PREF_KEY = "io.v.android.impl.google.services.gcm.TOKEN";
+    static final String EXTRA_RESTART_SERVICES = "RESTART_SERVICES";
+
+    public GcmRegistrationService() {
+        super("GcmRegistrationService");
+    }
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        try {
+            InstanceID instanceID = InstanceID.getInstance(this);
+            String token = instanceID.getToken("*", GoogleCloudMessaging.INSTANCE_ID_SCOPE);
+            // Store registration token in SharedPreferences.
+            SharedPreferences.Editor editor =
+                    PreferenceManager.getDefaultSharedPreferences(this).edit();
+            editor.putString(GCM_TOKEN_PREF_KEY, token);
+            editor.commit();
+        } catch (IOException e) {
+            Log.e(TAG, "Couldn't fetch GCM registration token: ", e);
+        }
+        boolean restartServices = intent.getBooleanExtra(EXTRA_RESTART_SERVICES, false);
+        try {
+            Util.wakeupServices(this, restartServices);
+        } catch (VException e) {
+            Log.e(TAG, "Couldn't wakeup services.", e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmTokenRefreshListenerService.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmTokenRefreshListenerService.java
new file mode 100644
index 0000000..a95eb5f
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmTokenRefreshListenerService.java
@@ -0,0 +1,21 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.impl.google.services.gcm;
+
+import android.content.Intent;
+
+import com.google.android.gms.iid.InstanceIDListenerService;
+
+/**
+ * Listens for server token change notifications and initiates token (and services) refresh.
+ */
+public class GcmTokenRefreshListenerService extends InstanceIDListenerService {
+    @Override
+    public void onTokenRefresh() {
+        Intent intent = new Intent(this, GcmRegistrationService.class);
+        intent.putExtra(GcmRegistrationService.EXTRA_RESTART_SERVICES, true);
+        startService(intent);
+    }
+}
\ No newline at end of file
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/Util.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/Util.java
new file mode 100644
index 0000000..a0ecc23
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/Util.java
@@ -0,0 +1,90 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.impl.google.services.gcm;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.v23.verror.VException;
+
+/**
+ * Utility GCM methods.
+ */
+class Util {
+    // Wakes up all services that have marked themselves for wakeup.
+    // If restart==true, services are first stopped and then started;  otherwise, they
+    // are just started.
+    static void wakeupServices(Context context, boolean restart) throws VException {
+        for (ServiceInfo service : getWakeableServices(context)) {
+            Intent intent = new Intent();
+            intent.setComponent(new ComponentName(service.packageName, service.name));
+            if (restart) {
+                context.stopService(intent);
+            }
+            context.startService(intent);
+        }
+    }
+
+    // Returns a list of all services that have marked themselves for wakeup.
+    static List<ServiceInfo> getWakeableServices(Context context) throws VException {
+        try {
+            ServiceInfo[] services = context.getPackageManager().getPackageInfo(
+                    context.getPackageName(),
+                    PackageManager.GET_META_DATA|PackageManager.GET_SERVICES).services;
+            if (services == null) {
+                throw new VException("Couldn't get services information for package: " +
+                        context.getPackageName());
+            }
+            ArrayList<ServiceInfo> ret = new ArrayList<>();
+            for (ServiceInfo service : services) {
+                if (service == null) continue;
+                if (!service.packageName.equals(context.getPackageName())) continue;
+                if (service.metaData == null) continue;
+                boolean wakeup = service.metaData.getBoolean("wakeup", false);
+                if (!wakeup) continue;
+                ret.add(service);
+            }
+            return ret;
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new VException(String.format(
+                    "Couldn't get package information for package %s: %s",
+                    context.getPackageName(), e.getMessage()));
+        }
+    }
+
+    /**
+     * Returns {@code true} iff the provided service is "wakeable", i.e., if it has the
+     * {@code "wakeup"} metadata attached to it.
+     *
+     * @param context     Android context representing the service
+     * @return            {@code true} iff the provided service is "wakeable"
+     * @throws VException if there was an error figuring out if a service is wakeable
+     */
+    public static boolean isServiceWakeable(Context context) throws VException {
+        if (!(context instanceof Service)) {
+            return false;
+        }
+        Service service = (Service) context;
+        try {
+            ServiceInfo info = service.getPackageManager().getServiceInfo(
+                    new ComponentName(service, service.getClass()), PackageManager.GET_META_DATA);
+            if (info.metaData == null) {
+                return false;
+            }
+            return info.metaData.getBoolean("wakeup", false);
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new VException(e.getMessage());
+        }
+    }
+
+    private Util() {}
+}
diff --git a/android-lib/src/main/java/io/v/android/v23/V.java b/android-lib/src/main/java/io/v/android/v23/V.java
index 6bd8920..4c849d8 100644
--- a/android-lib/src/main/java/io/v/android/v23/V.java
+++ b/android-lib/src/main/java/io/v/android/v23/V.java
@@ -5,12 +5,13 @@
 package io.v.android.v23;
 
 import android.content.Context;
+import android.content.Intent;
 
 import com.google.common.base.Preconditions;
 
+import io.v.android.impl.google.services.gcm.GcmRegistrationService;
 import io.v.v23.Options;
 import io.v.v23.context.VContext;
-import io.v.v23.rpc.Server;
 import io.v.v23.security.Blessings;
 import io.v.v23.security.BlessingStore;
 import io.v.v23.security.Constants;
@@ -80,11 +81,18 @@
                 throw new RuntimeException("Couldn't setup Vanadium principal", e);
             }
             globalContext = ctx;
+            // Start the GCM registration service, which obtains the GCM token for the app
+            // (if the app is configured to use GCM).
+            // NOTE: this call may lead to a recursive call to V.init() (in a separate thread),
+            // so keep this code below the line where 'globalContext' is set, or we may run
+            // into an infinite recursion.
+            Intent intent = new Intent(androidCtx, GcmRegistrationService.class);
+            androidCtx.startService(intent);
             return ctx;
         }
     }
 
-    // Inializes Vanadium state that's local to the invoking activity/service.
+    // Initializes Vanadium state that's local to the invoking activity/service.
     private static VContext initAndroidLocal(VContext ctx, Context androidCtx, Options opts) {
         return ctx.withValue(new AndroidContextKey(), androidCtx);
     }
diff --git a/android-lib/src/main/java/io/v/android/v23/VBeam.java b/android-lib/src/main/java/io/v/android/v23/VBeam.java
new file mode 100644
index 0000000..41ee7b6
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/v23/VBeam.java
@@ -0,0 +1,92 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.v23;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.nfc.NfcAdapter;
+import android.util.Pair;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import io.v.android.impl.google.services.beam.VBeamManager;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.ServerCall;
+import io.v.v23.security.Call;
+import io.v.v23.verror.VException;
+
+/**
+ * Enables sending authenticated data over Android Beam.
+ * <p>
+ * To support Beam in an activity you register an {@link io.v.android.v23.VBeam.IntentCreator}.
+ * When the user initiates an NFC tap, the framework will call
+ * {@link io.v.android.v23.VBeam.IntentCreator#createIntent(VContext, ServerCall)}. You should
+ * return an intent encapsulating the data you wish to share.
+ * <p>
+ * Internally this starts a Vanadium service on this phone. After tapping the app will start on
+ * the destination phone and contact the sender. You may then inspect the credentials the app
+ * is using on the destination phone and decide what data to send. For example, you may
+ * add their blessing to an ACL and then send the name of the object you are trying to share.
+ * <p>
+ * Beaming will fail if the app is in the foreground on both phones. The destination
+ * phone should not have the app open.
+ * <p>
+ * If the app is not installed on the destination phone, it will open in the Play store.
+ */
+public class VBeam {
+    private final static String TAG = "VBeam";
+
+    /**
+     * A callback to be invoked when Beam has been inititaed to another device.
+     */
+    public interface IntentCreator {
+        /**
+         * Create the intent to transfer to the destination phone.
+         * <p>
+         * This runs on the sender phone. The credentials for the receiver phone are available
+         * in {@code call}. For example you can use
+         * {@link io.v.v23.security.VSecurity#getRemoteBlessingNames(VContext, Call)} to find the
+         * receiver's blessings.
+         * <p>
+         * You should return an intent URI (as returned by {@link Intent#toUri(int)}), and
+         * optionally a byte array payload. This intent will be started on the receiver phone.
+         */
+        ListenableFuture<Pair<String, byte[]>> createIntent(VContext context, ServerCall call);
+    }
+
+    /**
+     * Set a callback that dynamically generates an Intent to send using Android Beam.
+     * <p>
+     * You should call this method during your Activity's onCreate(). You should cancel the context
+     * in your Activity's onDestroy(), but not before.
+     * <p>
+     * Do not mix calls to VBeam and NfcAdapter for the same activity.
+     */
+    public static boolean setBeamIntentCallback(VContext context,
+                                                Activity activity,
+                                                IntentCreator creator) throws VException {
+        NfcAdapter nfc = NfcAdapter.getDefaultAdapter(activity);
+        if (nfc == null)
+            return false;
+        if (creator == null) {
+            nfc.setNdefPushMessageCallback(null, activity);
+            return true;
+        }
+
+        final VBeamManager vBeamManager = new VBeamManager(context, activity, creator);
+        nfc.setNdefPushMessageCallback(vBeamManager, activity);
+        return true;
+    }
+
+    private VBeam() { }  // static
+
+    /**
+     * Returns the {@code byte[]} payload from an intent sent using VBeam.
+     * Call this on the receiving device to retrieve your payload.
+     */
+    public static byte[] getBeamPayload(Intent intent) {
+        return intent.getByteArrayExtra(VBeamManager.EXTRA_VBEAM_PAYLOAD);
+    }
+}
diff --git a/benchmarks/rpcbench/android/build.gradle b/benchmarks/rpcbench/android/build.gradle
index 120f1fb..11e6b95 100644
--- a/benchmarks/rpcbench/android/build.gradle
+++ b/benchmarks/rpcbench/android/build.gradle
@@ -6,7 +6,7 @@
     dependencies {
         classpath 'com.android.tools.build:gradle:1.3.0'
         classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0'
-        classpath 'io.v:gradle-plugin:1.3'
+        classpath 'io.v:gradle-plugin:1.4'
     }
 }
 
@@ -43,7 +43,7 @@
 }
 
 dependencies {
-    compile 'io.v:vanadium-android:1.0'
+    compile 'io.v:vanadium-android:1.6'
     compile 'com.android.support:appcompat-v7:23.1.0'
     androidTestCompile 'junit:junit:4.12'
 }
diff --git a/benchmarks/rpcbench/android/src/androidTest/java/io/v/rpcbench/AndroidRpcBenchmark.java b/benchmarks/rpcbench/android/src/androidTest/java/io/v/rpcbench/AndroidRpcBenchmark.java
index 2652baf..b77fa5f 100644
--- a/benchmarks/rpcbench/android/src/androidTest/java/io/v/rpcbench/AndroidRpcBenchmark.java
+++ b/benchmarks/rpcbench/android/src/androidTest/java/io/v/rpcbench/AndroidRpcBenchmark.java
@@ -4,22 +4,24 @@
 
 package io.v.rpcbench;
 
-import android.os.Debug;
 import android.test.AndroidTestCase;
 
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
 import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
 import java.util.Arrays;
-import java.util.Iterator;
 
 import io.v.android.v23.V;
+import io.v.v23.InputChannelCallback;
+import io.v.v23.InputChannels;
 import io.v.v23.VFutures;
 import io.v.v23.context.VContext;
 import io.v.v23.naming.Endpoint;
 import io.v.v23.rpc.ListenSpec;
-import io.v.v23.rpc.ReflectInvoker;
 import io.v.v23.rpc.Server;
 import io.v.v23.rpc.ServerCall;
 import io.v.v23.security.VSecurity;
@@ -39,23 +41,19 @@
         VContext serverContext = io.v.v23.V
                 .withNewServer(listenSpec, "", new EchoServer() {
                     @Override
-                    public byte[] echo(VContext ctx, ServerCall call, byte[] payload)
-                            throws VException {
-                        return payload;
+                    public ListenableFuture<byte[]> echo(VContext ctx, ServerCall call, byte[] payload) {
+                        return Futures.immediateFuture(payload);
                     }
 
                     @Override
-                    public void echoStream(VContext ctx, ServerCall call,
-                                           ServerStream<byte[], byte[]> stream)
-                            throws VException {
-                        Iterator<byte[]> byteIterator = stream.iterator();
-                        while (byteIterator.hasNext()) {
-                            byte[] payload = byteIterator.next();
-                            stream.send(payload);
-                        }
-                        if (stream.error() != null) {
-                            throw stream.error();
-                        }
+                    public ListenableFuture<Void> echoStream(VContext ctx, ServerCall call,
+                                                             final ServerStream<byte[], byte[]> stream) {
+                        return InputChannels.withCallback(stream, new InputChannelCallback<byte[]>() {
+                            @Override
+                            public ListenableFuture<Void> onNext(byte[] result) {
+                                return stream.send(result);
+                            }
+                        });
                     }
                 }, VSecurity.newAllowEveryoneAuthorizer());
         Server echoServer = V.getServer(serverContext);
@@ -75,11 +73,9 @@
             VFutures.sync(echoClient.echo(baseContext, payload));
         }
         for (int i = 0; i < 1000; i++) {
-            if (i == 999) { Debug.startMethodTracing(); }
             long start = System.nanoTime();
             VFutures.sync(echoClient.echo(baseContext, payload));
             long end = System.nanoTime();
-            if (i == 999) { Debug.stopMethodTracing(); }
             long duration = end - start;
             if (i == 0) {
                 cma = duration;
@@ -91,7 +87,7 @@
     }
 
     public void testSingleEncoder() throws IOException, ConversionException {
-        int nreps = 1;
+        int nreps = 1000;
         PipedOutputStream outStream = new PipedOutputStream();
         PipedInputStream inStream = new PipedInputStream(outStream);
         BinaryEncoder encoder = new BinaryEncoder(outStream);
@@ -120,7 +116,7 @@
     }
 
     public void testNewEncoderEachTime() throws VException {
-        int nreps = 1;
+        int nreps = 1000;
         byte[] payload = new byte[] { 0x01 };
         double cma = 0;
         for (int i = 0; i < nreps; i++) {
diff --git a/benchmarks/rpcbench/build.gradle b/benchmarks/rpcbench/build.gradle
index 939196d..3048e68 100644
--- a/benchmarks/rpcbench/build.gradle
+++ b/benchmarks/rpcbench/build.gradle
@@ -26,6 +26,10 @@
     compile 'com.google.caliper:caliper:1.0-beta-2'
 }
 
+static isDarwin() {
+    return System.properties['os.name'].toLowerCase().contains("os x")
+}
+
 task wrapper(type: Wrapper) {
     gradleVersion = '2.7'
 }
@@ -42,3 +46,9 @@
 
 tasks.run.dependsOn libProject.tasks.copyVanadiumLib
 tasks.run.jvmArgs "-Djava.library.path=${libProject.buildDir}/libs"
+tasks.run.jvmArgs "-XX:-TieredCompilation"  // needed for caliper
+if (isDarwin()) {
+    // See: https://github.com/vanadium/issues/issues/567
+    tasks.run.jvmArgs "-XX:+UnlockDiagnosticVMOptions"
+    tasks.run.jvmArgs "-XX:-LogEvents"
+}
diff --git a/benchmarks/syncbench/build.gradle b/benchmarks/syncbench/build.gradle
index 1b5f34c..3d56f09 100644
--- a/benchmarks/syncbench/build.gradle
+++ b/benchmarks/syncbench/build.gradle
@@ -13,6 +13,10 @@
     compile 'com.google.caliper:caliper:1.0-beta-2'
 }
 
+static isDarwin() {
+    return System.properties['os.name'].toLowerCase().contains("os x")
+}
+
 task copyLib(type: Copy) {
     from([project(':lib').buildDir.getAbsolutePath(), 'libs', 'libv23.so'].join(File.separator))
     destinationDir = new File(['src', 'main', 'resources'].join(File.separator))
@@ -27,3 +31,14 @@
 clean {
     delete 'src/main/resources/libv23.so'
 }
+
+def libProject = rootProject.findProject(':lib')
+
+tasks.run.dependsOn libProject.tasks.copyVanadiumLib
+tasks.run.jvmArgs "-Djava.library.path=${libProject.buildDir}/libs"
+tasks.run.jvmArgs "-XX:-TieredCompilation"  // needed for caliper
+if (isDarwin()) {
+    // See: https://github.com/vanadium/issues/issues/567
+    tasks.run.jvmArgs "-XX:+UnlockDiagnosticVMOptions"
+    tasks.run.jvmArgs "-XX:-LogEvents"
+}
\ No newline at end of file
diff --git a/benchmarks/syncbench/src/main/java/io/v/syncbench/SyncbaseBenchmark.java b/benchmarks/syncbench/src/main/java/io/v/syncbench/SyncbaseBenchmark.java
index 2095c3e..11978b2 100644
--- a/benchmarks/syncbench/src/main/java/io/v/syncbench/SyncbaseBenchmark.java
+++ b/benchmarks/syncbench/src/main/java/io/v/syncbench/SyncbaseBenchmark.java
@@ -79,7 +79,7 @@
 
     @AfterExperiment
     public void tearDown() throws VException {
-        syncbaseServer.stop();
+        baseContext.cancel();
     }
 
     @Benchmark
diff --git a/lib/.gitignore b/lib/.gitignore
index 4cf7f3a..ebdc8b5 100644
--- a/lib/.gitignore
+++ b/lib/.gitignore
@@ -2,9 +2,6 @@
 /bin/
 /vdl_bin/
 
-# Vanadium
-/.v23
-
 # Gradle
 .gradle/
 build/
diff --git a/lib/build.gradle b/lib/build.gradle
index 72d3cb4..daa8186 100644
--- a/lib/build.gradle
+++ b/lib/build.gradle
@@ -23,7 +23,7 @@
 
 // You should change this after releasing a new version of the library. See the
 // list of published versions at https://repo1.maven.org/maven2/io/v/vanadium.
-def releaseVersion = '1.5'
+def releaseVersion = '1.7'
 
 dependencies {
     compile group: 'joda-time', name: 'joda-time', version: '2.7'
diff --git a/lib/src/main/java/io/v/v23/rpc/ReflectInvoker.java b/lib/src/main/java/io/v/v23/rpc/ReflectInvoker.java
index 7a05351..a8e802d 100644
--- a/lib/src/main/java/io/v/v23/rpc/ReflectInvoker.java
+++ b/lib/src/main/java/io/v/v23/rpc/ReflectInvoker.java
@@ -282,7 +282,7 @@
                     } catch (InvocationTargetException | IllegalAccessException e) {
                         ret.setException(new VException(String.format(
                                 "Error invoking method %s: %s",
-                                method, e.getCause().getMessage())));
+                                method, e.getCause().toString())));
                     }
                 }
             });
diff --git a/lib/src/main/java/io/v/v23/syncbase/nosql/Database.java b/lib/src/main/java/io/v/v23/syncbase/nosql/Database.java
index 9367729..fe2d0f3 100644
--- a/lib/src/main/java/io/v/v23/syncbase/nosql/Database.java
+++ b/lib/src/main/java/io/v/v23/syncbase/nosql/Database.java
@@ -210,9 +210,7 @@
 
     /**
      * Compares the current schema version of the database with the schema version provided while
-     * creating this database handle.  If the current database schema version is lower, then the
-     * {@link SchemaUpgrader} associated with the schema is called. If {@link SchemaUpgrader} is
-     * successful, this method stores the new schema metadata in database.
+     * creating this database handle and updates the schema metadata if required.
      * <p>
      * This method also registers a conflict resolver with syncbase to receive conflicts.
      * <p>
diff --git a/lib/src/main/java/io/v/v23/syncbase/nosql/Schema.java b/lib/src/main/java/io/v/v23/syncbase/nosql/Schema.java
index 8d007f6..0a6da8e 100644
--- a/lib/src/main/java/io/v/v23/syncbase/nosql/Schema.java
+++ b/lib/src/main/java/io/v/v23/syncbase/nosql/Schema.java
@@ -12,23 +12,20 @@
  * Each database has a schema associated with it which defines the current version of the
  * database.  When a new version of an app wishes to change its data in a way that it is not
  * compatible with the old app's data, this app must change the schema version and provide relevant
- * upgrade logic in the specified {@link SchemaUpgrader}. The conflict resolution rules are also
- * associated with the schema version. Hence if the conflict resolution rules change then the schema
- * version also must be bumped.
+ * upgrade logic. The conflict resolution rules are also associated with the schema version. Hence
+ * if the conflict resolution rules change then the schema version also must be bumped.
  */
 public class Schema {
     private final SchemaMetadata metadata;
-    private final SchemaUpgrader upgrader;
     private final ConflictResolver resolver;
 
     /**
-     * Creates a new database schema with the specified metadata and schema upgrader.
+     * Creates a new database schema with the specified metadata and conflict resolver.
      * <p>
-     * Note: {@link SchemaUpgrader} is purely local and is not persisted.
+     * Note: {@link ConflictResolver} is purely local and is not persisted.
      */
-    public Schema(SchemaMetadata metadata, SchemaUpgrader upgrader, ConflictResolver resolver) {
+    public Schema(SchemaMetadata metadata, ConflictResolver resolver) {
         this.metadata = metadata;
-        this.upgrader = upgrader;
         this.resolver = resolver;
     }
 
@@ -38,12 +35,6 @@
     public SchemaMetadata getMetadata() { return this.metadata; }
 
     /**
-     * Returns the upgrade logic used for upgrading the schema when an app's schema version differs
-     * from the database's schema version.
-     */
-    public SchemaUpgrader getUpgrader() { return this.upgrader; }
-
-    /**
      * Returns a resolver that is used for conflict resolution.
      */
     public ConflictResolver getResolver() { return this.resolver; }
diff --git a/lib/src/main/java/io/v/v23/syncbase/nosql/SchemaUpgrader.java b/lib/src/main/java/io/v/v23/syncbase/nosql/SchemaUpgrader.java
deleted file mode 100644
index 2d113eb..0000000
--- a/lib/src/main/java/io/v/v23/syncbase/nosql/SchemaUpgrader.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// 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.syncbase.nosql;
-
-import io.v.v23.verror.VException;
-
-/**
- * An interface that must be implemented by an app in order to upgrade the database schema from
- * a lower version to a higher version.
- */
-public interface SchemaUpgrader {
-    /**
-     * Updgrades database from an old to the new schema version.
-     * <p>
-     * This method must be idempotent.
-     *
-     * @param  db         database to be upgraded
-     * @param  oldVersion old schema version
-     * @param  newVersion new schema version
-     * @throws VException if the database couldn't be upgraded
-     */
-    void run(Database db, int oldVersion, int newVersion) throws VException;
-}
\ No newline at end of file
diff --git a/projects/moments/app/build.gradle b/projects/moments/app/build.gradle
index c87af77..9637052 100644
--- a/projects/moments/app/build.gradle
+++ b/projects/moments/app/build.gradle
@@ -1,4 +1,3 @@
-
 buildscript {
     repositories {
         jcenter()
@@ -34,6 +33,8 @@
         targetSdkVersion 23
         versionCode 1
         versionName "1.0"
+        // Enabling multidex support.
+        multiDexEnabled true
     }
     buildTypes {
         release {
@@ -50,7 +51,8 @@
     compile 'com.android.support:appcompat-v7:23.1.1'
     compile 'com.android.support:design:23.1.1'
     compile 'com.android.support:support-v4:23.1.1'
-    compile 'io.v:vanadium-android:1.6'
+    compile 'com.android.support:multidex:1.0.0'
+    compile 'io.v:vanadium-android:1.7'
 }
 
 vdl {
diff --git a/projects/moments/app/src/main/AndroidManifest.xml b/projects/moments/app/src/main/AndroidManifest.xml
index 27cf748..1b19978 100644
--- a/projects/moments/app/src/main/AndroidManifest.xml
+++ b/projects/moments/app/src/main/AndroidManifest.xml
@@ -23,6 +23,7 @@
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
+        android:name="android.support.multidex.MultiDexApplication"
         android:supportsRtl="true"
         android:theme="@style/AppTheme">
         <activity
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/AdConverter.java b/projects/moments/app/src/main/java/io/v/moments/ifc/AdConverter.java
deleted file mode 100644
index 13dcecf..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/AdConverter.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// 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.moments.ifc;
-
-import io.v.v23.discovery.Service;
-
-/**
- * The io.v.v23.discovery.Service isn't a service, it's the *description* of a
- * service found in a discovery advertisement.
- *
- * Implementations of this interface construct an instance of T from the
- * attributes in Service, and/or by making an RPC to the real underlying service
- * described by the advertisement to get data needed to make a T.
- */
-public interface AdConverter<T> {
-    T make(Service service);
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/Advertiser.java b/projects/moments/app/src/main/java/io/v/moments/ifc/Advertiser.java
deleted file mode 100644
index f3e3f26..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/Advertiser.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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.moments.ifc;
-
-import com.google.common.util.concurrent.FutureCallback;
-
-/**
- * Advertiser controls, intended to be similar to Scanner controls.
- */
-public interface Advertiser {
-    /**
-     * Asynchronously start advertising.
-     *
-     * Callbacks can be expected to run on the UX thread.
-     *
-     * @param startupCallback    executed on success or failure of advertising
-     *                           startup.
-     * @param completionCallback executed on success or failure of advertising
-     *                           completion.  An advertisement might shutdown
-     *                           for reasons other than a call to stop, e.g. a
-     *                           timeout.
-     */
-    void start(FutureCallback<Void> startupCallback,
-               FutureCallback<Void> completionCallback);
-
-    /**
-     * True if stop could usefully be called.
-     */
-    boolean isAdvertising();
-
-    /**
-     * Synchronously stop advertising.  Should result in execution of
-     * completionCallback.
-     */
-    void stop();
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/Moment.java b/projects/moments/app/src/main/java/io/v/moments/ifc/Moment.java
index bf0fba1..74a5fb1 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/Moment.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ifc/Moment.java
@@ -11,12 +11,13 @@
 import org.joda.time.format.DateTimeFormatter;
 
 import io.v.moments.lib.Id;
-import io.v.v23.discovery.Attributes;
 
 /**
  * A photo with ancillary information.
  */
 public interface Moment extends HasId {
+    DateTimeFormatter FMT = DateTimeFormat.forPattern("yyyyMMdd_HHmmss");
+
     /**
      * A unique moment ID valid for the life of the app.
      */
@@ -55,8 +56,8 @@
     /**
      * An advertiser can be scheduled to start advertising, but not actually be
      * advertising yet.  There's also a lag to stop advertising.  This member
-     * tracks the desired eventual state, to maintain sensible UX, through
-     * phone rotations and whatnot.
+     * tracks the desired eventual state, to maintain sensible UX, through phone
+     * rotations and whatnot.
      */
     AdState getDesiredAdState();
 
@@ -70,12 +71,6 @@
      */
     boolean hasPhoto(Kind kind, Style style);
 
-
-    /**
-     * From this, make a set of discovery 'attributes'.
-     */
-    Attributes makeAttributes();
-
     /**
      * Get the specified photo.
      */
@@ -104,7 +99,5 @@
         HUGE, FULL, THUMB
     }
 
-    enum AdState { ON, OFF }
-
-    DateTimeFormatter FMT = DateTimeFormat.forPattern("yyyyMMdd_HHmmss");
+    enum AdState {ON, OFF}
 }
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/MomentFactory.java b/projects/moments/app/src/main/java/io/v/moments/ifc/MomentFactory.java
index 2b9dd01..6315030 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/MomentFactory.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ifc/MomentFactory.java
@@ -14,17 +14,19 @@
  * Makes moments, converts them to and from other formats.
  */
 public interface MomentFactory {
+    Moment make(Id id, int index, String author, String caption);
+
     void toPrefs(SharedPreferences.Editor editor, String prefix, Moment m);
 
+    Moment fromPrefs(SharedPreferences p, String prefix);
+
     void toBundle(Bundle b, String prefix, Moment m);
 
     Moment fromBundle(Bundle b, String prefix);
 
-    Moment make(Id id, int index, String author, String caption);
+    Attributes toAttributes(Moment moment);
 
-    Moment makeFromAttributes(Id id, int ordinal, Attributes attr);
-
-    Moment fromPrefs(SharedPreferences p, String prefix);
+    Moment fromAttributes(Id id, int ordinal, Attributes attr);
 
     enum F {
         DATE, AUTHOR, CAPTION, ORDINAL, ID, ADVERTISING
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/ScanListener.java b/projects/moments/app/src/main/java/io/v/moments/ifc/ScanListener.java
deleted file mode 100644
index e7eed01..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/ScanListener.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// 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.moments.ifc;
-
-import io.v.v23.discovery.Update;
-
-/**
- * Implementations of this interface can be registered with
- * {@link io.v.moments.lib.V23Manager#scan} to receive notifications when discovery events occur.
- */
-public interface ScanListener {
-    void scanUpdateReceived(Update result);
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/Scanner.java b/projects/moments/app/src/main/java/io/v/moments/ifc/Scanner.java
deleted file mode 100644
index b9084d9..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/Scanner.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// 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.moments.ifc;
-
-import com.google.common.util.concurrent.FutureCallback;
-
-import io.v.v23.InputChannelCallback;
-import io.v.v23.discovery.Update;
-
-/**
- * Scanner controls, intended to be similar to Advertiser controls.
- */
-public interface Scanner {
-    /**
-     * Asynchronously start scanning.
-     *
-     * Callbacks can be expected to run on the UX thread.
-     *
-     * @param startupCallback    executed on success or failure of scan
-     *                           startup.
-     * @param updateCallback     executed on each scan update (each found or
-     *                           lost advertisement).
-     * @param completionCallback executed on success or failure of scan
-     *                           completion.  A scan might shutdown for reasons
-     *                           other than a call to stop, e.g. a timeout.
-     */
-    void start(FutureCallback<Void> startupCallback,
-               InputChannelCallback<Update> updateCallback,
-               FutureCallback<Void> completionCallback);
-
-    /**
-     * True if stop could usefully be called.
-     */
-    boolean isScanning();
-
-    /**
-     * Synchronously stop scanning.  Should result in execution of
-     * completionCallback.
-     */
-    void stop();
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/package-info.java b/projects/moments/app/src/main/java/io/v/moments/ifc/package-info.java
index f7e96e4..6e57bcc 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/package-info.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ifc/package-info.java
@@ -3,6 +3,6 @@
 // license that can be found in the LICENSE file.
 
 /**
- * Iterfaces used by the moments app.
+ * Interfaces used by the moments app.
  */
 package io.v.moments.ifc;
diff --git a/projects/moments/app/src/main/java/io/v/moments/lib/DiscoveredList.java b/projects/moments/app/src/main/java/io/v/moments/lib/DiscoveredList.java
index c4cd093..dbc3f22 100644
--- a/projects/moments/app/src/main/java/io/v/moments/lib/DiscoveredList.java
+++ b/projects/moments/app/src/main/java/io/v/moments/lib/DiscoveredList.java
@@ -6,17 +6,19 @@
 
 import android.os.Handler;
 
-import io.v.moments.ifc.AdConverter;
 import io.v.moments.ifc.HasId;
 import io.v.moments.ifc.IdSet;
-import io.v.moments.ifc.ScanListener;
+import io.v.moments.v23.ifc.AdConverter;
+import io.v.moments.v23.ifc.AdvertisementFoundListener;
+import io.v.moments.v23.ifc.AdvertisementLostListener;
 import io.v.v23.discovery.Service;
-import io.v.v23.discovery.Update;
 
 /**
  * List that updates itself in response to found or lost advertisements.
  */
-public class DiscoveredList<T extends HasId> extends ObservedList<T> implements ScanListener {
+public class DiscoveredList<T extends HasId>
+        extends ObservedList<T>
+        implements AdvertisementFoundListener, AdvertisementLostListener {
     private static final String TAG = "DiscoveredList";
 
     private final Handler mHandler;
@@ -43,32 +45,19 @@
         mHandler = handler;
     }
 
-    @Override
-    public void scanUpdateReceived(Update update) {
-        if (update instanceof Update.Found) {
-            maybeInsertItem((Update.Found) update);
-            return;
-        }
-        removeItem((Update.Lost) update);
-    }
-
     /**
      * Accept the advertisement if it's not on the reject list.
      */
-    private void maybeInsertItem(Update.Found found) {
-        Service service = found.getElem().getService();
-        final Id id = Id.fromString(service.getInstanceId());
+    @Override
+    public void handleFoundAdvertisement(Service advertisement) {
+        final Id id = Id.fromString(advertisement.getInstanceId());
         if (mRejects.contains(id)) {
             return;
         }
-        final T item = mConverter.make(service);
+        final T item = mConverter.make(advertisement);
         if (item == null) {
             return;
         }
-        insertItem(id, item);
-    }
-
-    private void insertItem(final Id id, final T item) {
         mHandler.post(new Runnable() {
             @Override
             public void run() {
@@ -77,8 +66,12 @@
         });
     }
 
-    private void removeItem(Update.Lost lost) {
-        final Id id = Id.fromString(lost.getElem().getService().getInstanceId());
+    /**
+     * Remove the lost advertisement.
+     */
+    @Override
+    public void handleLostAdvertisement(Service advertisement) {
+        final Id id = Id.fromString(advertisement.getInstanceId());
         mHandler.post(new Runnable() {
             @Override
             public void run() {
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/FileUtil.java b/projects/moments/app/src/main/java/io/v/moments/lib/FileUtil.java
similarity index 98%
rename from projects/moments/app/src/main/java/io/v/moments/model/FileUtil.java
rename to projects/moments/app/src/main/java/io/v/moments/lib/FileUtil.java
index 4bb073b..b2b6d2a 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/FileUtil.java
+++ b/projects/moments/app/src/main/java/io/v/moments/lib/FileUtil.java
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package io.v.moments.model;
+package io.v.moments.lib;
 
 import java.io.File;
 
diff --git a/projects/moments/app/src/main/java/io/v/moments/lib/V23Manager.java b/projects/moments/app/src/main/java/io/v/moments/lib/V23Manager.java
deleted file mode 100644
index d627698..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/lib/V23Manager.java
+++ /dev/null
@@ -1,183 +0,0 @@
-// 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.moments.lib;
-
-import android.app.Activity;
-import android.content.Context;
-import android.util.Log;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import org.joda.time.Duration;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import io.v.android.libs.security.BlessingsManager;
-import io.v.android.v23.V;
-import io.v.v23.InputChannelCallback;
-import io.v.v23.InputChannels;
-import io.v.v23.context.VContext;
-import io.v.v23.discovery.Service;
-import io.v.v23.discovery.Update;
-import io.v.v23.discovery.VDiscovery;
-import io.v.v23.rpc.Server;
-import io.v.v23.security.BlessingPattern;
-import io.v.v23.security.Blessings;
-import io.v.v23.security.VSecurity;
-import io.v.v23.verror.VException;
-
-/**
- * Various static V23 utilities gathered into an instantiatable class.
- *
- * This allows v23 usage to be injected and mocked.  The class is a singleton to
- * avoid confusion about init, shutdown and the 'base context'.
- */
-public class V23Manager {
-    private static final String TAG = "V23Manager";
-    private static final String BLESSINGS_KEY = "BlessingsKey";
-    private static final List<BlessingPattern> NO_PATTERNS = new ArrayList<>();
-    private Context mAndroidCtx;
-    private VContext mV23Ctx = null;
-    private VDiscovery mDiscovery = null;
-
-    // Singleton.
-    private V23Manager() {
-    }
-
-    public void scan(
-            String query,
-            Duration duration,
-            FutureCallback<VContext> startupCallback,
-            InputChannelCallback<Update> updateCallback,
-            FutureCallback<Void> completionCallback) {
-        Log.d(TAG, "Starting scan with q=[" + query + "]");
-        if (mDiscovery == null) {
-            startupCallback.onFailure(
-                    new IllegalStateException("Discovery not ready."));
-            return;
-        }
-        VContext context = contextWithTimeout(duration);
-        Futures.addCallback(
-                InputChannels.withCallback(
-                        mDiscovery.scan(context, query), updateCallback),
-                completionCallback);
-        startupCallback.onSuccess(context);
-    }
-
-    public void advertise(
-            Service advertisement,
-            Duration duration,
-            FutureCallback<VContext> startupCallback,
-            FutureCallback<Void> completionCallback) {
-        if (mDiscovery == null) {
-            startupCallback.onFailure(
-                    new IllegalStateException("Discovery not ready."));
-            return;
-        }
-        VContext context = contextWithTimeout(duration);
-        ListenableFuture<ListenableFuture<Void>> hey =
-                mDiscovery.advertise(context, advertisement, NO_PATTERNS);
-        Futures.addCallback(hey, makeWrapped(context, startupCallback, completionCallback));
-    }
-
-    /**
-     * This exists to allow the scan and advertise interfaces to behave the same
-     * way to clients, and to deliver the context via the "success" callback
-     * so there's no chance of a race condition between usage of that context
-     * and code in the callback.
-     */
-    private FutureCallback<ListenableFuture<Void>> makeWrapped(
-            final VContext context,
-            final FutureCallback<VContext> startup,
-            final FutureCallback<Void> completion) {
-        return new FutureCallback<ListenableFuture<Void>>() {
-            @Override
-            public void onSuccess(ListenableFuture<Void> result) {
-                startup.onSuccess(context);
-                Futures.addCallback(result, completion);
-            }
-
-            @Override
-            public void onFailure(final Throwable t) {
-                startup.onFailure(t);
-            }
-        };
-    }
-
-
-    public synchronized void init(
-            Activity activity, FutureCallback<Blessings> blessingCallback) {
-        Log.d(TAG, "init");
-        if (mAndroidCtx != null) {
-            if (mAndroidCtx == activity.getApplicationContext()) {
-                Log.d(TAG, "Initialization already started.");
-                return;
-            } else {
-                Log.d(TAG, "Initialization with new context.");
-                shutdown();
-            }
-        }
-        mAndroidCtx = activity.getApplicationContext();
-        // Must call V.init before attempting to load blessings, so that proper
-        // code is loaded.
-        mV23Ctx = V.init(mAndroidCtx);
-        Log.d(TAG, "Attempting to get blessings.");
-        ListenableFuture<Blessings> f = BlessingsManager.getBlessings(
-                mV23Ctx, activity, BLESSINGS_KEY, true);
-        Futures.addCallback(f, blessingCallback);
-        try {
-            mDiscovery = V.newDiscovery(mV23Ctx);
-        } catch (VException e) {
-            Log.d(TAG, "Unable to get discovery object.", e);
-        }
-    }
-
-    public void shutdown() {
-        Log.d(TAG, "Shutdown");
-        if (mAndroidCtx == null) {
-            Log.d(TAG, "Was never initialized.");
-            return;
-        }
-        mV23Ctx.cancel();
-        mAndroidCtx = null;
-    }
-
-    public VContext makeServerContext(
-            String mountName, Object server) throws VException {
-        return V.withNewServer(
-                mV23Ctx.withCancel(),
-                mountName,
-                server,
-                VSecurity.newAllowEveryoneAuthorizer());
-    }
-
-    public Server getServer(VContext mServerCtx) {
-        return V.getServer(mServerCtx);
-    }
-
-    public VContext contextWithTimeout(Duration timeout) {
-        return mV23Ctx.withTimeout(timeout);
-    }
-
-    public static class Singleton {
-        private static volatile V23Manager instance;
-
-        public static V23Manager get() {
-            V23Manager result = instance;
-            if (instance == null) {
-                synchronized (Singleton.class) {
-                    result = instance;
-                    if (result == null) {
-                        instance = result = new V23Manager();
-                    }
-                }
-            }
-            return result;
-        }
-    }
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/AdConverterMoment.java b/projects/moments/app/src/main/java/io/v/moments/model/AdConverterMoment.java
index b07a8d3..3387786 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/AdConverterMoment.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/AdConverterMoment.java
@@ -15,7 +15,6 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
-import io.v.moments.ifc.AdConverter;
 import io.v.moments.ifc.Moment;
 import io.v.moments.ifc.Moment.Kind;
 import io.v.moments.ifc.Moment.Style;
@@ -23,7 +22,8 @@
 import io.v.moments.ifc.MomentFactory;
 import io.v.moments.lib.Id;
 import io.v.moments.lib.ObservedList;
-import io.v.moments.lib.V23Manager;
+import io.v.moments.v23.ifc.AdConverter;
+import io.v.moments.v23.ifc.V23Manager;
 import io.v.v23.discovery.Service;
 
 /**
@@ -76,7 +76,7 @@
         if (mRemoteMomentCache.containsKey(id)) {
             return mRemoteMomentCache.get(id);
         }
-        final Moment moment = mMomentFactory.makeFromAttributes(
+        final Moment moment = mMomentFactory.fromAttributes(
                 id, nextOrdinal(), descriptor.getAttrs());
         mRemoteMomentCache.put(id, moment);
 
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserFactory.java b/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserFactory.java
index 522ce76..9a3a98b 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserFactory.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserFactory.java
@@ -7,30 +7,42 @@
 import java.util.HashMap;
 import java.util.Map;
 
-import io.v.moments.ifc.Advertiser;
 import io.v.moments.ifc.IdSet;
 import io.v.moments.ifc.Moment;
+import io.v.moments.ifc.MomentFactory;
 import io.v.moments.lib.Id;
-import io.v.moments.lib.V23Manager;
+import io.v.moments.v23.ifc.Advertiser;
+import io.v.moments.v23.ifc.V23Manager;
 
 /**
- * Makes advertisers.  Keeps a record of all of them for the life of the app.
- * Can use this record to reject local advertisements when scanning, or to shut
- * down all advertising.
+ * Makes moment advertisers.
+ *
+ * Keeps a record of all of them for the life of the app. This record used to
+ * reject locally created advertisements when scanning, or to shut down all
+ * advertising.
  */
 public class AdvertiserFactory implements IdSet {
     private final V23Manager mV23Manager;
     private final Map<Id, Advertiser> mLocalAds = new HashMap<>();
+    private final MomentFactory mFactory;
 
-    public AdvertiserFactory(V23Manager v23Manager) {
+    public AdvertiserFactory(V23Manager v23Manager, MomentFactory factory) {
+        if (v23Manager == null) {
+            throw new IllegalArgumentException("Null v23Manager");
+        }
+        if (factory == null) {
+            throw new IllegalArgumentException("Null factory");
+        }
         mV23Manager = v23Manager;
+        mFactory = factory;
     }
 
     public Advertiser getOrMake(Moment moment) {
         if (contains(moment.getId())) {
             return mLocalAds.get(moment.getId());
         }
-        Advertiser result = new AdvertiserImpl(mV23Manager, moment);
+        Advertiser result = mV23Manager.makeAdvertiser(
+                new MomentAdCampaign(moment, mFactory));
         mLocalAds.put(moment.getId(), result);
         return result;
     }
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserImpl.java b/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserImpl.java
deleted file mode 100644
index ee08e5b..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserImpl.java
+++ /dev/null
@@ -1,223 +0,0 @@
-// 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.moments.model;
-
-import android.graphics.Bitmap;
-import android.support.annotation.NonNull;
-import android.util.Log;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.io.ByteArrayOutputStream;
-import java.util.ArrayList;
-import java.util.List;
-
-import io.v.moments.ifc.Advertiser;
-import io.v.moments.ifc.Moment;
-import io.v.moments.ifc.Moment.Kind;
-import io.v.moments.ifc.Moment.Style;
-import io.v.moments.lib.V23Manager;
-import io.v.v23.context.VContext;
-import io.v.v23.discovery.Attachments;
-import io.v.v23.discovery.Attributes;
-import io.v.v23.discovery.Service;
-import io.v.v23.naming.Endpoint;
-import io.v.v23.rpc.ServerCall;
-import io.v.v23.verror.VException;
-
-/**
- * Handles the advertising of a moment.
- *
- * Main role of class is to manage the advertising context and moment service
- * lifetimes.  This code too complex to leak into the UX code, and too moment
- * specific to be part of v23Manager. This class should have no knowledge of UX,
- * and UX should have no access to VContext, because said context is under
- * active development, and it's too complex to cover possible interactions with
- * tests.  This approaches hides it all behind the simple Advertiser interface.
- */
-public class AdvertiserImpl implements Advertiser {
-    static final String NO_MOUNT_NAME = "";
-    private static final String TAG = "AdvertiserImpl";
-
-    private final V23Manager mV23Manager;
-    private final Moment mMoment;
-
-    private VContext mAdvCtx;
-    private VContext mServerCtx;
-
-    public AdvertiserImpl(V23Manager v23Manager, Moment moment) {
-        if (v23Manager == null) {
-            throw new IllegalArgumentException("Null v23Manager");
-        }
-        if (moment == null) {
-            throw new IllegalArgumentException("Null moment");
-        }
-        mV23Manager = v23Manager;
-        mMoment = moment;
-    }
-
-    @Override
-    public String toString() {
-        return mMoment.getCaption();
-    }
-
-    @Override
-    public void start(
-            FutureCallback<Void> startupCallback,
-            FutureCallback<Void> completionCallback) {
-        Log.d(TAG, "Entering start.");
-        if (isAdvertising()) {
-            startupCallback.onFailure(
-                    new IllegalStateException("Already advertising."));
-            return;
-        }
-        try {
-            mServerCtx = mV23Manager.makeServerContext(
-                    NO_MOUNT_NAME, new MomentServer());
-        } catch (VException e) {
-            mServerCtx = null;
-            startupCallback.onFailure(
-                    new IllegalStateException("Unable to start service.", e));
-            return;
-        }
-        mV23Manager.advertise(
-                makeAdvertisement(
-                        mMoment.makeAttributes(), makeServerAddressList()),
-                Config.Discovery.DURATION,
-                makeWrapped(startupCallback), completionCallback);
-        Log.d(TAG, "Exiting start.");
-    }
-
-    @Override
-    public boolean isAdvertising() {
-        return mAdvCtx != null && !mAdvCtx.isCanceled();
-    }
-
-    @NonNull
-    private List<String> makeServerAddressList() {
-        List<String> addresses = new ArrayList<>();
-        Endpoint[] points = mV23Manager.getServer(
-                mServerCtx).getStatus().getEndpoints();
-        for (Endpoint point : points) {
-            addresses.add(point.toString());
-        }
-        return addresses;
-    }
-
-    /**
-     * A service must be started before advertising begins, which creates a
-     * cleanup problem should advertising fail to start. This callback accepts
-     * the advertising context on startup success, and cancels the already
-     * started service on failure. This wrapper necessary to cleanly kill the
-     * service should advertising fail to start.  This callback feeds info to
-     * the startup callback, which presumably has some effect on UX.
-     */
-    private FutureCallback<VContext> makeWrapped(
-            final FutureCallback<Void> startup) {
-        return new FutureCallback<VContext>() {
-            @Override
-            public void onSuccess(VContext context) {
-                mAdvCtx = context;
-                startup.onSuccess(null);
-            }
-
-            @Override
-            public void onFailure(final Throwable t) {
-                mAdvCtx = null;
-                cancelService();
-                startup.onFailure(t);
-            }
-        };
-    }
-
-    @Override
-    public void stop() {
-        Log.d(TAG, "Entering stop");
-        if (mAdvCtx != null) {
-            Log.d(TAG, "Cancelling advertising.");
-            if (!mAdvCtx.isCanceled()) {
-                mAdvCtx.cancel();
-            }
-            mAdvCtx = null;
-        }
-        cancelService();
-        Log.d(TAG, "Exiting stop");
-    }
-
-    private void cancelService() {
-        if (mServerCtx != null) {
-            Log.d(TAG, "Cancelling service.");
-            if (!mServerCtx.isCanceled()) {
-                mServerCtx.cancel();
-            }
-            mServerCtx = null;
-        }
-    }
-
-    /**
-     * Makes an instance of 'Service', which is actually a service description,
-     * i.e. an advertisement.
-     */
-    private Service makeAdvertisement(Attributes attrs,
-                                      List<String> addresses) {
-        return new Service(
-                mMoment.getId().toString(),
-                mMoment.toString(),
-                Config.Discovery.INTERFACE_NAME,
-                attrs,
-                addresses,
-                new Attachments());
-    }
-
-    /**
-     * Serves moment data over RPC.
-     */
-    class MomentServer implements MomentIfcServer {
-        private static final String TAG = "MomentServer";
-        private byte[] mRawBytes = null;  // lazy init
-        private byte[] mThumbBytes = null;  // lazy init
-
-        public ListenableFuture<MomentWireData> getBasics(
-                VContext ctx, ServerCall call) {
-            MomentWireData data = new MomentWireData();
-            data.setAuthor(mMoment.getAuthor());
-            data.setCaption(mMoment.getCaption());
-            data.setCreationTime(mMoment.getCreationTime().getMillis());
-            return Futures.immediateFuture(data);
-        }
-
-        private byte[] makeBytes(Bitmap bitmap) {
-            ByteArrayOutputStream stream = new ByteArrayOutputStream();
-            bitmap.compress(Bitmap.CompressFormat.PNG, 60, stream);
-            return stream.toByteArray();
-        }
-
-        private synchronized byte[] getFullBytes() {
-            if (mRawBytes == null) {
-                mRawBytes = makeBytes(mMoment.getPhoto(Kind.LOCAL, Style.FULL));
-            }
-            return mRawBytes;
-        }
-
-        private synchronized byte[] getThumbBytes() {
-            if (mThumbBytes == null) {
-                mThumbBytes = makeBytes(mMoment.getPhoto(Kind.LOCAL, Style.THUMB));
-            }
-            return mThumbBytes;
-        }
-
-        public ListenableFuture<byte[]> getThumbImage(
-                VContext ctx, ServerCall call) {
-            return Futures.immediateFuture(getThumbBytes());
-        }
-
-        public ListenableFuture<byte[]> getFullImage(VContext ctx, ServerCall call) {
-            return Futures.immediateFuture(getFullBytes());
-        }
-    }
-
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/BitMapper.java b/projects/moments/app/src/main/java/io/v/moments/model/BitMapper.java
index 131587f..24d9169 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/BitMapper.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/BitMapper.java
@@ -17,6 +17,7 @@
 import io.v.moments.ifc.Moment;
 import io.v.moments.ifc.Moment.Kind;
 import io.v.moments.ifc.Moment.Style;
+import io.v.moments.lib.FileUtil;
 import io.v.moments.lib.ObservedList;
 
 /**
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/Config.java b/projects/moments/app/src/main/java/io/v/moments/model/Config.java
index 8761a52..bbd530e 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/Config.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/Config.java
@@ -10,8 +10,6 @@
 import android.os.Environment;
 import android.os.Handler;
 
-import org.joda.time.Duration;
-
 import java.io.File;
 
 import io.v.moments.R;
@@ -60,24 +58,4 @@
                         R.dimen.moment_image_width)
         );
     }
-
-    /** Constants related to discovery. */
-    public static class Discovery {
-        /**
-         * Required type/interface name, probably a URL into a web-based
-         * ontology.  Necessary for querying.
-         */
-        public static final String INTERFACE_NAME = "v.io/x/ref.Moments";
-        /**
-         * To limit scans to see only this service.
-         */
-        public static final String QUERY = "v.InterfaceName=\"" + INTERFACE_NAME + "\"";
-
-        /**
-         * After this duration an advertisement or scan for an advertisement
-         * will automatically stop. Choice is arbitrary. A nice exercise would
-         * be to add this to a settings menu.
-         */
-        public static final Duration DURATION = Duration.standardMinutes(5);
-    }
 }
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/MomentAdCampaign.java b/projects/moments/app/src/main/java/io/v/moments/model/MomentAdCampaign.java
new file mode 100644
index 0000000..443a570
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/model/MomentAdCampaign.java
@@ -0,0 +1,153 @@
+// 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.moments.model;
+
+import android.graphics.Bitmap;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.moments.ifc.Moment;
+import io.v.moments.ifc.MomentFactory;
+import io.v.moments.v23.ifc.AdCampaign;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Attachments;
+import io.v.v23.discovery.Attributes;
+import io.v.v23.rpc.ServerCall;
+import io.v.v23.security.BlessingPattern;
+
+/**
+ * Makes objects that support the advertisement of a Moment.
+ */
+public class MomentAdCampaign implements AdCampaign {
+    /**
+     * Required type/interface name, probably a URL into a web-based ontology.
+     * Necessary for querying.
+     */
+    public static final String INTERFACE_NAME = "v.io/x/ref.Moments";
+    /**
+     * To limit scans to see only this service.
+     */
+    public static final String QUERY = "v.InterfaceName=\"" + INTERFACE_NAME + "\"";
+    /**
+     * Used for public advertisements (no limits on who can see them).
+     */
+    public static final List<BlessingPattern> NO_PATTERNS = new ArrayList<>();
+
+    private final Moment mMoment;
+    private final MomentFactory mFactory;
+
+    public MomentAdCampaign(Moment moment, MomentFactory factory) {
+        if (moment == null) {
+            throw new IllegalArgumentException("Null moment");
+        }
+        if (factory == null) {
+            throw new IllegalArgumentException("Null factory");
+        }
+        mMoment = moment;
+        mFactory = factory;
+    }
+
+    @Override
+    public String getInstanceId() {
+        return mMoment.getId().toString();
+    }
+
+    @Override
+    public String getInstanceName() {
+        return mMoment.toString();
+    }
+
+    @Override
+    public String getInterfaceName() {
+        return INTERFACE_NAME;
+    }
+
+    @Override
+    public Attributes getAttributes() {
+        return mFactory.toAttributes(mMoment);
+    }
+
+    /**
+     * No attachments (empty list).
+     */
+    @Override
+    public Attachments getAttachments() {
+        return new Attachments();
+    }
+
+    /**
+     * Empty string means make no attempt to mount a server in a mount table.
+     */
+    @Override
+    public String getMountName() {
+        return "";
+    }
+
+    @Override
+    public Object makeService() {
+        return new MomentServer();
+    }
+
+    /**
+     * A set of blessing patterns for whom this advertisement is meant; any
+     * entity not matching a pattern here won't see the advertisement.
+     */
+    @Override
+    public List<BlessingPattern> getVisibility() {
+        return NO_PATTERNS;
+    }
+
+    /**
+     * Serves moment data over RPC.
+     */
+    private class MomentServer implements MomentIfcServer {
+        private static final String TAG = "MomentServer";
+        private byte[] mRawBytes = null;  // lazy init
+        private byte[] mThumbBytes = null;  // lazy init
+
+        public ListenableFuture<MomentWireData> getBasics(
+                VContext ctx, ServerCall call) {
+            MomentWireData data = new MomentWireData();
+            data.setAuthor(mMoment.getAuthor());
+            data.setCaption(mMoment.getCaption());
+            data.setCreationTime(mMoment.getCreationTime().getMillis());
+            return Futures.immediateFuture(data);
+        }
+
+        private byte[] makeBytes(Bitmap bitmap) {
+            ByteArrayOutputStream stream = new ByteArrayOutputStream();
+            bitmap.compress(Bitmap.CompressFormat.PNG, 60, stream);
+            return stream.toByteArray();
+        }
+
+        private synchronized byte[] getFullBytes() {
+            if (mRawBytes == null) {
+                mRawBytes = makeBytes(mMoment.getPhoto(Moment.Kind.LOCAL, Moment.Style.FULL));
+            }
+            return mRawBytes;
+        }
+
+        private synchronized byte[] getThumbBytes() {
+            if (mThumbBytes == null) {
+                mThumbBytes = makeBytes(mMoment.getPhoto(Moment.Kind.LOCAL, Moment.Style.THUMB));
+            }
+            return mThumbBytes;
+        }
+
+        public ListenableFuture<byte[]> getThumbImage(
+                VContext ctx, ServerCall call) {
+            return Futures.immediateFuture(getThumbBytes());
+        }
+
+        public ListenableFuture<byte[]> getFullImage(VContext ctx, ServerCall call) {
+            return Futures.immediateFuture(getFullBytes());
+        }
+    }
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/MomentFactoryImpl.java b/projects/moments/app/src/main/java/io/v/moments/model/MomentFactoryImpl.java
index c87d576..f341775 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/MomentFactoryImpl.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/MomentFactoryImpl.java
@@ -34,7 +34,7 @@
     }
 
     @Override
-    public Moment makeFromAttributes(Id id, int ordinal, Attributes attr) {
+    public Moment fromAttributes(Id id, int ordinal, Attributes attr) {
         return new MomentImpl(
                 mBitMapper, id, ordinal,
                 attr.get(F.AUTHOR.toString()),
@@ -44,6 +44,16 @@
     }
 
     @Override
+    public Attributes toAttributes(Moment moment) {
+        Attributes attr = new Attributes();
+        attr.put(MomentFactory.F.AUTHOR.toString(), moment.getAuthor());
+        attr.put(MomentFactory.F.CAPTION.toString(), moment.getCaption());
+        attr.put(MomentFactory.F.DATE.toString(),
+                Moment.FMT.print(moment.getCreationTime()));
+        return attr;
+    }
+
+    @Override
     public void toBundle(Bundle b, String prefix, Moment m) {
         KeyMaker km = new KeyMaker(prefix);
         b.putString(km.get(F.ID), m.getId().toString());
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/MomentImpl.java b/projects/moments/app/src/main/java/io/v/moments/model/MomentImpl.java
index 96e9569..ed521cd 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/MomentImpl.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/MomentImpl.java
@@ -8,16 +8,13 @@
 
 import org.joda.time.DateTime;
 
-import io.v.moments.ifc.Advertiser;
-import io.v.moments.ifc.MomentFactory;
-import io.v.moments.lib.Id;
 import io.v.moments.ifc.Moment;
-import io.v.v23.discovery.Attributes;
+import io.v.moments.lib.Id;
 
 /**
  * A photo and ancillary information.
  */
-public class MomentImpl implements Moment {
+class MomentImpl implements Moment {
     private static final String NOT_LETTERS_DIGITS = "[^a-zA-Z0-9]";
     protected final BitMapper mBitMapper;
     private final DateTime mCreationTime;
@@ -48,15 +45,6 @@
         mDesiredAdState = value;
     }
 
-    @Override
-    public Attributes makeAttributes() {
-        Attributes attr = new Attributes();
-        attr.put(MomentFactory.F.AUTHOR.toString(), getAuthor());
-        attr.put(MomentFactory.F.CAPTION.toString(), getCaption());
-        attr.put(MomentFactory.F.DATE.toString(), FMT.print(getCreationTime()));
-        return attr;
-    }
-
     public boolean hasPhoto(Kind kind, Style style) {
         return mBitMapper.exists(getOrdinal(), kind, style);
     }
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/ScannerImpl.java b/projects/moments/app/src/main/java/io/v/moments/model/ScannerImpl.java
deleted file mode 100644
index 82f14f3..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/model/ScannerImpl.java
+++ /dev/null
@@ -1,114 +0,0 @@
-// 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.moments.model;
-
-import android.util.Log;
-
-import com.google.common.util.concurrent.FutureCallback;
-
-import io.v.moments.ifc.Scanner;
-import io.v.moments.lib.V23Manager;
-import io.v.v23.InputChannelCallback;
-import io.v.v23.context.VContext;
-import io.v.v23.discovery.Update;
-
-/**
- * Handles scanning for moments.
- *
- * This class similar to AdvertiserImpl - see that class for more commentary -
- * in that it functions as decoupling of the UX from the moving target that is
- * the V23/VContext API
- *
- * At the moment, the complexity is far less than it is for advertising
- * (scanning doesn't require a running service), so this class doesn't add much
- * value over using an instance of V23Manager and VContext directly in whatever
- * class uses a Scanner.  To make this class more useful in decoupling moments
- * specific code from v23 specifics, this class could tease apart a scan Update,
- * and feed advertisement data from the Update.Found and Update.Lost object
- * directly into, say, the appropriate instances of java.util.function.Consumer<T>
- * that were passed to #start en lieu of the InputChannelCallback<Update>
- * updateCallback.  That way the caller would have no exposure to v23 classes.
- */
-public class ScannerImpl implements Scanner {
-    private static final String TAG = "ScannerImpl";
-    private final V23Manager mV23Manager;
-    private final String mQuery;
-    private VContext mScanCtx;
-
-    public ScannerImpl(V23Manager v23Manager, String query) {
-        if (v23Manager == null) {
-            throw new IllegalArgumentException("Null v23Manager.");
-        }
-        if (query == null || query.isEmpty()) {
-            throw new IllegalArgumentException("Empty query.");
-        }
-        mV23Manager = v23Manager;
-        mQuery = query;
-    }
-
-    @Override
-    public String toString() {
-        return "scan(" + mQuery + "," + isScanning() + ")";
-    }
-
-    /**
-     * Accepts the scanning context on success (or assures that it's null on
-     * failure), then activates the callback supplied by the client (likely to
-     * change the UX).
-     */
-    private FutureCallback<VContext> makeWrapped(
-            final FutureCallback<Void> startup) {
-        return new FutureCallback<VContext>() {
-            @Override
-            public void onSuccess(VContext context) {
-                mScanCtx = context;
-                startup.onSuccess(null);
-            }
-
-            @Override
-            public void onFailure(final Throwable t) {
-                mScanCtx = null;
-                startup.onFailure(t);
-            }
-        };
-    }
-
-    @Override
-    public void start(
-            FutureCallback<Void> startupCallback,
-            InputChannelCallback<Update> updateCallback,
-            FutureCallback<Void> completionCallback) {
-        Log.d(TAG, "Entering start.");
-        if (isScanning()) {
-            startupCallback.onFailure(
-                    new IllegalStateException("Already scanning."));
-            return;
-        }
-        mV23Manager.scan(
-                mQuery, Config.Discovery.DURATION,
-                makeWrapped(startupCallback),
-                updateCallback,
-                completionCallback);
-        Log.d(TAG, "Exiting start.");
-    }
-
-    @Override
-    public boolean isScanning() {
-        return mScanCtx != null && !mScanCtx.isCanceled();
-    }
-
-    @Override
-    public void stop() {
-        Log.d(TAG, "Entering stop");
-        if (mScanCtx != null) {
-            Log.d(TAG, "Cancelling scan.");
-            if (!mScanCtx.isCanceled()) {
-                mScanCtx.cancel();
-            }
-            mScanCtx = null;
-        }
-        Log.d(TAG, "Exiting stop");
-    }
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/DividerItemDecoration.java b/projects/moments/app/src/main/java/io/v/moments/ux/DividerItemDecoration.java
index bc2a56a..507c6fc 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/DividerItemDecoration.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/DividerItemDecoration.java
@@ -31,7 +31,7 @@
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
 
-public class DividerItemDecoration extends RecyclerView.ItemDecoration {
+class DividerItemDecoration extends RecyclerView.ItemDecoration {
 
     public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
     public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java b/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java
index f1a91c0..1c85820 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java
@@ -34,24 +34,25 @@
 import java.util.concurrent.Executors;
 
 import io.v.moments.R;
-import io.v.moments.ifc.Advertiser;
 import io.v.moments.ifc.Moment;
 import io.v.moments.ifc.MomentFactory;
-import io.v.moments.ifc.Scanner;
 import io.v.moments.lib.DiscoveredList;
+import io.v.moments.lib.FileUtil;
 import io.v.moments.lib.Id;
 import io.v.moments.lib.ObservedList;
 import io.v.moments.lib.PermissionManager;
-import io.v.moments.lib.V23Manager;
 import io.v.moments.model.AdConverterMoment;
 import io.v.moments.model.AdvertiserFactory;
 import io.v.moments.model.BitMapper;
 import io.v.moments.model.Config;
-import io.v.moments.model.FileUtil;
+import io.v.moments.model.MomentAdCampaign;
 import io.v.moments.model.MomentFactoryImpl;
-import io.v.moments.model.ScannerImpl;
 import io.v.moments.model.StateStore;
 import io.v.moments.model.Toaster;
+import io.v.moments.v23.ifc.Advertiser;
+import io.v.moments.v23.ifc.Scanner;
+import io.v.moments.v23.ifc.V23Manager;
+import io.v.moments.v23.impl.V23ManagerImpl;
 import io.v.v23.security.Blessings;
 
 /**
@@ -81,9 +82,9 @@
  *
  * TODO: when reloading from prefs, don't change advertise or scan state. only
  * do that when reloading from bundle. TODO: ScannerImpl should handle the
- * Update parsing currently done by DiscoveredList. TODO: unit tests. TODO:
- * Add version number to prefs, ignore and overwrite state if old version (to
- * avoid need to manually wipe data to avoid crashes).
+ * Update parsing currently done by DiscoveredList. TODO: unit tests. TODO: Add
+ * version number to prefs, ignore and overwrite state if old version (to avoid
+ * need to manually wipe data to avoid crashes).
  */
 public class MainActivity extends AppCompatActivity {
     private static final String TAG = "MainActivity";
@@ -106,7 +107,7 @@
     // For changes to UX.
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     // For discovery, serving and behaving as a client.
-    private final V23Manager mV23Manager = V23Manager.Singleton.get();
+    private final V23Manager mV23Manager = V23ManagerImpl.Singleton.get();
 
     // See wireUxToDataModel for discussion of the following.
     private StateStore mStateStore;
@@ -188,12 +189,12 @@
         // Compresses byte data, converts byte[] to bitmap, manages file storage.
         mBitMapper = Config.makeBitmapper(this);
 
-        // Makes advertisers.  Needs v23Manager to do advertising.
-        mAdvertiserFactory = new AdvertiserFactory(mV23Manager);
-
         // Makes moments.  Each moment needs a bitmapper to read its BitMaps.
         mMomentFactory = new MomentFactoryImpl(mBitMapper);
 
+        // Makes advertisers.  Needs v23Manager to do advertising.
+        mAdvertiserFactory = new AdvertiserFactory(mV23Manager, mMomentFactory);
+
         // Local moments, with photos taken by the local device.
         mLocalMoments = new ObservedList<>();
 
@@ -212,7 +213,7 @@
 
         Toaster toaster = new Toaster(this);
 
-        mScanner = new ScannerImpl(mV23Manager, Config.Discovery.QUERY);
+        mScanner = mV23Manager.makeScanner(MomentAdCampaign.QUERY);
         mScanSwitchHolder = new ScanSwitchHolder(
                 toaster, mScanner, mRemoteMoments);
 
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java b/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java
index 3950771..b85050f 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java
@@ -5,7 +5,6 @@
 package io.v.moments.ux;
 
 import android.content.Context;
-import android.os.Handler;
 import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -21,7 +20,7 @@
 /**
  * Stacks two moment lists in a recycler view.
  */
-public class MomentAdapter extends RecyclerView.Adapter<MomentHolder>
+class MomentAdapter extends RecyclerView.Adapter<MomentHolder>
         implements ListObserver {
     private final ObservedList<Moment> mRemoteMoments;
     private final ObservedList<Moment> mLocalMoments;
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java b/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java
index e172f1e..df8b18a 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java
@@ -13,26 +13,32 @@
 import android.widget.CompoundButton;
 import android.widget.ImageView;
 import android.widget.TextView;
-import android.widget.Toast;
 
 import com.google.common.util.concurrent.FutureCallback;
 
+import org.joda.time.Duration;
+
 import java.util.concurrent.CancellationException;
 
 import io.v.moments.R;
-import io.v.moments.ifc.Advertiser;
 import io.v.moments.ifc.Moment;
 import io.v.moments.ifc.Moment.Kind;
 import io.v.moments.ifc.Moment.Style;
 import io.v.moments.model.Toaster;
+import io.v.moments.v23.ifc.Advertiser;
 
 import static io.v.moments.ifc.Moment.AdState;
 
 /**
  * Holds the views comprising a Moment for a RecyclerView.
  */
-public class MomentHolder extends RecyclerView.ViewHolder {
+class MomentHolder extends RecyclerView.ViewHolder {
     private static final String TAG = "MomentHolder";
+    /**
+     * After this duration a advertisement automatically stop. Choice is
+     * arbitrary.
+     */
+    private static final Duration DURATION = Duration.standardMinutes(5);
     private final TextView mAuthorTextView;
     private final TextView mCaptionTextView;
     private final SwitchCompat mAdvertiseButton;
@@ -106,7 +112,8 @@
                     if (!advertiser.isAdvertising()) {
                         advertiser.start(
                                 makeAdvertiseStartCallback(moment),
-                                makeAdvertiseStopCallback(moment));
+                                makeAdvertiseStopCallback(moment),
+                                DURATION);
                     } else {
                         Log.d(TAG, "Advertiser already on.");
                     }
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/ScanSwitchHolder.java b/projects/moments/app/src/main/java/io/v/moments/ux/ScanSwitchHolder.java
index c5187f4..bb660d5 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/ScanSwitchHolder.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/ScanSwitchHolder.java
@@ -4,24 +4,20 @@
 
 package io.v.moments.ux;
 
-import android.app.Activity;
 import android.support.v7.widget.SwitchCompat;
 import android.util.Log;
 import android.widget.CompoundButton;
-import android.widget.Toast;
 
 import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
+
+import org.joda.time.Duration;
 
 import java.util.concurrent.CancellationException;
 
 import io.v.moments.ifc.Moment;
-import io.v.moments.ifc.Scanner;
 import io.v.moments.lib.DiscoveredList;
 import io.v.moments.model.Toaster;
-import io.v.v23.InputChannelCallback;
-import io.v.v23.discovery.Update;
+import io.v.moments.v23.ifc.Scanner;
 
 /**
  * Manages scanning UX.
@@ -29,8 +25,12 @@
  * The callbacks provided will be run on the UX thread, so its safe to perform
  * UX operations in the callbacks.
  */
-public class ScanSwitchHolder implements CompoundButton.OnCheckedChangeListener {
+class ScanSwitchHolder implements CompoundButton.OnCheckedChangeListener {
     private static final String TAG = "ScanSwitchHolder";
+    /**
+     * After this duration a scan automatically stop. Choice is arbitrary.
+     */
+    private static final Duration DURATION = Duration.standardMinutes(5);
     private final Scanner mScanner;
     private final Toaster mToaster;
     private final DiscoveredList<Moment> mRemoteMoments;
@@ -64,7 +64,12 @@
                 Log.d(TAG, "Asked to start scanning, but already scanning.");
                 return;
             }
-            mScanner.start(makeStartupCallback(), makeUpdateCallback(), makeCompletionCallback());
+            mScanner.start(
+                    makeStartupCallback(),
+                    mRemoteMoments,
+                    mRemoteMoments,
+                    makeCompletionCallback(),
+                    DURATION);
         } else {
             if (!mScanner.isScanning()) {
                 Log.d(TAG, "Asked to stop scanning, but already not scanning.");
@@ -99,17 +104,9 @@
         };
     }
 
-    private InputChannelCallback<Update> makeUpdateCallback() {
-        return new InputChannelCallback<Update>() {
-            @Override
-            public ListenableFuture<Void> onNext(Update result) {
-                mRemoteMoments.scanUpdateReceived(result);
-                return Futures.immediateFuture(null);
-            }
-        };
-    }
-
-    /** Verify that scanning is off and that the UX reflects that fact. */
+    /**
+     * Verify that scanning is off and that the UX reflects that fact.
+     */
     private void cleanUpPostStop() {
         Log.d(TAG, "cleanUpPostStop");
         if (mScanner.isScanning()) {
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdCampaign.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdCampaign.java
new file mode 100644
index 0000000..bc8155d
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdCampaign.java
@@ -0,0 +1,69 @@
+// 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.moments.v23.ifc;
+
+import java.util.List;
+
+import io.v.v23.discovery.Attachments;
+import io.v.v23.discovery.Attributes;
+import io.v.v23.security.BlessingPattern;
+
+/**
+ * Provides the data needed to run an advertisement.
+ */
+public interface AdCampaign {
+    /**
+     * Unique Id associated with the advertisement, used to discriminate when an
+     * ad is found or lost.
+     */
+    String getInstanceId();
+
+    /**
+     * Optional human readable name, can be used in query discrimination.
+     */
+    String getInstanceName();
+
+    /**
+     * Optional service interface name, can be used in query discrimination. The
+     * name, if provided, should be the name of the service interface associated
+     * with the result of #makeServer().
+     */
+    String getInterfaceName();
+
+    /**
+     * Map of 'smallish' name/value pairs to send with the advertisement.
+     */
+    Attributes getAttributes();
+
+    /**
+     * Larger blobs of data to made available asynchronously to scanners.
+     */
+    Attachments getAttachments();
+
+    /**
+     * Makes an instance of a service (a set of handlers) that will be run
+     * during the life of the advertisement.
+     *
+     * I.e., every time an advertisement is started using this campaign, this
+     * factory method will be called to create a new service object with a clean
+     * state.  The service object will be used to start an actual server that
+     * will serve requests only as long as the advertisement.
+     *
+     * If null returned, no server is launched.
+     */
+    Object makeService();
+
+    /**
+     * Name at which the service associated with #makeService() should be
+     * mounted. Can be empty.
+     */
+    String getMountName();
+
+    /**
+     * A set of blessing patterns for whom this advertisement is meant; any
+     * entity not matching a pattern here won't see the advertisement.
+     */
+    List<BlessingPattern> getVisibility();
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdConverter.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdConverter.java
new file mode 100644
index 0000000..24db2ea
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdConverter.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.moments.v23.ifc;
+
+import io.v.v23.discovery.Service;
+
+/**
+ * Implementations of this interface construct an instance of T from the
+ * attributes in the advertisement, and/or by making RPCs to services associated
+ * with or otherwise mentioned by the advertisement.
+ *
+ * TODO(jregan): This method should return ListenableFuture<T>.
+ */
+public interface AdConverter<T> {
+    T make(Service advertisement);
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementFoundListener.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementFoundListener.java
new file mode 100644
index 0000000..887284f
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementFoundListener.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.moments.v23.ifc;
+
+import io.v.v23.discovery.Service;
+
+/**
+ * Something that handles recently found advertisements.
+ */
+public interface AdvertisementFoundListener {
+    void handleFoundAdvertisement(Service advertisement);
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementLostListener.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementLostListener.java
new file mode 100644
index 0000000..da8be17
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementLostListener.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.moments.v23.ifc;
+
+import io.v.v23.discovery.Service;
+
+/**
+ * Something that handles recently lost advertisements.
+ */
+public interface AdvertisementLostListener {
+    void handleLostAdvertisement(Service advertisement);
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Advertiser.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Advertiser.java
new file mode 100644
index 0000000..10c5c2d
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Advertiser.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.moments.v23.ifc;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import org.joda.time.Duration;
+
+/**
+ * Advertiser - can start, stop, and restart advertisements.
+ *
+ * The start and stop cycle is appropriate for connection to a toggle button.
+ */
+public interface Advertiser {
+    /**
+     * Asynchronously start advertising.
+     *
+     * Callbacks can be expected to run on the UX thread.
+     *
+     * @param onStart Callback with success or failure handlers for advertiser
+     *                startup.  A success can switch a toggle button to "on".
+     * @param onStop  Callback with success or failure handlers for advertiser
+     *                shutdown. An advertiser might shutdown for reasons other
+     *                than a call to stop, e.g. a timeout.  The callback can,
+     *                say, switch a toggle button back to "off".
+     * @param timeout Amount of time until the advertisement self-cancels.
+     */
+    void start(FutureCallback<Void> onStart,
+               FutureCallback<Void> onStop,
+               Duration timeout);
+
+    /**
+     * True if stop could usefully be called.
+     */
+    boolean isAdvertising();
+
+    /**
+     * Synchronously stop advertising.  Should result in execution of onStop
+     * callback.
+     */
+    void stop();
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Scanner.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Scanner.java
new file mode 100644
index 0000000..057f42d
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Scanner.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.moments.v23.ifc;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import org.joda.time.Duration;
+
+/**
+ * Scanner - can start, stop, and restart a scan for advertisements.
+ *
+ * The start and stop cycle is appropriate for connection to a toggle button.
+ */
+public interface Scanner {
+    /**
+     * Asynchronously start scanning.
+     *
+     * Callbacks can be expected to run on the UX thread.
+     *
+     * @param onStart       Callback with success or failure handlers for scan
+     *                      startup.  A success can switch a toggle button to
+     *                      "on".
+     * @param foundListener Handler executed on each found newly found
+     *                      advertisement.
+     * @param lostListener  Handler executed on each previously seen but now
+     *                      lost advertisement.
+     * @param onStop        Callback with success or failure handlers for scan
+     *                      shutdown. A scan might shutdown for reasons other
+     *                      than a call to stop, e.g. a timeout.  The callback
+     *                      can, say, switch a toggle button back to "off".
+     * @param timeout       Amount of time until the scan self-cancels.
+     */
+    void start(FutureCallback<Void> onStart,
+               AdvertisementFoundListener foundListener,
+               AdvertisementLostListener lostListener,
+               FutureCallback<Void> onStop,
+               Duration timeout);
+
+    /**
+     * True if stop could usefully be called.
+     */
+    boolean isScanning();
+
+    /**
+     * Synchronously stop scanning.  Should result in execution of onStop
+     * callback.
+     */
+    void stop();
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/V23Manager.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/V23Manager.java
new file mode 100644
index 0000000..00949e1
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/V23Manager.java
@@ -0,0 +1,66 @@
+// 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.moments.v23.ifc;
+
+import android.app.Activity;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import org.joda.time.Duration;
+
+import io.v.v23.context.VContext;
+import io.v.v23.security.Blessings;
+
+/**
+ * Secure distributed computing via underlying v23 APIs.
+ *
+ * This and other interfaces in the encompassing package comprise an API that
+ * might feel more comfortable to java Android developers.  It wraps the
+ * underlying static v23 methods in a framework of injectable, mockable
+ * instances.
+ */
+public interface V23Manager {
+    /**
+     * Start V23 runtime bound to the given activity, and give it a callback via
+     * which it will get its blessings.  This should be called on onCreate().
+     *
+     * When the blessings come in, the app can safely use v23 operations that
+     * require a notion of identity, but until that time should make no attempt
+     * to do so.
+     */
+    void init(Activity activity, FutureCallback<Blessings> blessingCallback);
+
+    /**
+     * Shutdown the v23 runtime.  This should be called in onDestroy() to cancel
+     * any lingering contexts associated with v23 operations (advertising,
+     * scanning, serving etc.), so that a subsequent call to init - say, during
+     * destroy/create lifecycle event series - will start with clean state in
+     * the v23 runtime.
+     */
+    void shutdown();
+
+    /**
+     * Used by v23 clients to make v23 RPCs.
+     *
+     * @param duration Amount of time until the operation self-cancels.
+     */
+    VContext contextWithTimeout(Duration duration);
+
+    /**
+     * Returns an advertiser bound to the given adCampaign.
+     *
+     * @param adCampaign Immutable description of the ad to run.
+     * @return Advertiser that can start, stop and restart the advertisement.
+     */
+    Advertiser makeAdvertiser(AdCampaign adCampaign);
+
+    /**
+     * Returns a scanner that will look for advertisements matching the query.
+     *
+     * @param query Query limiting the ads that are processed.
+     * @return Scanner that can start, stop and restart the scan.
+     */
+    Scanner makeScanner(String query);
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/impl/V23ManagerImpl.java b/projects/moments/app/src/main/java/io/v/moments/v23/impl/V23ManagerImpl.java
new file mode 100644
index 0000000..b64e262
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/impl/V23ManagerImpl.java
@@ -0,0 +1,417 @@
+// 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.moments.v23.impl;
+
+import android.app.Activity;
+import android.content.Context;
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.joda.time.Duration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.android.libs.security.BlessingsManager;
+import io.v.android.v23.V;
+import io.v.moments.v23.ifc.AdCampaign;
+import io.v.moments.v23.ifc.AdvertisementFoundListener;
+import io.v.moments.v23.ifc.AdvertisementLostListener;
+import io.v.moments.v23.ifc.Advertiser;
+import io.v.moments.v23.ifc.Scanner;
+import io.v.moments.v23.ifc.V23Manager;
+import io.v.v23.InputChannelCallback;
+import io.v.v23.InputChannels;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Service;
+import io.v.v23.discovery.Update;
+import io.v.v23.discovery.VDiscovery;
+import io.v.v23.naming.Endpoint;
+import io.v.v23.security.Blessings;
+import io.v.v23.security.VSecurity;
+import io.v.v23.verror.VException;
+
+/**
+ * Various static V23 utilities gathered into an instantiatable class.
+ *
+ * This allows V23 usage to be injected and mocked.  The class is a singleton to
+ * avoid confusion about init, shutdown and the 'base context'.
+ *
+ * Nothing in here has anything in particular to do with Moments; it's an object
+ * hiding an evolving API based on static functions.
+ */
+public class V23ManagerImpl implements V23Manager {
+    private static final String TAG = "V23ManagerImpl";
+    private static final String BLESSINGS_KEY = "BlessingsKey";
+    private Context mAndroidCtx;
+    private VContext mV23Ctx = null;
+    private VDiscovery mDiscovery = null;
+
+    // Constructor should only be called from tests.
+    /* package private */ V23ManagerImpl() {
+    }
+
+    @Override
+    public synchronized void init(
+            Activity activity, FutureCallback<Blessings> blessingCallback) {
+        Log.d(TAG, "init");
+        if (mAndroidCtx != null) {
+            if (mAndroidCtx == activity.getApplicationContext()) {
+                Log.d(TAG, "Initialization already started.");
+                return;
+            } else {
+                Log.d(TAG, "Initialization with new context.");
+                shutdown();
+            }
+        }
+        mAndroidCtx = activity.getApplicationContext();
+        // Must call V.init before attempting to load blessings, so that proper
+        // code is loaded.
+        mV23Ctx = V.init(mAndroidCtx);
+        Log.d(TAG, "Attempting to get blessings.");
+        ListenableFuture<Blessings> f = BlessingsManager.getBlessings(
+                mV23Ctx, activity, BLESSINGS_KEY, true);
+        Futures.addCallback(f, blessingCallback);
+        try {
+            mDiscovery = V.newDiscovery(mV23Ctx);
+        } catch (VException e) {
+            Log.d(TAG, "Unable to get discovery object.", e);
+        }
+    }
+
+    @Override
+    public void shutdown() {
+        Log.d(TAG, "Shutdown");
+        if (mAndroidCtx == null) {
+            Log.d(TAG, "Was never initialized.");
+            return;
+        }
+        mV23Ctx.cancel();
+        mAndroidCtx = null;
+    }
+
+    private VContext makeServerContext(
+            String mountName, Object server) throws VException {
+        return V.withNewServer(
+                mV23Ctx.withCancel(),
+                mountName,
+                server,
+                VSecurity.newAllowEveryoneAuthorizer());
+    }
+
+    private List<String> makeServerAddressList(VContext serverCtx) {
+        List<String> addresses = new ArrayList<>();
+        Endpoint[] points = V.getServer(
+                serverCtx).getStatus().getEndpoints();
+        for (Endpoint point : points) {
+            addresses.add(point.toString());
+        }
+        return addresses;
+    }
+
+    @Override
+    public VContext contextWithTimeout(Duration timeout) {
+        return mV23Ctx.withTimeout(timeout);
+    }
+
+    @Override
+    public Advertiser makeAdvertiser(AdCampaign adCampaign) {
+        return new AdvertiserImpl(adCampaign);
+    }
+
+    @Override
+    public Scanner makeScanner(String query) {
+        return new ScannerImpl(query);
+    }
+
+    public static class Singleton {
+        private static volatile V23ManagerImpl instance;
+
+        public static V23ManagerImpl get() {
+            V23ManagerImpl result = instance;
+            if (instance == null) {
+                synchronized (Singleton.class) {
+                    result = instance;
+                    if (result == null) {
+                        instance = result = new V23ManagerImpl();
+                    }
+                }
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Manages one instance of advertising - one ad, one backing service.
+     *
+     * This class hides/manages the two v23 contexts necessary to run a service
+     * and a related advertisement.
+     */
+    class AdvertiserImpl implements Advertiser {
+        private static final String TAG = "AdvertiserImpl";
+
+        private final AdCampaign mAdCampaign;
+
+        private VContext mAdvCtx;
+        private VContext mServerCtx;
+        private Duration mDuration;
+
+        public AdvertiserImpl(AdCampaign adCampaign) {
+            if (adCampaign == null) {
+                throw new IllegalArgumentException("Null adCampaign");
+            }
+            mAdCampaign = adCampaign;
+        }
+
+        @Override
+        public void start(FutureCallback<Void> onStartCallback,
+                          FutureCallback<Void> onStopCallback,
+                          Duration timeout) {
+            Log.d(TAG, "Entering start.");
+            if (isAdvertising()) {
+                onStartCallback.onFailure(
+                        new IllegalStateException("Already advertising."));
+                return;
+            }
+
+            if (timeout == null) {
+                throw new IllegalArgumentException("Null timeout");
+            }
+            mDuration = timeout;
+
+            if (mDiscovery == null) {
+                onStartCallback.onFailure(
+                        new IllegalStateException("Discovery not ready."));
+                return;
+            }
+
+            try {
+                mServerCtx = makeServerContext(
+                        mAdCampaign.getMountName(),
+                        mAdCampaign.makeService());
+            } catch (VException e) {
+                onStartCallback.onFailure(
+                        new IllegalStateException("Unable to start service.", e));
+                return;
+            }
+
+
+            VContext context = contextWithTimeout(mDuration);
+
+            Service advertisement = new Service(
+                    mAdCampaign.getInstanceId(),
+                    mAdCampaign.getInstanceName(),
+                    mAdCampaign.getInterfaceName(),
+                    mAdCampaign.getAttributes(),
+                    makeServerAddressList(mServerCtx),
+                    mAdCampaign.getAttachments());
+
+            ListenableFuture<ListenableFuture<Void>> nestedFuture =
+                    mDiscovery.advertise(
+                            context,
+                            advertisement,
+                            mAdCampaign.getVisibility());
+
+            Futures.addCallback(
+                    nestedFuture,
+                    deliverContextCallback(
+                            context,
+                            confirmCleanStartCallback(onStartCallback),
+                            onStopCallback));
+
+            Log.d(TAG, "Exiting start.");
+        }
+
+        /**
+         * A service must be started before advertising begins, which creates a
+         * cleanup problem should advertising fail to start. This callback
+         * accepts the advertising context on startup success, and cancels the
+         * already started service on failure.
+         *
+         * This wrapper necessary to cleanly kill the service should advertising
+         * fail to start.  This callback feeds info to the startup callback,
+         * which presumably has some effect on UX.
+         */
+        private FutureCallback<VContext> confirmCleanStartCallback(
+                final FutureCallback<Void> onStart) {
+            return new FutureCallback<VContext>() {
+                @Override
+                public void onSuccess(VContext context) {
+                    mAdvCtx = context;
+                    onStart.onSuccess(null);
+                }
+
+                @Override
+                public void onFailure(final Throwable t) {
+                    mAdvCtx = null;
+                    cancelService();
+                    onStart.onFailure(t);
+                }
+            };
+        }
+
+        /**
+         * This exists to allow the scan and advertise interfaces to behave the
+         * same way to clients (accepting simple Callback<Void> for both onStart
+         * and onStop), and to deliver the context via the "success" callback so
+         * there's no chance of a race condition between usage of that context
+         * and code in the onStart callback.
+         */
+        private FutureCallback<ListenableFuture<Void>> deliverContextCallback(
+                final VContext context,
+                final FutureCallback<VContext> onStart,
+                final FutureCallback<Void> onStop) {
+            return new FutureCallback<ListenableFuture<Void>>() {
+                @Override
+                public void onSuccess(ListenableFuture<Void> result) {
+                    onStart.onSuccess(context);
+                    Futures.addCallback(result, onStop);
+                }
+
+                @Override
+                public void onFailure(final Throwable t) {
+                    onStart.onFailure(t);
+                }
+            };
+        }
+
+        @Override
+        public boolean isAdvertising() {
+            return mAdvCtx != null && !mAdvCtx.isCanceled();
+        }
+
+        @Override
+        public void stop() {
+            if (!isAdvertising()) {
+                throw new IllegalStateException("Not advertising.");
+            }
+            Log.d(TAG, "Entering stop");
+            if (mAdvCtx != null) {
+                Log.d(TAG, "Cancelling advertising.");
+                if (!mAdvCtx.isCanceled()) {
+                    mAdvCtx.cancel();
+                }
+                mAdvCtx = null;
+            }
+            cancelService();
+            Log.d(TAG, "Exiting stop");
+        }
+
+        private void cancelService() {
+            if (mServerCtx != null) {
+                Log.d(TAG, "Cancelling service.");
+                if (!mServerCtx.isCanceled()) {
+                    mServerCtx.cancel();
+                }
+                mServerCtx = null;
+            }
+        }
+    }
+
+    /**
+     * Handles scanning for moments.
+     *
+     * To make this class more useful in decoupling moments specific code from
+     * v23 specifics, this class could tease apart a scan Update, and feed
+     * advertisement data from the Update.Found and Update.Lost object directly
+     * into, say, the appropriate instances of java.util.function.Consumer<T>
+     * that were passed to #start en lieu of the InputChannelCallback<Update>
+     * updateCallback.  That way the caller would have no exposure to v23
+     * classes (Update and Lost).
+     */
+    class ScannerImpl implements Scanner {
+        private static final String TAG = "ScannerImpl";
+        private final String mQuery;
+        private Duration mDuration;
+        private VContext mScanCtx;
+
+        public ScannerImpl(String query) {
+            if (query == null || query.isEmpty()) {
+                throw new IllegalArgumentException("Empty query.");
+            }
+            mQuery = query;
+        }
+
+        @Override
+        public String toString() {
+            return "scan(" + mQuery + "," + isScanning() + ")";
+        }
+
+        @Override
+        public void start(
+                FutureCallback<Void> onStart,
+                AdvertisementFoundListener foundListener,
+                AdvertisementLostListener lostListener,
+                FutureCallback<Void> onStop,
+                Duration timeout) {
+            Log.d(TAG, "Entering start.");
+            if (isScanning()) {
+                onStart.onFailure(
+                        new IllegalStateException("Already scanning."));
+                return;
+            }
+            if (timeout == null) {
+                throw new IllegalArgumentException("Null timeout.");
+            }
+            mDuration = timeout;
+            Log.d(TAG, "Starting scan with q=[" + mQuery + "]");
+            if (mDiscovery == null) {
+                onStart.onFailure(
+                        new IllegalStateException("Discovery not ready."));
+                return;
+            }
+            mScanCtx = contextWithTimeout(mDuration);
+
+            Futures.addCallback(
+                    InputChannels.withCallback(
+                            mDiscovery.scan(mScanCtx, mQuery),
+                            makeUpdateCallback(foundListener, lostListener)),
+                    onStop);
+
+            onStart.onSuccess(null);
+
+            Log.d(TAG, "Exiting start.");
+        }
+
+        private InputChannelCallback<Update> makeUpdateCallback(
+                final AdvertisementFoundListener foundListener,
+                final AdvertisementLostListener lostListener) {
+            return new InputChannelCallback<Update>() {
+                @Override
+                public ListenableFuture<Void> onNext(Update result) {
+                    if (result instanceof Update.Found) {
+                        foundListener.handleFoundAdvertisement(
+                                ((Update.Found) result).getElem().getService());
+                    } else {
+                        lostListener.handleLostAdvertisement(
+                                ((Update.Lost) result).getElem().getService());
+                    }
+                    return Futures.immediateFuture(null);
+                }
+            };
+        }
+
+        @Override
+        public boolean isScanning() {
+            return mScanCtx != null && !mScanCtx.isCanceled();
+        }
+
+        @Override
+        public void stop() {
+            Log.d(TAG, "Entering stop");
+            if (mScanCtx != null) {
+                Log.d(TAG, "Cancelling scan.");
+                if (!mScanCtx.isCanceled()) {
+                    mScanCtx.cancel();
+                }
+                mScanCtx = null;
+            }
+            Log.d(TAG, "Exiting stop");
+        }
+    }
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/package-info.java b/projects/moments/app/src/main/java/io/v/moments/v23/package-info.java
new file mode 100644
index 0000000..e658636
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/package-info.java
@@ -0,0 +1,13 @@
+// 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.
+
+/**
+ * Below this point sits code that wraps the underlying, evolving v23 API.
+ *
+ * None of it depends on 'moments'-specific code/data.
+ *
+ * The wrapper facilitates the use of the underlying API, and makes it easier to
+ * test classes that use the API.
+ */
+package io.v.moments.v23;
diff --git a/projects/moments/app/src/test/java/io/v/moments/lib/DiscoveredListTest.java b/projects/moments/app/src/test/java/io/v/moments/lib/DiscoveredListTest.java
index 5a91cdc..4cd4df0 100644
--- a/projects/moments/app/src/test/java/io/v/moments/lib/DiscoveredListTest.java
+++ b/projects/moments/app/src/test/java/io/v/moments/lib/DiscoveredListTest.java
@@ -6,8 +6,6 @@
 
 import android.os.Handler;
 
-import com.google.common.collect.ImmutableList;
-
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -18,20 +16,10 @@
 import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
 
-import java.util.HashSet;
-import java.util.Set;
-import java.util.UUID;
-
-import io.v.moments.ifc.AdConverter;
 import io.v.moments.ifc.HasId;
 import io.v.moments.ifc.IdSet;
 import io.v.moments.ifc.ListObserver;
-import io.v.v23.discovery.Attachments;
-import io.v.v23.discovery.Attributes;
-import io.v.v23.discovery.Found;
-import io.v.v23.discovery.Lost;
-import io.v.v23.discovery.Service;
-import io.v.v23.discovery.Update;
+import io.v.moments.v23.ifc.AdConverter;
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.never;
@@ -150,7 +138,7 @@
         mList.setObserver(mObserver);
 
         when(mConverter.make(mAdvertisement)).thenReturn(THING0);
-        mList.scanUpdateReceived(new Update.Found(new Found(mAdvertisement)));
+        mList.handleFoundAdvertisement(mAdvertisement);
 
         verify(mHandler).post(mRunnable.capture());
         mRunnable.getValue().run();
@@ -168,7 +156,7 @@
 
         when(mRejects.contains(ID0)).thenReturn(true);
 
-        mList.scanUpdateReceived(new Update.Found(new Found(mAdvertisement)));
+        mList.handleFoundAdvertisement(mAdvertisement);
 
         verifyZeroInteractions(mHandler);
         verifyZeroInteractions(mObserver);
@@ -182,7 +170,7 @@
     public void handleUnrecognizedLost() throws Exception {
         mList.setObserver(mObserver);
 
-        mList.scanUpdateReceived(new Update.Lost(new Lost(mAdvertisement)));
+        mList.handleLostAdvertisement(mAdvertisement);
 
         verify(mHandler).post(mRunnable.capture());
         mRunnable.getValue().run();
@@ -202,7 +190,7 @@
 
         when(mRejects.contains(ID0)).thenReturn(true);
 
-        mList.scanUpdateReceived(new Update.Lost(new Lost(mAdvertisement)));
+        mList.handleLostAdvertisement(mAdvertisement);
 
         verify(mHandler).post(mRunnable.capture());
         mRunnable.getValue().run();
@@ -223,7 +211,7 @@
         verify(mObserver).notifyItemInserted(0);
         assertEquals(1, mList.size());
 
-        mList.scanUpdateReceived(new Update.Lost(new Lost(mAdvertisement)));
+        mList.handleLostAdvertisement(mAdvertisement);
 
         verify(mHandler).post(mRunnable.capture());
 
diff --git a/projects/moments/app/src/test/java/io/v/moments/model/AdConverterMomentTest.java b/projects/moments/app/src/test/java/io/v/moments/model/AdConverterMomentTest.java
index d4b69a1..1097ca0 100644
--- a/projects/moments/app/src/test/java/io/v/moments/model/AdConverterMomentTest.java
+++ b/projects/moments/app/src/test/java/io/v/moments/model/AdConverterMomentTest.java
@@ -29,7 +29,7 @@
 import io.v.moments.ifc.MomentFactory;
 import io.v.moments.lib.Id;
 import io.v.moments.lib.ObservedList;
-import io.v.moments.lib.V23Manager;
+import io.v.moments.v23.ifc.V23Manager;
 import io.v.v23.context.VContext;
 
 import static org.junit.Assert.assertEquals;
@@ -87,7 +87,7 @@
         when(mMoment.getId()).thenReturn(ID);
         when(mAdvertisement.getAttrs()).thenReturn(mAttributes);
         when(mAdvertisement.getAddrs()).thenReturn(ADDRESSES);
-        when(mMomentFactory.makeFromAttributes(
+        when(mMomentFactory.fromAttributes(
                 eq(ID), anyInt(), eq(mAttributes))).thenReturn(mMoment);
         when(mClientFactory.makeClient(eq("/" + ADDRESS0))).thenReturn(mClient);
         when(mV23Manager.contextWithTimeout(
@@ -124,7 +124,7 @@
         // Make the moment - this is the call being tested.
         assertEquals(mMoment, mConverter.make(mAdvertisement));
 
-        verify(mMomentFactory).makeFromAttributes(
+        verify(mMomentFactory).fromAttributes(
                 eq(ID), mOrdinal.capture(), eq(mAttributes));
 
         // The ordinal value supplied to the factory should be one.
diff --git a/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserFactoryTest.java b/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserFactoryTest.java
index 28718ce..68f1672 100644
--- a/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserFactoryTest.java
+++ b/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserFactoryTest.java
@@ -4,11 +4,14 @@
 
 package io.v.moments.model;
 
+import org.joda.time.Duration;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
 
@@ -16,15 +19,20 @@
 import java.util.Iterator;
 import java.util.Set;
 
-import io.v.moments.ifc.Advertiser;
 import io.v.moments.ifc.Moment;
 import io.v.moments.ifc.MomentFactory;
 import io.v.moments.lib.Id;
-import io.v.moments.lib.V23Manager;
+import io.v.moments.v23.ifc.AdCampaign;
+import io.v.moments.v23.ifc.Advertiser;
+import io.v.moments.v23.ifc.V23Manager;
 
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertSame;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 @RunWith(MockitoJUnitRunner.class)
@@ -35,15 +43,29 @@
     @Rule
     public ExpectedException mThrown = ExpectedException.none();
 
+    @Captor
+    ArgumentCaptor<AdCampaign> mCampaign;
+    @Captor
+    ArgumentCaptor<Duration> mDuration;
+
     @Mock
     V23Manager mV23Manager;
     @Mock
+    MomentFactory mMomentFactory;
+    @Mock
     Moment mMoment;
+    @Mock
+    Advertiser mAdvertiser0;
+    @Mock
+    Advertiser mAdvertiser1;
+
     AdvertiserFactory mFactory;
 
     @Before
     public void setup() throws Exception {
-        mFactory = new AdvertiserFactory(mV23Manager);
+        mFactory = new AdvertiserFactory(mV23Manager, mMomentFactory);
+        when(mV23Manager.makeAdvertiser(
+                any(AdCampaign.class))).thenReturn(mAdvertiser0);
     }
 
     @Test
@@ -51,18 +73,27 @@
         when(mMoment.getId()).thenReturn(ID0);
 
         Advertiser a0 = mFactory.getOrMake(mMoment);
+        assertSame(mAdvertiser0, a0);
         assertTrue(mFactory.contains(ID0));
 
         Iterator<Advertiser> iter = mFactory.allAdvertisers().iterator();
         assertEquals(a0, iter.next());
         assertFalse(iter.hasNext());
+
+        verify(mV23Manager).makeAdvertiser(mCampaign.capture());
+
+        assertNotNull(mCampaign.getValue());
     }
 
     @Test
     public void makeTwo() throws Exception {
         when(mMoment.getId()).thenReturn(ID0);
         Advertiser a0 = mFactory.getOrMake(mMoment);
+
         when(mMoment.getId()).thenReturn(ID1);
+        when(mV23Manager.makeAdvertiser(
+                any(AdCampaign.class))).thenReturn(mAdvertiser1);
+
         Advertiser a1 = mFactory.getOrMake(mMoment);
 
         assertTrue(mFactory.contains(ID0));
diff --git a/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserImplTest.java b/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserImplTest.java
deleted file mode 100644
index 7ac73fc..0000000
--- a/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserImplTest.java
+++ /dev/null
@@ -1,171 +0,0 @@
-// 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.moments.model;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.runners.MockitoJUnitRunner;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import io.v.moments.ifc.Moment;
-import io.v.moments.lib.Id;
-import io.v.moments.lib.V23Manager;
-import io.v.v23.context.VContext;
-import io.v.v23.discovery.Attributes;
-import io.v.v23.discovery.Service;
-import io.v.v23.naming.Endpoint;
-import io.v.v23.rpc.Server;
-import io.v.v23.rpc.ServerStatus;
-import io.v.v23.security.BlessingPattern;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-@RunWith(MockitoJUnitRunner.class)
-public class AdvertiserImplTest {
-    static final Id ID = Id.makeRandom();
-    static final String MOMENT_NAME = "cabbage";
-    static final String ADDRESS = "192.168.notrealfoo";
-
-    @Rule
-    public ExpectedException mThrown = ExpectedException.none();
-
-    @Mock
-    V23Manager mV23Manager;
-    @Mock
-    VContext mServerContext;
-    @Mock
-    Server mServer;
-    @Mock
-    Moment mMoment;
-    @Mock
-    Endpoint mEndpoint;
-    @Mock
-    ServerStatus mServerStatus;
-    @Mock
-    VContext mContext;
-
-    @Captor
-    ArgumentCaptor<Service> mAdvertisement;
-    @Captor
-    ArgumentCaptor<List<BlessingPattern>> mBlessingList;
-
-    Attributes mAttrs;
-
-    AdvertiserImpl mAdvertiser;
-
-    @Before
-    public void setup() throws Exception {
-        mAttrs = new Attributes(makeFakeAttributes());
-
-        when(mV23Manager.makeServerContext(
-                eq(AdvertiserImpl.NO_MOUNT_NAME),
-                any(AdvertiserImpl.MomentServer.class))).thenReturn(mServerContext);
-        when(mV23Manager.getServer(mServerContext)).thenReturn(mServer);
-        when(mServer.getStatus()).thenReturn(mServerStatus);
-
-        Endpoint[] endpoints = {mEndpoint};
-        when(mEndpoint.toString()).thenReturn(ADDRESS);
-
-        when(mServerStatus.getEndpoints()).thenReturn(endpoints);
-
-        when(mMoment.getId()).thenReturn(ID);
-        when(mMoment.toString()).thenReturn(MOMENT_NAME);
-        when(mMoment.makeAttributes()).thenReturn(mAttrs);
-
-        List<BlessingPattern> list = any();
-        when(mV23Manager.advertise(
-                any(Service.class),
-                list)).thenReturn(mContext);
-
-        mAdvertiser = new AdvertiserImpl(mV23Manager, mMoment);
-    }
-
-    @Test
-    public void construction1() {
-        mThrown.expect(IllegalArgumentException.class);
-        mThrown.expectMessage("Null v23Manager");
-        mAdvertiser = new AdvertiserImpl(null, mMoment);
-    }
-
-    @Test
-    public void construction2() {
-        mThrown.expect(IllegalArgumentException.class);
-        mThrown.expectMessage("Null moment");
-        mAdvertiser = new AdvertiserImpl(mV23Manager, null);
-    }
-
-    @Test
-    public void advertiseStartSuccess() {
-        assertFalse(mAdvertiser.isAdvertising());
-        mAdvertiser.start();
-
-        verify(mV23Manager).advertise(
-                mAdvertisement.capture(),
-                mBlessingList.capture());
-
-        assertEquals(0, mBlessingList.getValue().size());
-
-        Service service = mAdvertisement.getValue();
-        assertEquals(ID.toString(), service.getInstanceId());
-        assertEquals(MOMENT_NAME, service.getInstanceName());
-        assertEquals(Config.INTERFACE_NAME, service.getInterfaceName());
-        assertSame(mAttrs, service.getAttrs());
-
-        List<String> addresses = service.getAddrs();
-        assertTrue(addresses.contains(ADDRESS));
-        assertEquals(1, addresses.size());
-
-        assertTrue(mAdvertiser.isAdvertising());
-    }
-
-    @Test
-    public void advertiseStartFailure() {
-        assertFalse(mAdvertiser.isAdvertising());
-        mAdvertiser.start();
-        assertTrue(mAdvertiser.isAdvertising());
-        mThrown.expect(IllegalStateException.class);
-        mThrown.expectMessage("Already advertising.");
-        mAdvertiser.start();
-    }
-
-    @Test
-    public void advertiseStopFailure() {
-        mThrown.expect(IllegalStateException.class);
-        mThrown.expectMessage("Not advertising.");
-        mAdvertiser.stop();
-    }
-
-    @Test
-    public void advertiseStopSuccess() throws Exception {
-        advertiseStartSuccess();
-        assertTrue(mAdvertiser.isAdvertising());
-        mAdvertiser.stop();
-        verify(mContext).cancel();
-        verify(mServerContext).cancel();
-        assertFalse(mAdvertiser.isAdvertising());
-    }
-
-    static Map<String, String> makeFakeAttributes() {
-        Map<String, String> result = new HashMap<>();
-        result.put("color", "teal");
-        return result;
-    }
-}
diff --git a/projects/moments/app/src/test/java/io/v/moments/model/BitMapperTest.java b/projects/moments/app/src/test/java/io/v/moments/model/BitMapperTest.java
index e06f188..3c72241 100644
--- a/projects/moments/app/src/test/java/io/v/moments/model/BitMapperTest.java
+++ b/projects/moments/app/src/test/java/io/v/moments/model/BitMapperTest.java
@@ -19,6 +19,7 @@
 import java.io.File;
 
 import io.v.moments.ifc.Moment;
+import io.v.moments.lib.FileUtil;
 
 import static org.junit.Assert.assertEquals;
 
diff --git a/projects/moments/app/src/test/java/io/v/moments/model/MomentAdCampaignTest.java b/projects/moments/app/src/test/java/io/v/moments/model/MomentAdCampaignTest.java
new file mode 100644
index 0000000..dce6e6a
--- /dev/null
+++ b/projects/moments/app/src/test/java/io/v/moments/model/MomentAdCampaignTest.java
@@ -0,0 +1,105 @@
+// 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.moments.model;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.joda.time.DateTime;
+
+import io.v.moments.ifc.Moment;
+import io.v.moments.ifc.MomentFactory;
+import io.v.moments.lib.Id;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Attributes;
+import io.v.v23.rpc.ServerCall;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MomentAdCampaignTest {
+    static final Id ID = Id.makeRandom();
+    static final String PIZZA = "pizza";
+    static final String AUTHOR = "shake a spear";
+    static final String CAPTION = "If we pull this off, we'll eat like kings.";
+    static final DateTime CREATION_TIME = DateTime.now();
+
+    @Rule
+    public ExpectedException mThrown = ExpectedException.none();
+
+    @Mock
+    MomentFactory mMomentFactory;
+    @Mock
+    Moment mMoment;
+    @Mock
+    Attributes mAttributes;
+    @Mock
+    VContext mCtx;
+    @Mock
+    ServerCall mCall;
+
+    MomentAdCampaign mCampaign;
+
+    @Before
+    public void setup() throws Exception {
+        when(mMoment.getId()).thenReturn(ID);
+        when(mMoment.toString()).thenReturn(PIZZA);
+        when(mMoment.getCaption()).thenReturn(CAPTION);
+        when(mMoment.getAuthor()).thenReturn(AUTHOR);
+        when(mMoment.getCreationTime()).thenReturn(CREATION_TIME);
+        when(mMomentFactory.toAttributes(mMoment)).thenReturn(mAttributes);
+        mCampaign = new MomentAdCampaign(mMoment, mMomentFactory);
+    }
+
+    @Test
+    public void makeWithoutMomentThrowsException() {
+        mThrown.expect(IllegalArgumentException.class);
+        mThrown.expectMessage("Null moment");
+        mCampaign = new MomentAdCampaign(null, mMomentFactory);
+    }
+
+    @Test
+    public void makeWithoutFactoryThrowsException() {
+        mThrown.expect(IllegalArgumentException.class);
+        mThrown.expectMessage("Null factory");
+        mCampaign = new MomentAdCampaign(mMoment, null);
+    }
+
+    @Test
+    public void emptyMountName() throws Exception {
+        assertEquals("", mCampaign.getMountName());
+    }
+
+    @Test
+    public void properInterfaceName() throws Exception {
+        assertEquals(
+                MomentAdCampaign.INTERFACE_NAME, mCampaign.getInterfaceName());
+    }
+
+    @Test
+    public void factoryMakesAttributes() throws Exception {
+        assertSame(mAttributes, mCampaign.getAttributes());
+    }
+
+    /**
+     * TODO(jregan): Service needs more coverage.
+     */
+    @Test
+    public void checkService() throws Exception {
+        MomentIfcServer server = (MomentIfcServer) mCampaign.makeService();
+        assertNotNull(server);
+        MomentWireData data = server.getBasics(mCtx, mCall).get();
+        assertEquals(AUTHOR, data.getAuthor());
+        assertEquals(CAPTION, data.getCaption());
+        assertEquals(CREATION_TIME.getMillis(), data.getCreationTime());
+    }
+}
diff --git a/projects/moments/app/src/test/java/io/v/moments/v23/impl/V23ManagerImplTest.java b/projects/moments/app/src/test/java/io/v/moments/v23/impl/V23ManagerImplTest.java
new file mode 100644
index 0000000..a25763e
--- /dev/null
+++ b/projects/moments/app/src/test/java/io/v/moments/v23/impl/V23ManagerImplTest.java
@@ -0,0 +1,65 @@
+// 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.moments.v23.impl;
+
+import android.app.Activity;
+import android.content.Context;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import io.v.v23.security.Blessings;
+
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class V23ManagerImplTest {
+
+    @Rule
+    public ExpectedException mThrown = ExpectedException.none();
+
+    @Mock
+    Activity mActivity;
+    @Mock
+    Context mContext;
+
+    V23ManagerImpl mManager;
+
+    FutureCallback<Blessings> makeBlessingsCallback() {
+        return new FutureCallback<Blessings>() {
+            @Override
+            public void onSuccess(Blessings blessings) {
+            }
+
+            @Override
+            public void onFailure(final Throwable t) {
+            }
+        };
+    }
+
+    @Before
+    public void setup() throws Exception {
+        mManager = new V23ManagerImpl();
+        when(mActivity.getApplicationContext()).thenReturn(mContext);
+    }
+
+    // Disabled @Test
+    public void initialization() throws Exception {
+        mManager.init(mActivity, makeBlessingsCallback());
+        mManager.shutdown();
+    }
+
+    @Test
+    public void placeholder() {
+        // TODO(jregan): Add tests.  Tricky because must start v23 runtime.
+    }
+}