TBR java moments; cleanup pass to docs and ifc.

Change-Id: I74a1e51944cc05bc7bc9be17362dfef01eba4bd9
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/model/AdConverterMoment.java b/projects/moments/app/src/main/java/io/v/moments/model/AdConverterMoment.java
index 6355ab7..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
@@ -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 2b26e51..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
@@ -9,6 +9,7 @@
 
 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.v23.ifc.Advertiser;
 import io.v.moments.v23.ifc.V23Manager;
@@ -16,16 +17,24 @@
 /**
  * Makes moment advertisers.
  *
- * More importantly, 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.
+ * 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) {
@@ -33,9 +42,7 @@
             return mLocalAds.get(moment.getId());
         }
         Advertiser result = mV23Manager.makeAdvertiser(
-                new MomentAdCampaign(moment),
-                Config.Discovery.DURATION,
-                Config.Discovery.NO_PATTERNS);
+                new MomentAdCampaign(moment, mFactory));
         mLocalAds.put(moment.getId(), result);
         return result;
     }
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 7f07857..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,14 +10,9 @@
 import android.os.Environment;
 import android.os.Handler;
 
-import org.joda.time.Duration;
-
 import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
 
 import io.v.moments.R;
-import io.v.v23.security.BlessingPattern;
 
 /**
  * Configuration.
@@ -63,29 +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);
-
-        /**
-         * Used for public advertisements (no limits on who can see them).
-         */
-        public static final List<BlessingPattern> NO_PATTERNS = new ArrayList<>();
-    }
 }
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
index df2e572..443a570 100644
--- 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
@@ -10,47 +10,98 @@
 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.Service;
+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.
  */
-class MomentAdCampaign implements AdCampaign {
-    private static final String NO_MOUNT_NAME = "";
+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) {
+    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 NO_MOUNT_NAME;
+        return "";
     }
 
-    public Object makeServer() {
+    @Override
+    public Object makeService() {
         return new MomentServer();
     }
 
     /**
-     * Makes an instance of 'Service', which is actually a service description,
-     * i.e. an advertisement.
+     * A set of blessing patterns for whom this advertisement is meant; any
+     * entity not matching a pattern here won't see the advertisement.
      */
-    public Service makeAdvertisement(List<String> addresses) {
-        return new Service(
-                mMoment.getId().toString(), /* instance Id */
-                mMoment.toString(), /* instance name */
-                Config.Discovery.INTERFACE_NAME, /* interface name */
-                mMoment.makeAttributes(),
-                addresses,
-                new Attachments() /* no attachments */);
+    @Override
+    public List<BlessingPattern> getVisibility() {
+        return NO_PATTERNS;
     }
 
     /**
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 33e22a0..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,15 +8,13 @@
 
 import org.joda.time.DateTime;
 
-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;
@@ -47,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/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 d6c17ed..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
@@ -45,6 +45,7 @@
 import io.v.moments.model.AdvertiserFactory;
 import io.v.moments.model.BitMapper;
 import io.v.moments.model.Config;
+import io.v.moments.model.MomentAdCampaign;
 import io.v.moments.model.MomentFactoryImpl;
 import io.v.moments.model.StateStore;
 import io.v.moments.model.Toaster;
@@ -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,8 +213,7 @@
 
         Toaster toaster = new Toaster(this);
 
-        mScanner = mV23Manager.makeScanner(
-                Config.Discovery.QUERY, Config.Discovery.DURATION);
+        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 9b86be3..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
@@ -16,22 +16,29 @@
 
 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.v23.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;
@@ -105,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 003653a..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
@@ -10,6 +10,8 @@
 
 import com.google.common.util.concurrent.FutureCallback;
 
+import org.joda.time.Duration;
+
 import java.util.concurrent.CancellationException;
 
 import io.v.moments.ifc.Moment;
@@ -23,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;
@@ -62,7 +68,8 @@
                     makeStartupCallback(),
                     mRemoteMoments,
                     mRemoteMoments,
-                    makeCompletionCallback());
+                    makeCompletionCallback(),
+                    DURATION);
         } else {
             if (!mScanner.isScanning()) {
                 Log.d(TAG, "Asked to stop scanning, but already not scanning.");
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
index e8a329a..bc8155d 100644
--- 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
@@ -6,27 +6,64 @@
 
 import java.util.List;
 
-import io.v.v23.discovery.Service;
+import io.v.v23.discovery.Attachments;
+import io.v.v23.discovery.Attributes;
+import io.v.v23.security.BlessingPattern;
 
 /**
- * Makes objects that support advertising.
+ * Provides the data needed to run an advertisement.
  */
 public interface AdCampaign {
     /**
-     * Makes an instance of a service that will be run during the life of the
-     * advertisement.
+     * Unique Id associated with the advertisement, used to discriminate when an
+     * ad is found or lost.
      */
-    Object makeServer();
+    String getInstanceId();
 
     /**
-     * Name at which the service should be mounted. Can be empty.
+     * 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();
 
     /**
-     * Makes an instance of 'Service', which is actually a service description,
-     * i.e. an advertisement.  The argument is the list of real addresses at
-     * which the service can be found (presumes no mount name).
+     * A set of blessing patterns for whom this advertisement is meant; any
+     * entity not matching a pattern here won't see the advertisement.
      */
-    Service makeAdvertisement(List<String> addresses);
+    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
index 26e59b8..24db2ea 100644
--- 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
@@ -7,13 +7,12 @@
 import io.v.v23.discovery.Service;
 
 /**
- * The io.v.v23.discovery.Service isn't a service, it's the *description* of a
- * service used as 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.
+ * 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 service);
+    T make(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
index d6f7631..10c5c2d 100644
--- 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
@@ -6,8 +6,12 @@
 
 import com.google.common.util.concurrent.FutureCallback;
 
+import org.joda.time.Duration;
+
 /**
- * Advertiser controls, intended to be similar to Scanner controls.
+ * Advertiser - can start, stop, and restart advertisements.
+ *
+ * The start and stop cycle is appropriate for connection to a toggle button.
  */
 public interface Advertiser {
     /**
@@ -15,14 +19,17 @@
      *
      * Callbacks can be expected to run on the UX thread.
      *
-     * @param onStart executed on success or failure of advertising startup.
-     *
-     * @param onStop  executed on success or failure of advertising completion.
-     *                An advertisement might shutdown for reasons other than a
-     *                call to stop, e.g. a timeout.
+     * @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);
+               FutureCallback<Void> onStop,
+               Duration timeout);
 
     /**
      * True if stop could usefully be called.
@@ -30,8 +37,8 @@
     boolean isAdvertising();
 
     /**
-     * Synchronously stop advertising.  Should result in execution of
-     * completionCallback.
+     * 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
index 35ec1d7..057f42d 100644
--- 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
@@ -6,8 +6,12 @@
 
 import com.google.common.util.concurrent.FutureCallback;
 
+import org.joda.time.Duration;
+
 /**
- * Scanner controls, intended to be similar to Advertiser controls.
+ * 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 {
     /**
@@ -15,17 +19,24 @@
      *
      * Callbacks can be expected to run on the UX thread.
      *
-     * @param onStart       executed on success or failure of scan startup.
-     * @param foundListener executed on each found advertisement.
-     * @param lostListener  executed on each lost advertisement.
-     * @param onStop        executed on success or failure of scan completion. A
-     *                      scan might shutdown for reasons other than a call to
-     *                      stop, e.g. a timeout.
+     * @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);
+               FutureCallback<Void> onStop,
+               Duration timeout);
 
     /**
      * True if stop could usefully be called.
@@ -33,8 +44,8 @@
     boolean isScanning();
 
     /**
-     * Synchronously stop scanning.  Should result in execution of
-     * completionCallback.
+     * 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
index cb0ccfe..00949e1 100644
--- 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
@@ -10,45 +10,57 @@
 
 import org.joda.time.Duration;
 
-import java.util.List;
-
 import io.v.v23.context.VContext;
-import io.v.v23.security.BlessingPattern;
 import io.v.v23.security.Blessings;
 
 /**
- * V23 functionality; service creation and discovery.
+ * 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.
+     * 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);
+    void init(Activity activity, FutureCallback<Blessings> blessingCallback);
 
     /**
-     * Shutdown the v23 runtime.  This should be called in onDestroy to clean up
-     * any lingering contexts associated with advertising or scanning.
+     * 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 to construct RPCs.
+     * Used by v23 clients to make v23 RPCs.
+     *
+     * @param duration Amount of time until the operation self-cancels.
      */
-    VContext contextWithTimeout(Duration timeout);
+    VContext contextWithTimeout(Duration duration);
 
     /**
-     * Returns an advertiser that will start advertising using the adCampaign
-     * for a fixed time 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,
-                              Duration duration,
-                              List<BlessingPattern> visibility);
+    Advertiser makeAdvertiser(AdCampaign adCampaign);
 
     /**
-     * Returns a scanner that will look for advertisements matching the query,
-     * for a fixed time duration.
+     * 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, Duration duration);
+    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
index 2df557c..b64e262 100644
--- 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
@@ -28,10 +28,10 @@
 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.BlessingPattern;
 import io.v.v23.security.Blessings;
 import io.v.v23.security.VSecurity;
 import io.v.v23.verror.VException;
@@ -120,15 +120,13 @@
     }
 
     @Override
-    public Advertiser makeAdvertiser(AdCampaign adCampaign,
-                                     Duration duration,
-                                     List<BlessingPattern> visibility) {
-        return new AdvertiserImpl(adCampaign, duration, visibility);
+    public Advertiser makeAdvertiser(AdCampaign adCampaign) {
+        return new AdvertiserImpl(adCampaign);
     }
 
     @Override
-    public Scanner makeScanner(String query, Duration duration) {
-        return new ScannerImpl(query, duration);
+    public Scanner makeScanner(String query) {
+        return new ScannerImpl(query);
     }
 
     public static class Singleton {
@@ -158,33 +156,22 @@
         private static final String TAG = "AdvertiserImpl";
 
         private final AdCampaign mAdCampaign;
-        private final Duration mDuration;
-        private final List<BlessingPattern> mVisibility;
 
         private VContext mAdvCtx;
         private VContext mServerCtx;
+        private Duration mDuration;
 
-        public AdvertiserImpl(
-                AdCampaign adCampaign,
-                Duration duration,
-                List<BlessingPattern> visibility) {
+        public AdvertiserImpl(AdCampaign adCampaign) {
             if (adCampaign == null) {
                 throw new IllegalArgumentException("Null adCampaign");
             }
-            if (duration == null) {
-                throw new IllegalArgumentException("Null duration");
-            }
-            if (visibility == null) {
-                throw new IllegalArgumentException("Null visibility");
-            }
             mAdCampaign = adCampaign;
-            mDuration = duration;
-            mVisibility = visibility;
         }
 
         @Override
         public void start(FutureCallback<Void> onStartCallback,
-                          FutureCallback<Void> onStopCallback) {
+                          FutureCallback<Void> onStopCallback,
+                          Duration timeout) {
             Log.d(TAG, "Entering start.");
             if (isAdvertising()) {
                 onStartCallback.onFailure(
@@ -192,6 +179,11 @@
                 return;
             }
 
+            if (timeout == null) {
+                throw new IllegalArgumentException("Null timeout");
+            }
+            mDuration = timeout;
+
             if (mDiscovery == null) {
                 onStartCallback.onFailure(
                         new IllegalStateException("Discovery not ready."));
@@ -201,7 +193,7 @@
             try {
                 mServerCtx = makeServerContext(
                         mAdCampaign.getMountName(),
-                        mAdCampaign.makeServer());
+                        mAdCampaign.makeService());
             } catch (VException e) {
                 onStartCallback.onFailure(
                         new IllegalStateException("Unable to start service.", e));
@@ -211,12 +203,19 @@
 
             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,
-                            mAdCampaign.makeAdvertisement(
-                                    makeServerAddressList(mServerCtx)),
-                            mVisibility);
+                            advertisement,
+                            mAdCampaign.getVisibility());
 
             Futures.addCallback(
                     nestedFuture,
@@ -328,17 +327,13 @@
     class ScannerImpl implements Scanner {
         private static final String TAG = "ScannerImpl";
         private final String mQuery;
-        private final Duration mDuration;
+        private Duration mDuration;
         private VContext mScanCtx;
 
-        public ScannerImpl(String query, Duration duration) {
+        public ScannerImpl(String query) {
             if (query == null || query.isEmpty()) {
                 throw new IllegalArgumentException("Empty query.");
             }
-            if (duration == null) {
-                throw new IllegalArgumentException("Null duration.");
-            }
-            mDuration = duration;
             mQuery = query;
         }
 
@@ -352,14 +347,18 @@
                 FutureCallback<Void> onStart,
                 AdvertisementFoundListener foundListener,
                 AdvertisementLostListener lostListener,
-                FutureCallback<Void> onStop) {
+                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(
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
index 9faee0b..e658636 100644
--- 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
@@ -3,9 +3,9 @@
 // license that can be found in the LICENSE file.
 
 /**
- * Below this point sits code that wraps the evolving v23 API.
+ * Below this point sits code that wraps the underlying, evolving v23 API.
  *
- * None of it depends on any 'moments' code/data.
+ * 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.
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 65ee128..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
@@ -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 4dfeafc..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
@@ -17,22 +17,21 @@
 
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Set;
 
 import io.v.moments.ifc.Moment;
+import io.v.moments.ifc.MomentFactory;
 import io.v.moments.lib.Id;
 import io.v.moments.v23.ifc.AdCampaign;
 import io.v.moments.v23.ifc.Advertiser;
 import io.v.moments.v23.ifc.V23Manager;
-import io.v.v23.security.BlessingPattern;
 
+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.Matchers.eq;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -45,15 +44,15 @@
     public ExpectedException mThrown = ExpectedException.none();
 
     @Captor
-    ArgumentCaptor<MomentAdCampaign> mSupporter;
+    ArgumentCaptor<AdCampaign> mCampaign;
     @Captor
     ArgumentCaptor<Duration> mDuration;
-    @Captor
-    ArgumentCaptor<List<BlessingPattern>> mBlessings;
 
     @Mock
     V23Manager mV23Manager;
     @Mock
+    MomentFactory mMomentFactory;
+    @Mock
     Moment mMoment;
     @Mock
     Advertiser mAdvertiser0;
@@ -64,12 +63,9 @@
 
     @Before
     public void setup() throws Exception {
-        mFactory = new AdvertiserFactory(mV23Manager);
+        mFactory = new AdvertiserFactory(mV23Manager, mMomentFactory);
         when(mV23Manager.makeAdvertiser(
-                any(AdCampaign.class),
-                eq(Config.Discovery.DURATION),
-                eq(Config.Discovery.NO_PATTERNS)
-        )).thenReturn(mAdvertiser0);
+                any(AdCampaign.class))).thenReturn(mAdvertiser0);
     }
 
     @Test
@@ -84,11 +80,9 @@
         assertEquals(a0, iter.next());
         assertFalse(iter.hasNext());
 
-        verify(mV23Manager).makeAdvertiser(
-                mSupporter.capture(), mDuration.capture(), mBlessings.capture());
+        verify(mV23Manager).makeAdvertiser(mCampaign.capture());
 
-        assertEquals(Config.Discovery.DURATION, mDuration.getValue());
-        assertSame(Config.Discovery.NO_PATTERNS, mBlessings.getValue());
+        assertNotNull(mCampaign.getValue());
     }
 
     @Test
@@ -98,10 +92,7 @@
 
         when(mMoment.getId()).thenReturn(ID1);
         when(mV23Manager.makeAdvertiser(
-                any(AdCampaign.class),
-                eq(Config.Discovery.DURATION),
-                eq(Config.Discovery.NO_PATTERNS)
-        )).thenReturn(mAdvertiser1);
+                any(AdCampaign.class))).thenReturn(mAdvertiser1);
 
         Advertiser a1 = mFactory.getOrMake(mMoment);
 
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());
+    }
+}