Merge branch 'master' into ble
diff --git a/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java b/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java
index 928d625..ef85a61 100644
--- a/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java
+++ b/lib/src/main/java/io/v/impl/google/rpc/ServerImpl.java
@@ -4,8 +4,6 @@
 
 package io.v.impl.google.rpc;
 
-import io.v.impl.google.channel.ChannelIterable;
-import io.v.v23.rpc.NetworkChange;
 import io.v.v23.rpc.Server;
 import io.v.v23.rpc.ServerStatus;
 import io.v.v23.verror.VException;
@@ -16,9 +14,6 @@
     private native void nativeAddName(long nativePtr, String name) throws VException;
     private native void nativeRemoveName(long nativePtr, String name);
     private native ServerStatus nativeGetStatus(long nativePtr) throws VException;
-    private native Iterable<NetworkChange> nativeWatchNetwork(long nativePtr) throws VException;
-    private native void nativeUnwatchNetwork(long nativePtr, ChannelIterable<NetworkChange> channel)
-            throws VException;
     private native void nativeStop(long nativePtr) throws VException;
     private native void nativeFinalize(long nativePtr);
 
@@ -43,25 +38,6 @@
         }
     }
     @Override
-    public Iterable<NetworkChange> watchNetwork() {
-        try {
-            return nativeWatchNetwork(this.nativePtr);
-        } catch (VException e) {
-            throw new RuntimeException("Couldn't watch network", e);
-        }
-    }
-    @Override
-    public void unwatchNetwork(Iterable<NetworkChange> it) {
-        if (it == null || !(it instanceof ChannelIterable)) {
-            return;
-        }
-        try {
-            nativeUnwatchNetwork(this.nativePtr, (ChannelIterable<NetworkChange>) it);
-        } catch (VException e) {
-            throw new RuntimeException("Couldn't unwatch network", e);
-        }
-    }
-    @Override
     public void stop() throws VException {
         nativeStop(this.nativePtr);
     }
diff --git a/lib/src/main/java/io/v/v23/rpc/Server.java b/lib/src/main/java/io/v/v23/rpc/Server.java
index 8bdbd81..0146525 100644
--- a/lib/src/main/java/io/v/v23/rpc/Server.java
+++ b/lib/src/main/java/io/v/v23/rpc/Server.java
@@ -34,26 +34,6 @@
     ServerStatus getStatus();
 
     /**
-     * Returns an iterator over server's network changes.  The returned iterator blocks
-     * if there aren't any immediate changes.  Some change events may be lost if the reader is
-     * too slow in its iterations.
-     * <p>
-     * You should be aware that the iterator:
-     * <p><ul>
-     *     <li>can be created <strong>only</strong> once</li>
-     *     <li>does not support {@link java.util.Iterator#remove remove}</li>
-     * </ul>
-     */
-    Iterable<NetworkChange> watchNetwork();
-
-    /**
-     * Unregisters an iterator previously returned via {@link #watchNetwork}.
-     *
-     * @param it an iterator previously returned via {@link #watchNetwork}
-     */
-    void unwatchNetwork(Iterable<NetworkChange> it);
-
-    /**
      * Gracefully stops all services on this server.  New calls are rejected, but any in-flight
      * calls are allowed to complete.  All published mountpoints are unmounted.  This call waits for
      * this process to complete and returns once the server has been shut down.
diff --git a/lib/src/test/java/io/v/v23/syncbase/SyncbaseTest.java b/lib/src/test/java/io/v/v23/syncbase/SyncbaseTest.java
index f92c383..9da28a8 100644
--- a/lib/src/test/java/io/v/v23/syncbase/SyncbaseTest.java
+++ b/lib/src/test/java/io/v/v23/syncbase/SyncbaseTest.java
@@ -17,7 +17,7 @@
 import io.v.v23.services.syncbase.nosql.BlobRef;
 import io.v.v23.services.syncbase.nosql.KeyValue;
 import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
-import io.v.v23.services.syncbase.nosql.SyncgroupPrefix;
+import io.v.v23.services.syncbase.nosql.TableRow;
 import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
 import io.v.v23.syncbase.nosql.BatchDatabase;
 import io.v.v23.syncbase.nosql.BlobReader;
@@ -317,7 +317,7 @@
 
         // "A" creates the group.
         SyncgroupSpec spec = new SyncgroupSpec("test", allowAll,
-            ImmutableList.of(new SyncgroupPrefix(TABLE_NAME, "")),
+            ImmutableList.of(new TableRow(TABLE_NAME, "")),
             ImmutableList.<String>of(), false);
         SyncgroupMemberInfo memberInfo = new SyncgroupMemberInfo((byte) 1);
         Syncgroup group = db.getSyncgroup(groupName);
@@ -331,14 +331,14 @@
         // TODO(spetrovic): test leave() and destroy().
 
         SyncgroupSpec specRMW = new SyncgroupSpec("testRMW", allowAll,
-            ImmutableList.of(new SyncgroupPrefix(TABLE_NAME, "")),
+            ImmutableList.of(new TableRow(TABLE_NAME, "")),
             ImmutableList.<String>of(), false);
         assertThat(group.getSpec(ctx).keySet()).isNotEmpty();
         String version = group.getSpec(ctx).keySet().iterator().next();
         group.setSpec(ctx, specRMW, version);
         assertThat(group.getSpec(ctx).values()).containsExactly(specRMW);
         SyncgroupSpec specOverwrite = new SyncgroupSpec("testOverwrite", allowAll,
-            ImmutableList.of(new SyncgroupPrefix(TABLE_NAME, "")),
+            ImmutableList.of(new TableRow(TABLE_NAME, "")),
             ImmutableList.<String>of(), false);
         group.setSpec(ctx, specOverwrite, "");
         assertThat(group.getSpec(ctx).values()).containsExactly(specOverwrite);
diff --git a/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java b/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java
index cd62a0c..20eaffe 100644
--- a/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java
+++ b/lib/src/test/java/io/v/x/jni/test/fortune/FortuneTest.java
@@ -17,7 +17,6 @@
 import io.v.v23.rpc.Dispatcher;
 import io.v.v23.rpc.Invoker;
 import io.v.v23.rpc.ListenSpec;
-import io.v.v23.rpc.NetworkChange;
 import io.v.v23.rpc.Server;
 import io.v.v23.rpc.ServerCall;
 import io.v.v23.rpc.ServiceObjectWithAuthorizer;
@@ -243,17 +242,6 @@
         }
     }
 
-    public void testWatchNetwork() throws Exception {
-        FortuneServer server = new FortuneServerImpl();
-        ctx = V.withNewServer(ctx, "", server, null);
-
-        // TODO(spetrovic): Figure out how to force network change in android and test that the
-        // changes get announced on this channel.
-        Server s = V.getServer(ctx);
-        Iterable<NetworkChange> channel = s.watchNetwork();
-        s.unwatchNetwork(channel);
-    }
-
     public void testContext() throws Exception {
         FortuneServer server = new FortuneServerImpl();
         ctx = V.withNewServer(ctx, "", server, null);
diff --git a/projects/syncslidepresenter/build.gradle b/projects/syncslidepresenter/build.gradle
index e13c1f1..258e6da 100644
--- a/projects/syncslidepresenter/build.gradle
+++ b/projects/syncslidepresenter/build.gradle
@@ -13,7 +13,10 @@
     }
 }
 
-mainClassName = "io.v.syncslidepresenter"
+mainClassName = "io.v.syncslidepresenter.Main"
+// Work around potential freezes in Java/Vanadium binaries
+// described by http://v.io/i/567.
+applicationDefaultJvmArgs = ['-XX:+UnlockDiagnosticVMOptions', '-XX:-LogEvents']
 
 repositories {
     mavenCentral()
@@ -25,12 +28,14 @@
     compile 'com.beust:jcommander:1.48'
 }
 
-task copyLib(type: Copy) {
+task copyLib(type: Copy, dependsOn: project(':lib').tasks.'copyVanadiumLib') {
     from([project(':lib').buildDir.getAbsolutePath(), 'libs', 'libv23.dylib'].join(File.separator))
     from([project(':lib').buildDir.getAbsolutePath(), 'libs', 'libv23.so'].join(File.separator))
     destinationDir = new File(['src', 'main', 'resources'].join(File.separator))
 }
 
+tasks.'processResources'.dependsOn(copyLib)
+
 clean {
     delete 'src/main/resources/libv23.so'
     delete 'src/main/resources/libv23.dylib'
@@ -40,5 +45,5 @@
     inputPaths += [project(':projects:syncslides').projectDir.absolutePath + '/app/src/main/java']
 }
 
-sourceCompatibility = '1.8'
-targetCompatibility = '1.8'
\ No newline at end of file
+sourceCompatibility = '1.7'
+targetCompatibility = '1.7'
diff --git a/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java b/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java
index 4f23fa5..73ba305 100644
--- a/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java
+++ b/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java
@@ -7,6 +7,7 @@
 import com.beust.jcommander.JCommander;
 import com.beust.jcommander.Parameter;
 import com.beust.jcommander.ParameterException;
+import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -21,11 +22,27 @@
 import java.awt.Window;
 import java.awt.event.ComponentAdapter;
 import java.awt.event.ComponentEvent;
-import java.awt.image.BufferedImage;
+import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
+import java.io.File;
 import java.io.IOException;
+import java.io.InputStreamReader;
 import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Set;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.rmi.AlreadyBoundException;
+import java.rmi.Naming;
+import java.rmi.NotBoundException;
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.rmi.registry.LocateRegistry;
+import java.rmi.server.UnicastRemoteObject;
 import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -36,11 +53,18 @@
 
 import io.v.android.apps.syncslides.db.VCurrentSlide;
 import io.v.android.apps.syncslides.db.VSlide;
+import io.v.android.apps.syncslides.discovery.ParticipantClient;
+import io.v.android.apps.syncslides.discovery.ParticipantClientFactory;
+import io.v.android.apps.syncslides.discovery.Presentation;
 import io.v.impl.google.naming.NamingUtil;
 import io.v.impl.google.services.syncbase.SyncbaseServer;
 import io.v.v23.V;
 import io.v.v23.context.VContext;
+import io.v.v23.namespace.Namespace;
 import io.v.v23.naming.Endpoint;
+import io.v.v23.naming.GlobReply;
+import io.v.v23.naming.MountEntry;
+import io.v.v23.naming.MountedServer;
 import io.v.v23.rpc.Server;
 import io.v.v23.security.BlessingPattern;
 import io.v.v23.security.access.AccessList;
@@ -62,7 +86,22 @@
 import io.v.v23.vom.VomUtil;
 
 /**
- * The entry point for syncslidepresenter.
+ * The entry point for syncslidepresenter. To run:
+ *
+ * <pre>
+ *     cd $JIRI_ROOT/release/java
+ *     ./gradlew :projects:syncslidepresenter:installDist
+ *     ./projects/syncslidepresenter/build/install/syncslidepresenter/bin/syncslidepresenter
+ * </pre>
+ *
+ * <p>For reasons not yet fully understood, applications running both Swing and Vanadium freeze
+ * randomly (at least on Mac OS X). We work around this by running two JVMs. The main JVM will
+ * create a sub-JVM in a separate thread, and will then proceed to join a syncgroup and look for
+ * slide data. Once a new slide is detected, the bytes representing the slide will be sent to
+ * the sub-JVM via RMI.
+ *
+ * <p>The sub-JVM simply listens for slide bytes and then, using {@link ImageIO#read}, decodes
+ * those bytes and displays the resulting image in a JFrame.
  */
 public class Main {
     private static final Logger logger = Logger.getLogger(Main.class.getName());
@@ -70,6 +109,7 @@
     private static final String SYNCBASE_DB = "syncslides";
     private static final String PRESENTATIONS_TABLE = "Presentations";
     private static final String DECKS_TABLE = "Decks";
+    private static final int MAX_PORT_PICKER_ATTEMPTS = 100;
     private final Table presentations;
     private final Table decks;
     private final ImageViewer viewer;
@@ -78,7 +118,9 @@
 
     private VContext context;
 
-    public static void main(String[] args) throws SyncbaseServer.StartException, VException, IOException {
+    public static void main(String[] args)
+            throws SyncbaseServer.StartException, VException, IOException, NotBoundException,
+            AlreadyBoundException {
         Options options = new Options();
         JCommander commander = new JCommander(options);
         try {
@@ -94,13 +136,16 @@
             return;
         }
 
-        // Make command-Q do the same as closing the main frame (i.e. exit).
-        System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
+        ImageViewer viewer = null;
+        if (options.swing) {
+            // We're just going to be running Swing. Do that and then stop.
+            startSwingServer();
+            return;
+        }
 
-        JFrame frame = new JFrame();
-        enableOSXFullscreen(frame);
-        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
-        frame.setVisible(true);
+        // Otherwise, we're a Vanadium client. Connect to the swing server and then
+        // set up vanadium.
+        viewer = startAndConnectToSwingServer(options.swingServerTimeoutSeconds);
 
         VContext baseContext = V.init();
 
@@ -109,10 +154,10 @@
         Permissions permissions = new Permissions(ImmutableMap.of("1", acl));
         String name = NamingUtil.join(options.mountPrefix, UUID.randomUUID().toString());
         logger.info("Mounting new syncbase server at " + name);
-        baseContext = SyncbaseServer.withNewServer(
-                baseContext.withTimeout(Duration.standardSeconds(options.mountTimeoutSeconds)),
-                new SyncbaseServer.Params().withPermissions(permissions).withName(name));
-        final Server server = V.getServer(baseContext);
+        VContext mountContext = SyncbaseServer.withNewServer(baseContext,
+                new SyncbaseServer.Params().withPermissions(permissions).withName(name)
+                .withStorageRootDir(options.storageRootDir));
+        final Server server = V.getServer(mountContext);
         if (server.getStatus().getEndpoints().length > 0) {
             logger.info("Mounted syncbase server at the following endpoints: ");
             for (Endpoint e : server.getStatus().getEndpoints()) {
@@ -139,22 +184,46 @@
                 presentations.create(baseContext, permissions);
             }
 
-            JPanel panel = new JPanel(new GridBagLayout());
-            ScaleToFitJPanel presentationPanel = new ScaleToFitJPanel();
-            GridBagConstraints constraints = new GridBagConstraints();
-            constraints.weightx = 1;
-            constraints.weighty = 1;
-            constraints.fill = GridBagConstraints.BOTH;
-            panel.add(presentationPanel, constraints);
-            frame.getContentPane().add(panel);
-            frame.pack();
+            Main m = new Main(baseContext, viewer, db, decks, presentations);
 
-            Main m = new Main(baseContext, presentationPanel, db, decks, presentations);
-            m.joinPresentation(options.presentationName, options.joinTimeoutSeconds,
-                    options.currentSlideKey, options.slideRowFormat);
+            Presentation presentation = new Discovery(
+                    baseContext, options.mountPrefix, options.deckPrefix).getPresentation();
+            logger.info("Using presentation: " + presentation);
+            m.joinPresentation(presentation, options.joinTimeoutSeconds, options.slideRowFormat);
         }
     }
 
+    private static void startSwingServer() throws IOException, AlreadyBoundException {
+        logger.info("inside startSwingServer");
+        // Make command-Q do the same as closing the main frame (i.e. exit).
+        System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
+        System.setProperty("java.rmi.server.hostname", "localhost");
+
+        JFrame frame = new JFrame();
+        enableOSXFullscreen(frame);
+        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+        frame.setVisible(true);
+
+        JPanel panel = new JPanel(new GridBagLayout());
+        ScaleToFitJPanel presentationPanel = new ScaleToFitJPanel();
+        GridBagConstraints constraints = new GridBagConstraints();
+        constraints.weightx = 1;
+        constraints.weighty = 1;
+        constraints.fill = GridBagConstraints.BOTH;
+        panel.add(presentationPanel, constraints);
+        frame.getContentPane().add(panel);
+        frame.pack();
+
+        int port = pickUnusedPort();
+        LocateRegistry.createRegistry(port);
+        String name = "//localhost:" + port + "/" + UUID.randomUUID();
+        logger.info("swing server binding to " + name);
+        Naming.bind(name, new RemoteImageViewer(presentationPanel));
+
+        // The parent JVM will expect to read this value.
+        System.out.println(name);
+    }
+
     public Main(VContext context, ImageViewer viewer, Database db, Table decks,
                 Table presentations) throws VException {
         this.context = context;
@@ -164,11 +233,10 @@
         this.viewer = viewer;
     }
 
-    public void joinPresentation(final String syncgroupName,
+    public void joinPresentation(final Presentation presentation,
                                  int joinTimeoutSeconds,
-                                 String currentSlideKey,
                                  String slideRowFormat) throws VException {
-        Syncgroup syncgroup = db.getSyncgroup(syncgroupName);
+        Syncgroup syncgroup = db.getSyncgroup(presentation.getSyncgroupName());
         syncgroup.join(context.withTimeout(Duration.standardSeconds(joinTimeoutSeconds)),
                 new SyncgroupMemberInfo((byte) 1));
         for (String member : syncgroup.getMembers(context).keySet()) {
@@ -180,7 +248,10 @@
         }
         BatchDatabase batch = db.beginBatch(context, null);
         ResumeMarker marker = batch.getResumeMarker(context);
-        Stream<WatchChange> watchStream = db.watch(context, presentations.name(), currentSlideKey,
+        String rowKey = Joiner.on("/").join(presentation.getDeckId(), presentation
+                .getPresentationId(), "CurrentSlide");
+        logger.info("going to watch row key " + rowKey);
+        Stream<WatchChange> watchStream = db.watch(context, presentations.name(), rowKey,
                 marker);
 
         for (WatchChange w : watchStream) {
@@ -191,11 +262,10 @@
                         VCurrentSlide.class);
                 logger.info("Current slide: " + currentSlide);
                 // Read the corresponding slide.
-                VSlide slide = (VSlide) decks.getRow(String.format(slideRowFormat,
-                        currentSlide.getNum())).get(context, VSlide.class);
-                final BufferedImage image = ImageIO.read(
-                        new ByteArrayInputStream(slide.getThumbnail()));
-                viewer.setImage(image);
+                String row = String.format(slideRowFormat, presentation.getDeckId(),
+                        currentSlide.getNum());
+                VSlide slide = (VSlide) decks.getRow(row).get(context, VSlide.class);
+                viewer.setImage(slide.getThumbnail());
             } catch (IOException | VException e) {
                 logger.log(Level.WARNING, "exception encountered while handling change event", e);
             }
@@ -221,33 +291,36 @@
     }
 
     public static class Options {
+        @Parameter(names = {"-s", "--storageRootDir"},
+                description = "the root directory to use for local storage")
+        private String storageRootDir = Joiner.on(File.separator).join(
+                System.getProperty("java.io.tmpdir"), "syncslidepresenter-storage");
+
+        @Parameter(names = {"-d", "--deckPrefix"},
+                description = "mounttable prefix for live presentations.")
+        private String deckPrefix = "happyDeck";
+
+        @Parameter(names = {"--swing"})
+        private boolean swing = false;
+
         @Parameter(names = {"-m", "--mountPrefix"}, description = "the base path in the namespace"
                 + " where the syncbase service will be mounted")
         private String mountPrefix = "/192.168.86.254:8101";
 
-        @Parameter(names = {"-p", "--presentationName"},
-                description = "the presentation to watch")
-        private String presentationName = "/192.168.86.254:8101/990005300000537/%%sync/"
-                + "syncslides/deckId1/randomPresentationId1";
-
-        @Parameter(names = {"--mountTimeout"},
-                description = "the number of seconds to wait for the syncbase server to be created")
-        private int mountTimeoutSeconds = 10;
-
         @Parameter(names = {"--joinTimeout"},
                 description = "the number of seconds to wait to join the presentation")
         private int joinTimeoutSeconds = 10;
 
-        @Parameter(names = {"-k", "--currentSlideKey"},
-                description = "the row key containing the current slide")
-        private String currentSlideKey = "deckId1/randomPresentationId1/CurrentSlide";
-
         @Parameter(names = {"-f", "--slideRowFormat"},
                 description = "a pattern specifying where slide rows are found")
-        private String slideRowFormat = "deckId1/slides/%04d";
+        private String slideRowFormat = "%s/slides/%04d";
 
         @Parameter(names = {"-h", "--help"}, description = "display this help message", help = true)
         private boolean help = false;
+
+        @Parameter(names = {"--swingServerTimeoutSeconds"},
+                description = "the amount of time to wait for the swing server to start")
+        private int swingServerTimeoutSeconds = 10;
     }
 
     private static class ScaleToFitJPanel extends JPanel implements ImageViewer {
@@ -291,14 +364,169 @@
         }
 
         @Override
-        public void setImage(Image image) {
-            this.image = image;
-            setPreferredSize(new Dimension(image.getWidth(null), image.getHeight(null)));
-            repaint();
+        public void setImage(byte[] imageData) throws RemoteException {
+            try {
+                this.image = ImageIO.read(new ByteArrayInputStream(imageData));
+                setPreferredSize(new Dimension(image.getWidth(null), image.getHeight(null)));
+                repaint();
+            } catch (IOException e) {
+                throw new RemoteException("Could not set image", e);
+            }
         }
     }
 
-    private interface ImageViewer {
-        void setImage(Image image);
+    private interface ImageViewer extends Remote {
+        void setImage(byte[] imageData) throws RemoteException;
+    }
+
+    private static class RemoteImageViewer extends UnicastRemoteObject implements ImageViewer {
+        private final ImageViewer viewer;
+
+        protected RemoteImageViewer(ImageViewer viewer) throws RemoteException {
+            super();
+
+            this.viewer = viewer;
+        }
+
+        @Override
+        public void setImage(byte[] imageData) throws RemoteException {
+            viewer.setImage(imageData);
+        }
+    }
+
+    private static ImageViewer startAndConnectToSwingServer(int timeoutSeconds) throws IOException,
+            NotBoundException {
+        String javaHome = System.getProperty("java.home");
+        if (javaHome == null) {
+            throw new IllegalStateException("System property java.home is not set");
+        }
+        File javaBin = new File(javaHome, Joiner.on(File.separator).join("bin", "java"));
+        if (!javaBin.exists() || !javaBin.canExecute()) {
+            throw new IllegalStateException("Java binary " + javaBin
+                    + " does not exist or is not executable");
+        }
+        ProcessBuilder builder = new ProcessBuilder(javaBin.getAbsolutePath(),
+                "-classpath", System.getProperty("java.class.path"),
+                Main.class.getCanonicalName(), "--swing");
+        builder.redirectError(ProcessBuilder.Redirect.INHERIT);
+        logger.info("starting " + builder.command());
+        // The first line of output should be the address to which we should connect.
+        final Process process = builder.start();
+        logger.info("started " + process);
+
+        // Get a timeout timer going.
+        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
+        service.schedule(new Runnable() {
+            @Override
+            public void run() {
+                process.destroyForcibly();
+            }
+        }, timeoutSeconds, TimeUnit.SECONDS);
+        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+        String name = reader.readLine();
+        // If the timeout didn't fire, we don't want it killing our process now!
+        service.shutdownNow();
+        if (name != null) {
+            logger.info("vanadium listener sending slide images to " + name);
+            ImageViewer viewer = (ImageViewer) Naming.lookup(name);
+            // Hack: when the swing viewer exits, terminate our main app.
+            new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    do {
+                        try {
+                            process.waitFor();
+                            break;
+                        } catch (InterruptedException e) {
+                            // Ignored.
+                        }
+                    } while (true);
+                    System.exit(0);
+                }
+            }, "SwingServerTerminationWatcher").start();
+            return viewer;
+        } else {
+            throw new IOException("could not read slide name");
+        }
+    }
+
+    private static int pickUnusedPort() throws IOException {
+        for (int i = 0; i < MAX_PORT_PICKER_ATTEMPTS; i++) {
+            ServerSocket socket = new ServerSocket();
+            try {
+                socket.bind(new InetSocketAddress(0));
+                int port = ((InetSocketAddress) socket.getLocalSocketAddress()).getPort();
+                return port;
+            } catch (IOException e) {
+                // Couldn't bind, try again.
+            } finally {
+                try {
+                    socket.close();
+                } catch (IOException closeException) {
+                    logger.log(Level.WARNING, "Exception caught during close", closeException);
+                }
+            }
+        }
+
+        throw new IOException("Couldn't locate unused port");
+    }
+
+    private static class Discovery {
+        public static final Duration MT_TIMEOUT =
+                Duration.standardSeconds(10);
+
+        private final VContext context;
+        private final String mtName;
+        private final String deckPrefix;
+
+        public Discovery(VContext context, String mtName, String deckPrefix) {
+            this.context = context;
+            this.mtName = mtName;
+            this.deckPrefix = deckPrefix;
+        }
+
+        private Set<String> scan(String pattern) {
+            logger.info("Scanning MT " + mtName + " with pattern \"" + pattern + "\"");
+            try {
+                Namespace ns = V.getNamespace(context);
+                ns.setRoots(ImmutableList.of(mtName));
+                Set<String> result = new HashSet<String>();
+                VContext ctx = context.withTimeout(MT_TIMEOUT);
+                for (GlobReply reply : V.getNamespace(ctx).glob(ctx, pattern)) {
+                    if (reply instanceof GlobReply.Entry) {
+                        MountEntry entry = ((GlobReply.Entry) reply).getElem();
+                        result.add(entry.getName());
+                        for (MountedServer server : entry.getServers()) {
+                            logger.info("    endPoint: \"" + server.getServer() + "\"");
+                        }
+                    }
+                }
+                return result;
+            } catch (VException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+
+        private Presentation findPreso(String serviceName) {
+            V.getNamespace(context).flushCacheEntry(context, serviceName);
+            ParticipantClient client =
+                    ParticipantClientFactory.getParticipantClient(serviceName);
+            try {
+                Presentation p = client.get(context.withTimeout(
+                        Duration.standardSeconds(5)));
+                return p;
+            } catch (VException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+
+        public Presentation getPresentation() {
+            Set<String> services = scan(deckPrefix + "/*");
+            if (services.size() < 1) {
+                return null;
+            }
+            // Just grab the first one.
+            return findPreso(services.iterator().next());
+        }
     }
 }
diff --git a/projects/syncslides/app/src/main/AndroidManifest.xml b/projects/syncslides/app/src/main/AndroidManifest.xml
index 6030afe..0dff6f4 100644
--- a/projects/syncslides/app/src/main/AndroidManifest.xml
+++ b/projects/syncslides/app/src/main/AndroidManifest.xml
@@ -8,9 +8,13 @@
     <uses-permission android:name="android.permission.GET_ACCOUNTS" />
     <uses-permission android:name="android.permission.USE_CREDENTIALS" />
 
+    <!-- Read the user's name from contacts so we can display it with the question. -->
+    <uses-permission android:name="android.permission.READ_PROFILE" />
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+
     <application
         android:allowBackup="true"
-        android:icon="@mipmap/ic_launcher"
+        android:icon="@mipmap/vanadium_launcher"
         android:label="@string/app_name"
         android:theme="@style/AppTheme" >
         <activity
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserActivity.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserActivity.java
index 36304cb..c9a8d92 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserActivity.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserActivity.java
@@ -38,7 +38,6 @@
         V23Manager.Singleton.get().init(getApplicationContext(), this);
 
         mDB = DB.Singleton.get(getApplicationContext());
-        mDB.init(this);
 
         setContentView(R.layout.activity_deck_chooser);
 
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserFragment.java
index 6c25a93..0307bf4 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckChooserFragment.java
@@ -229,9 +229,11 @@
         Slide[] ret = new Slide[slides.length()];
         for (int i = 0; i < slides.length(); ++i) {
             JSONObject slide = slides.getJSONObject(i);
+            // TODO(jregan): Avoid the extra image conversion work.
+            // Reading into a bitmap, only to compress into bytes again.
             Bitmap thumb = readImage(dir, slide.getString("Thumb"));
             String note = slide.getString("Note");
-            ret[i] = new SlideImpl(thumb, note);
+            ret[i] = new SlideImpl(DeckFactory.makeBytesFromBitmap(thumb), note);
         }
         return ret;
     }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckListAdapter.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckListAdapter.java
index 75fbe0b..e46130d 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckListAdapter.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckListAdapter.java
@@ -17,6 +17,9 @@
 import android.widget.TextView;
 import android.widget.Toolbar;
 
+import java.util.Calendar;
+import java.util.Locale;
+
 import io.v.android.apps.syncslides.db.DB;
 import io.v.android.apps.syncslides.discovery.DiscoveryManager;
 import io.v.android.apps.syncslides.model.Deck;
@@ -32,7 +35,7 @@
         implements Listener {
     private static final String TAG = "DeckListAdapter";
     private DB.DBList<Deck> mDecks;
-    private DB.DBList<Deck> mLiveDecks;
+    private DiscoveryManager mLiveDecks;
     private DB mDB;
 
     public DeckListAdapter(DB db) {
@@ -87,7 +90,7 @@
     }
 
     @Override
-    public void onBindViewHolder(final ViewHolder holder, int deckIndex) {
+    public void onBindViewHolder(final ViewHolder holder, final int deckIndex) {
         final Deck deck;
         final Role role;
         // If the position is less than the number of live presentation decks, get deck card from
@@ -103,7 +106,10 @@
         } else {
             deck = mDecks.get(deckIndex - mLiveDecks.getItemCount());
             // TODO(afergan): Set actual date here.
-            holder.mToolbarLastOpened.setText("Opened on Oct 26, 2015");
+            final Calendar cal = Calendar.getInstance();
+            holder.mToolbarLastOpened.setText("Opened on "
+                    + cal.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.US) + " "
+                    + cal.get(Calendar.DAY_OF_MONTH) + ", " + cal.get(Calendar.YEAR));
             holder.mToolbarLastOpened.setVisibility(View.VISIBLE);
             holder.mToolbarLiveNow.setVisibility(View.GONE);
             holder.mToolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
@@ -132,28 +138,37 @@
                 Log.d(TAG, "Clicking through to PresentationActivity.");
                 final Context context = v.getContext();
                 if (role == Role.AUDIENCE) {
-                    //String deviceId = "355499060906851";  // Black Nexus 6.
-                    String deviceId = "355499060490393"; // Nexus 6 ZX1G22MLNL
+                    final Participant p = mLiveDecks.getParticipant(deckIndex);
+                    Log.d(TAG, "Joining:");
+                    Log.d(TAG, "  syncgroupName = " + p.getSyncgroupName());
+                    Log.d(TAG, " presentationId = " + p.getPresentationId());
                     DB.Singleton.get(context).joinPresentation(
-                            // TODO(kash): Use the real syncgroup name.
-                            "/192.168.86.254:8101/" + deviceId + "/%%sync/syncslides/" +
-                                    "deckId1/randomPresentationId1",
+                            p.getSyncgroupName(),
                             new DB.Callback<Void>() {
                                 @Override
                                 public void done(Void aVoid) {
-                                    showSlides(context, deck, role);
+                                    showSlides(
+                                            context, deck, role,
+                                            p.getSyncgroupName(),
+                                            p.getPresentationId());
                                 }
                             });
                 } else {
-                    showSlides(context, deck, role);
+                    showSlides(
+                            context, deck, role,
+                            Participant.Unknown.SYNCGROUP_NAME,
+                            Participant.Unknown.PRESENTATION_ID);
                 }
             }
         });
     }
 
-    private void showSlides(Context context, Deck deck, Role role) {
+    private void showSlides(Context context, Deck deck, Role role,
+                            String syncName, String presId) {
         Intent intent = new Intent(context, PresentationActivity.class);
         intent.putExtra(Deck.B.DECK_ID, deck.getId());
+        intent.putExtra(Participant.B.SYNCGROUP_NAME, syncName);
+        intent.putExtra(Participant.B.PRESENTATION_ID, presId);
         intent.putExtra(Participant.B.PARTICIPANT_ROLE, role);
         context.startActivity(intent);
     }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigateFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigateFragment.java
index 46dfc86..51a051c 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigateFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigateFragment.java
@@ -30,9 +30,12 @@
 import java.util.List;
 
 import io.v.android.apps.syncslides.db.DB;
+import io.v.android.apps.syncslides.db.VPerson;
+import io.v.android.apps.syncslides.misc.V23Manager;
 import io.v.android.apps.syncslides.model.Question;
 import io.v.android.apps.syncslides.model.Role;
 import io.v.android.apps.syncslides.model.Slide;
+import io.v.v23.security.Blessings;
 
 /**
  * Provides both the presenter and audience views for navigating through a presentation.
@@ -76,9 +79,10 @@
     private Role mRole;
     private List<Question> mQuestionList;
     private DB.QuestionListener mQuestionListener;
+    private boolean mDriving = false;
+    private DB.DriverListener mDriverListener;
     private boolean mEditing;
-    private int mQuestionerPosition;
-    private boolean mSynced;
+    private String mQuestionId;
     private DB.CurrentSlideListener mCurrentSlideListener;
     private DB mDB;
     private TextView mSlideNumText;
@@ -113,16 +117,11 @@
         mDeckId = args.getString(DECK_ID_KEY);
         mPresentationId = args.getString(PRESENTATION_ID_KEY);
         mLoadingCurrentSlide = -1;
-        mUserSlideNum = args.getInt(SLIDE_NUM_KEY);
+        mCurrentSlideNum = mUserSlideNum = args.getInt(SLIDE_NUM_KEY);
         mRole = (Role) args.get(ROLE_KEY);
-        if (((PresentationActivity) getActivity()).getSynced()) {
-            sync();
-        } else {
-            unsync();
-        }
         final View rootView = inflater.inflate(R.layout.fragment_navigate, container, false);
         mFabSync = rootView.findViewById(R.id.audience_sync_fab);
-        if (mSynced || mRole != Role.AUDIENCE) {
+        if (((PresentationActivity) getActivity()).getSynced() || mRole != Role.AUDIENCE) {
             mFabSync.setVisibility(View.INVISIBLE);
         } else {
             mFabSync.setVisibility(View.VISIBLE);
@@ -234,11 +233,15 @@
                 // the slides to load.
                 if (mLoadingCurrentSlide != -1) {
                     currentSlideChanged(mLoadingCurrentSlide);
-                } else {
-                    updateView();
                 }
+                updateView();
             }
         });
+        if (((PresentationActivity) getActivity()).getSynced()) {
+            sync();
+        } else {
+            unsync();
+        }
 
         return rootView;
     }
@@ -247,14 +250,27 @@
     public void onStart() {
         super.onStart();
         ((PresentationActivity) getActivity()).setUiImmersive(true);
+        mCurrentSlideListener = new DB.CurrentSlideListener() {
+            @Override
+            public void onChange(int slideNum) {
+                NavigateFragment.this.currentSlideChanged(slideNum);
+            }
+        };
+        mDB.addCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener);
         if (mRole == Role.AUDIENCE) {
-            mCurrentSlideListener = new DB.CurrentSlideListener() {
+            final Blessings blessings = V23Manager.Singleton.get().getBlessings();
+            mDriverListener = new DB.DriverListener() {
                 @Override
-                public void onChange(int slideNum) {
-                    NavigateFragment.this.currentSlideChanged(slideNum);
+                public void onChange(VPerson driver) {
+                    if (driver != null && driver.getBlessing().equals(blessings.toString())) {
+                        mDriving = true;
+                    } else {
+                        mDriving = false;
+                    }
+
                 }
             };
-            mDB.addCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener);
+            mDB.setDriverListener(mDeckId, mPresentationId, mDriverListener);
         }
         if (mRole == Role.PRESENTER) {
             mQuestionListener = new DB.QuestionListener() {
@@ -277,8 +293,9 @@
     public void onStop() {
         super.onStop();
         ((PresentationActivity) getActivity()).setUiImmersive(false);
+        mDB.removeCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener);
         if (mRole == Role.AUDIENCE) {
-            mDB.removeCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener);
+            mDB.removeDriverListener(mDeckId, mPresentationId, mDriverListener);
         }
         if (mRole == Role.PRESENTER) {
             mDB.removeQuestionListener(mDeckId, mPresentationId, mQuestionListener);
@@ -312,30 +329,37 @@
         return false;
     }
 
+    /**
+     * If the user is editing the text field and the text has changed, save the
+     * notes locally and in Syncbase.
+     */
     public void saveNotes() {
-        if (mEditing) {
-            Toast.makeText(getContext(), "Saving notes", Toast.LENGTH_SHORT).show();
-            mNotes.clearFocus();
-            InputMethodManager inputManager =
-                    (InputMethodManager) getContext().
-                            getSystemService(Context.INPUT_METHOD_SERVICE);
+        final String notes = mNotes.getText().toString();
+        if (mEditing && (!notes.equals(mSlides.get(mUserSlideNum).getNotes()))) {
+            toast("Saving notes");
+            mSlides.get(mUserSlideNum).setNotes(notes);
+            mDB.setSlideNotes(mDeckId, mUserSlideNum, notes);
+        }
+        mNotes.clearFocus();
+        InputMethodManager inputManager =
+                (InputMethodManager) getContext().
+                        getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (getActivity().getCurrentFocus() != null) {
             inputManager.hideSoftInputFromWindow(
                     getActivity().getCurrentFocus().getWindowToken(),
                     InputMethodManager.HIDE_NOT_ALWAYS);
-            ((PresentationActivity) getActivity()).setUiImmersive(true);
         }
+        ((PresentationActivity) getActivity()).setUiImmersive(true);
     }
 
     private void unsync() {
-        if (mRole == Role.AUDIENCE && mSynced) {
-            mSynced = false;
+        if (mRole == Role.AUDIENCE && ((PresentationActivity) getActivity()).getSynced()) {
             ((PresentationActivity) getActivity()).setUnsynced();
             mFabSync.setVisibility(View.VISIBLE);
         }
     }
 
     private void sync() {
-        mSynced = true;
         mUserSlideNum = mCurrentSlideNum;
         ((PresentationActivity) getActivity()).setSynced();
         updateView();
@@ -351,7 +375,7 @@
         }
         if (mUserSlideNum < mSlides.size() - 1) {
             mUserSlideNum++;
-            if (mRole == Role.PRESENTER) {
+            if (mRole == Role.PRESENTER || mDriving) {
                 mDB.setCurrentSlide(mDeckId, mPresentationId, mUserSlideNum);
             }
             updateView();
@@ -369,7 +393,7 @@
         }
         if (mUserSlideNum > 0) {
             mUserSlideNum--;
-            if (mRole == Role.PRESENTER) {
+            if (mRole == Role.PRESENTER || mDriving) {
                 mDB.setCurrentSlide(mDeckId, mPresentationId, mUserSlideNum);
             }
             updateView();
@@ -388,7 +412,7 @@
             return;
         }
         mCurrentSlideNum = slideNum;
-        if (mSynced) {
+        if (((PresentationActivity) getActivity()).getSynced()) {
             mUserSlideNum = slideNum;
             updateView();
         }
@@ -402,22 +426,15 @@
         DB db = DB.Singleton.get(getActivity().getApplicationContext());
         switch (mRole) {
             case AUDIENCE:
-                db.askQuestion(mDeckId, mPresentationId, "Audience member", "#1");
-                Toast toast = Toast.makeText(getActivity().getApplicationContext(),
-                        "You have been added to the Q&A queue.", Toast.LENGTH_LONG);
-                toast.show();
+                db.askQuestion(mDeckId, mPresentationId,
+                        SignInActivity.getUserName(getActivity()));
+                toast("You have been added to the Q&A queue.");
                 break;
             case PRESENTER:
                 if (mQuestionList == null || mQuestionList.size() == 0) {
                     break;
                 }
-                // TODO(kash): It would be better to pass the entire Question to the dialog.
-                String[] questioners = new String[mQuestionList.size()];
-                for (int i = 0; i < mQuestionList.size(); i++) {
-                    questioners[i] = mQuestionList.get(i).getFirstName() + " "
-                            + mQuestionList.get(i).getLastName();
-                }
-                DialogFragment dialog = QuestionDialogFragment.newInstance(questioners);
+                DialogFragment dialog = QuestionDialogFragment.newInstance(mQuestionList);
                 dialog.setTargetFragment(this, DIALOG_REQUEST_CODE);
                 dialog.show(getFragmentManager(), "QuestionerDialogFragment");
                 break;
@@ -455,10 +472,7 @@
     @Override
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         if (requestCode == DIALOG_REQUEST_CODE) {
-            // TODO(afergan): Using the position is insufficient if the list of questioners changes
-            // while the dialog is showing.
-            mQuestionerPosition = data.getIntExtra(
-                    QuestionDialogFragment.QUESTION_BUNDLE_KEY, 0);
+            mQuestionId = data.getStringExtra(QuestionDialogFragment.QUESTION_ID_KEY);
             handoffControl();
         }
     }
@@ -469,20 +483,33 @@
      * text.
      */
     private void handoffControl() {
-        //TODO(afergan): Change slide presenter to the audience member at mQuestionerPosition.
+        Question handoff = null;
+        for (Question question : mQuestionList) {
+            if (question.getId().equals(mQuestionId)) {
+                handoff = question;
+                break;
+            }
+        }
+        if (handoff == null) {
+            toast("No such question");
+            return;
+        }
+
+        sync();
+        mDB.handoffQuestion(mDeckId, mPresentationId, handoff.getId());
+
         View.OnClickListener snackbarClickListener = new NavigateClickListener() {
             @Override
             public void onClick(View v) {
                 super.onClick(v);
-                //TODO(afergan): End handoff, presenter regains control of presentation.
+                mDB.resumeControl(mDeckId, mPresentationId);
             }
         };
 
         ((PresentationActivity) getActivity()).setUiImmersive(true);
-        Snackbar snack = Snackbar.make(getView(), getResources().getString(
-                        R.string.handoff_message) + " "
-                        + mQuestionList.get(mQuestionerPosition).getFirstName() + " "
-                        + mQuestionList.get(mQuestionerPosition).getLastName(),
+        Snackbar snack = Snackbar.make(
+                getView(),
+                getResources().getString(R.string.handoff_message) + " " + handoff.getName(),
                 Snackbar.LENGTH_INDEFINITE)
                 .setAction(getResources().getString(R.string.end_handoff),
                         snackbarClickListener)
@@ -520,6 +547,10 @@
         }
     }
 
+    private void toast(String message) {
+        Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
+    }
+
     public class NavigateClickListener implements View.OnClickListener {
         @Override
         public void onClick(View v) {
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java
index 2f5d437..8e57ac4 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java
@@ -62,7 +62,8 @@
     private DrawerLayout mDrawerLayout;
     private ListView mDrawerListView;
     private View mFragmentContainerView;
-    private JSONObject mUserProfile;
+    private String mUserEmail;
+    private String mUserName;
 
     private int mCurrentSelectedPosition = 0;
     private boolean mFromSavedInstanceState;
@@ -79,12 +80,8 @@
         // drawer. See PREF_USER_LEARNED_DRAWER for details.
         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
         mUserLearnedDrawer = prefs.getBoolean(PREF_USER_LEARNED_DRAWER, false);
-        String userProfileJsonStr = prefs.getString(SignInActivity.PREF_USER_PROFILE_JSON, "");
-        try {
-            mUserProfile = new JSONObject(userProfileJsonStr);
-        } catch (JSONException e) {
-            Log.e(TAG, "Couldn't parse user profile data: " + userProfileJsonStr);
-        }
+        mUserEmail = SignInActivity.getUserEmail(getActivity());
+        mUserName = SignInActivity.getUserName(getActivity());
 
         if (savedInstanceState != null) {
             mCurrentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION);
@@ -113,30 +110,16 @@
                 selectItem(position);
             }
         });
-        mDrawerListView.setAdapter(new ArrayAdapter<JSONObject>(
+        mDrawerListView.setAdapter(new ArrayAdapter<String>(
                 getActionBar().getThemedContext(),
                 android.R.layout.simple_list_item_activated_2,
                 android.R.id.text1,
-                new JSONObject[]{mUserProfile}) {
+                new String[]{ mUserName }) {
             @Override
             public View getView(int position, View convertView, ViewGroup parent) {
-                JSONObject userProfile = getItem(position);
-                String name = "";
-                String email = "";
-                if (userProfile != null) {
-                    try {
-                        name = userProfile.getString("name");
-                        email = userProfile.getString("email");
-                    } catch (JSONException e) {
-                        Log.e(TAG, "Error reading from user profile: " + e.getMessage());
-                    }
-                }
-                if (name.isEmpty() && email.isEmpty()) {
-                    name = "USER1";
-                }
                 View view = super.getView(position, convertView, parent);
-                ((TextView) view.findViewById(android.R.id.text1)).setText(name);
-                ((TextView) view.findViewById(android.R.id.text2)).setText(email);
+                ((TextView) view.findViewById(android.R.id.text1)).setText(mUserName);
+                ((TextView) view.findViewById(android.R.id.text2)).setText(mUserEmail);
                 return view;
             }
         });
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationActivity.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationActivity.java
index 9ff1fa9..4d4f99c 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationActivity.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationActivity.java
@@ -6,13 +6,17 @@
 
 
 import android.content.Intent;
+import android.database.Cursor;
 import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.support.v4.app.FragmentTransaction;
 import android.support.v7.app.AppCompatActivity;
 import android.util.Log;
 import android.view.View;
 import android.widget.Toast;
 
 import io.v.android.apps.syncslides.db.DB;
+import io.v.android.apps.syncslides.db.VPerson;
 import io.v.android.apps.syncslides.discovery.ParticipantPeer;
 import io.v.android.apps.syncslides.misc.Config;
 import io.v.android.apps.syncslides.misc.V23Manager;
@@ -20,6 +24,7 @@
 import io.v.android.apps.syncslides.model.DeckFactory;
 import io.v.android.apps.syncslides.model.Participant;
 import io.v.android.apps.syncslides.model.Role;
+import io.v.v23.security.Blessings;
 
 public class PresentationActivity extends AppCompatActivity {
     private static final String TAG = "PresentationActivity";
@@ -33,8 +38,21 @@
      * of the activity.
      */
     private Role mRole;
-    // TODO(kash): Replace this with the presentation id.
-    private String mPresentationId = "randomPresentationId1";
+    /**
+     * Makes decks, handles deck conversions.
+     */
+    private DeckFactory mDeckFactory;
+
+    /**
+     * The presentation ID.
+     */
+    private String mPresentationId;
+
+    /**
+     * The syncgroup name.
+     */
+    private String mSyncgroupName;
+
     private boolean mSynced;
 
     /**
@@ -52,11 +70,10 @@
         super.onCreate(savedInstanceState);
         Log.d(TAG, "onCreate");
         // Initialize the DeckFactory.
-        DeckFactory.Singleton.get(getApplicationContext());
+        mDeckFactory = DeckFactory.Singleton.get(getApplicationContext());
         // Immediately initialize V23, possibly sending user to the
         // AccountManager to get blessings.
         V23Manager.Singleton.get().init(getApplicationContext(), this);
-        DB.Singleton.get(getApplicationContext()).init(this);
         setContentView(R.layout.activity_presentation);
 
         mShouldBeAdvertising = false;
@@ -67,11 +84,15 @@
             deckId = getIntent().getStringExtra(Deck.B.DECK_ID);
             mRole = (Role) getIntent().getSerializableExtra(
                     Participant.B.PARTICIPANT_ROLE);
+            mPresentationId = getIntent().getStringExtra(Participant.B.PRESENTATION_ID);
+            mSyncgroupName = getIntent().getStringExtra(Participant.B.SYNCGROUP_NAME);
             mSynced = true;
         } else {
             Log.d(TAG, "savedInstanceState is NOT null");
             mRole = (Role) savedInstanceState.get(Participant.B.PARTICIPANT_ROLE);
             deckId = savedInstanceState.getString(Deck.B.DECK_ID);
+            mPresentationId = savedInstanceState.getString(Participant.B.PRESENTATION_ID);
+            mSyncgroupName = savedInstanceState.getString(Participant.B.SYNCGROUP_NAME);
             mSynced = savedInstanceState.getBoolean(Participant.B.PARTICIPANT_SYNCED);
             mShouldBeAdvertising = savedInstanceState.getBoolean(Participant.B.PARTICIPANT_SHOULD_ADV);
             if (mShouldBeAdvertising) {
@@ -79,12 +100,34 @@
             }
         }
 
-        Log.d(TAG, "Role = " + mRole);
-        mDeck = DB.Singleton.get(getApplicationContext()).getDeck(deckId);
-        if (mDeck == null) {
-            throw new IllegalArgumentException("Unusable deckId: "+ deckId);
+        // TODO(kash): This is a total hack.  I thought that the deck would be
+        // loaded by this point, but we aren't actually guaranteed that.  After
+        // this is fixed, we can uncomment handleError in SyncbaseDB.getDeck().
+        while ((mDeck = DB.Singleton.get(getApplicationContext()).getDeck(deckId)) == null) {
+            Log.d(TAG, "Waiting for deck to load...");
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
         }
-        Log.d(TAG, "Using deck: " + mDeck);
+        if (mDeck == null) {
+            throw new IllegalArgumentException("Unusable deckId: " + deckId);
+        }
+        Log.d(TAG, "Unpacked state:");
+        Log.d(TAG, "  mShouldBeAdvertising = " + mShouldBeAdvertising);
+        Log.d(TAG, "                 mRole = " + mRole);
+        Log.d(TAG, "       mPresentationId = " + mPresentationId);
+        Log.d(TAG, "        mSyncgroupName = " + mSyncgroupName);
+        Log.d(TAG, "                  Deck = " + mDeck);
+        Log.d(TAG, "               mSynced = " + mSynced);
+
+        if (mRole.equals(Role.AUDIENCE)) {
+            if (mPresentationId.equals(Participant.Unknown.PRESENTATION_ID) ||
+                    mSyncgroupName.equals(Participant.Unknown.SYNCGROUP_NAME)) {
+                throw new IllegalArgumentException("Cannot be an audience.");
+            }
+        }
 
         // TODO(jregan): This appears to be an attempt to avoid fragment
         // re-inflation, possibly the right thing to do is move the code
@@ -124,6 +167,8 @@
         super.onSaveInstanceState(b);
         Log.d(TAG, "onSaveInstanceState");
         b.putSerializable(Participant.B.PARTICIPANT_ROLE, mRole);
+        b.putString(Participant.B.PRESENTATION_ID, mPresentationId);
+        b.putString(Participant.B.SYNCGROUP_NAME, mSyncgroupName);
         b.putString(Deck.B.DECK_ID, mDeck.getId());
         b.putBoolean(Participant.B.PARTICIPANT_SYNCED, mSynced);
         b.putBoolean(Participant.B.PARTICIPANT_SHOULD_ADV, mShouldBeAdvertising);
@@ -180,10 +225,20 @@
             return;
         }
         if (shouldUseV23()) {
-            V23Manager.Singleton.get().mount(
+            V23Manager v23Manager = V23Manager.Singleton.get();
+            Blessings blessings = v23Manager.getBlessings();
+            v23Manager.mount(
                     Config.MtDiscovery.makeMountName(mDeck),
-                    new ParticipantPeer.Server(mDeck));
-            Log.d(TAG, "MT advertising started.");
+                    new ParticipantPeer.Server(
+                            mDeckFactory.make(mDeck),
+                            mDeck.getId(),
+                            new VPerson(blessings.toString(), SignInActivity.getUserName(this)),
+                            mSyncgroupName,
+                            mPresentationId));
+            Log.d(TAG, "MT advertising started:");
+            Log.d(TAG, "    mSyncgroupName = " + mSyncgroupName);
+            Log.d(TAG, "   mPresentationId = " + mPresentationId);
+            Log.d(TAG, "             mDeck = " + mDeck);
         } else {
             Log.d(TAG, "No means to start advertising.");
         }
@@ -215,15 +270,17 @@
         DB db = DB.Singleton.get(getApplicationContext());
         db.createPresentation(mDeck.getId(), new DB.Callback<DB.CreatePresentationResult>() {
             @Override
-            public void done(DB.CreatePresentationResult startPresentationResult) {
+            public void done(DB.CreatePresentationResult result) {
                 Log.i(TAG, "Started presentation");
                 Toast.makeText(getApplicationContext(), "Started presentation",
                         Toast.LENGTH_SHORT).show();
+                mPresentationId = result.presentationId;
+                mSyncgroupName = result.syncgroupName;
                 startAdvertising();
+                navigateToSlide(0);
             }
         });
         mRole = Role.PRESENTER;
-        showNavigateFragmentWithBackStack(0);
     }
 
     /**
@@ -252,21 +309,26 @@
     }
 
     /**
-     * Shows the navigate fragment where the user can see the current slide and
-     * navigate to other components of the slide presentation. This version
-     * includes an add to the back stack so that the user can back out from the
-     * navigate fragment to slide list.
+     * Shows the navigate fragment where the user can see the given slide and
+     * navigate to other components of the slide presentation. If the role is
+     * not AUDIENCE, this version includes an add to the back stack so that
+     * the user can back out from the navigate fragment to slide list.
      *
-     * @param slideNum the number of the current slide to show in the fragment
+     * @param slideNum the number of the slide to show in the fragment
      */
-    public void showNavigateFragmentWithBackStack(int slideNum) {
+    public void navigateToSlide(int slideNum) {
+        // The user picked this specific slide.  Don't try to stay synced.
+        setUnsynced();
+
         NavigateFragment fragment = NavigateFragment.newInstance(
                 mDeck.getId(), mPresentationId, slideNum, mRole);
-        getSupportFragmentManager()
+        FragmentTransaction transaction = getSupportFragmentManager()
                 .beginTransaction()
-                .replace(R.id.fragment, fragment)
-                .addToBackStack("")
-                .commit();
+                .replace(R.id.fragment, fragment);
+        if (mRole != Role.AUDIENCE) {
+            transaction.addToBackStack("");
+        }
+        transaction.commit();
     }
 
     /**
@@ -300,5 +362,4 @@
     public void setUnsynced() {
         mSynced = false;
     }
-
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/QuestionDialogFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/QuestionDialogFragment.java
index 539e081..05239e1 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/QuestionDialogFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/QuestionDialogFragment.java
@@ -12,19 +12,21 @@
 import android.os.Bundle;
 import android.support.v4.app.DialogFragment;
 
+import java.util.List;
+
+import io.v.android.apps.syncslides.model.Question;
+
 /**
  * Dialog for the presenter to pick a questioner.
  */
 public class QuestionDialogFragment extends DialogFragment {
-    public static final String QUESTION_BUNDLE_KEY = "questioner_position";
+    public static final String QUESTION_ID_KEY = "question_id_key";
     private static final String QUESTIONER_LIST_KEY = "questioner_list_key";
 
-    private String[] mQuestionerList;
-
-    public static QuestionDialogFragment newInstance(String[] questionerList) {
+    public static QuestionDialogFragment newInstance(List<Question> questions) {
         QuestionDialogFragment fragment = new QuestionDialogFragment();
         Bundle args = new Bundle();
-        args.putStringArray(QUESTIONER_LIST_KEY, questionerList);
+        args.putParcelableArray(QUESTIONER_LIST_KEY, questions.toArray(new Question[0]));
         fragment.setArguments(args);
         return fragment;
     }
@@ -32,12 +34,16 @@
     @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
         Bundle args = getArguments();
-        mQuestionerList = args.getStringArray(QUESTIONER_LIST_KEY);
+        final Question[] questions = (Question[]) args.getParcelableArray(QUESTIONER_LIST_KEY);
         AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        String[] questioners = new String[questions.length];
+        for (int i = 0; i < questions.length; i++) {
+            questioners[i] = questions[i].getName();
+        }
         builder.setTitle(R.string.question_message)
-                .setItems(mQuestionerList, new DialogInterface.OnClickListener() {
+                .setItems(questioners, new DialogInterface.OnClickListener() {
                     public void onClick(DialogInterface dialog, int which) {
-                        sendResult(which);
+                        sendResult(questions[which].getId());
                     }
                 });
         return builder.create();
@@ -49,10 +55,10 @@
         ((PresentationActivity) getActivity()).setUiImmersive(true);
     }
 
-    // Send back the position of the questioner to the NavigateFragment.
-    private void sendResult(int position) {
+    // Send back the question's ID to the NavigateFragment.
+    private void sendResult(String id) {
         Intent intent = new Intent();
-        intent.putExtra(QUESTION_BUNDLE_KEY, position);
+        intent.putExtra(QUESTION_ID_KEY, id);
         getTargetFragment().onActivityResult(
                 getTargetRequestCode(), Activity.RESULT_OK, intent);
     }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java
index 3d225bc..c760e16 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java
@@ -14,10 +14,12 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.database.Cursor;
 import android.os.AsyncTask;
 import android.os.Handler;
 import android.os.Message;
 import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
 import android.support.v7.app.AppCompatActivity;
 import android.os.Bundle;
 import android.util.Log;
@@ -42,8 +44,10 @@
 public class SignInActivity extends AppCompatActivity {
     private static final String TAG = "SignInActivity";
 
-    public static final String PREF_USER_ACCOUNT_NAME = "user_account";
-    public static final String PREF_USER_PROFILE_JSON = "user_profile";
+    private static final String PREF_USER_ACCOUNT_NAME = "user_account";
+    private static final String PREF_USER_NAME_FROM_CONTACTS = "user_name_from_contacts";
+    private static final String PREF_USER_NAME_FROM_PROFILE = "user_name_from_profile";
+    private static final String PREF_USER_PROFILE_JSON = "user_profile";
 
     private static final int REQUEST_CODE_PICK_ACCOUNT = 1000;
     private static final int REQUEST_CODE_FETCH_USER_PROFILE_APPROVAL = 1001;
@@ -57,6 +61,43 @@
     private String mAccountName;
     private ProgressDialog mProgressDialog;
 
+    /**
+     * Returns the best-effort email of the signed-in user.
+     */
+    public static String getUserEmail(Context ctx) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+        return prefs.getString(PREF_USER_ACCOUNT_NAME, "");
+    }
+
+    /**
+     * Returns the best-effort full name of the signed-in user.
+     */
+    public static String getUserName(Context ctx) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+        // First try to read the user name we obtained from profile, as it's most accurate.
+        if (prefs.contains(PREF_USER_NAME_FROM_PROFILE)) {
+            return prefs.getString(PREF_USER_NAME_FROM_PROFILE, "Anonymous User");
+        }
+        return prefs.getString(PREF_USER_NAME_FROM_CONTACTS, "Anonymous User");
+    }
+
+    /**
+     * Returns the Google profile information of the signed-in user, or {@code null} if the
+     * profile information couldn't be retrieved.
+     */
+    public static JSONObject getUserProfile(Context ctx) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+        String userProfileJsonStr = prefs.getString(PREF_USER_PROFILE_JSON, "");
+        if (!userProfileJsonStr.isEmpty()) {
+            try {
+                return new JSONObject(userProfileJsonStr);
+            } catch (JSONException e) {
+                Log.e(TAG, "Couldn't parse user profile data: " + userProfileJsonStr);
+            }
+        }
+        return null;
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -107,7 +148,33 @@
         editor.putString(PREF_USER_ACCOUNT_NAME, accountName);
         editor.commit();
 
-        fetchUserProfile();
+        fetchUserNameFromContacts();
+
+        // NOTE(spetrovic): For demo purposes, fetching user profile is too risky as it requires
+        // internet access.  So we disable it for now.
+        //fetchUserProfile();
+        finishActivity();
+    }
+
+    private void fetchUserNameFromContacts() {
+        // Get the user's full name from Contacts.
+        Cursor c = getContentResolver().query(ContactsContract.Profile.CONTENT_URI,
+                null, null, null, null);
+        String[] columnNames = c.getColumnNames();
+        String userName = "Anonymous User";
+        while (c.moveToNext()) {
+            for (int j = 0; j < columnNames.length; j++) {
+                String columnName = columnNames[j];
+                if (!columnName.equals(ContactsContract.Contacts.DISPLAY_NAME)) {
+                    continue;
+                }
+                userName = c.getString(c.getColumnIndex(columnName));
+            }
+        }
+        c.close();
+        SharedPreferences.Editor editor = mPrefs.edit();
+        editor.putString(PREF_USER_NAME_FROM_CONTACTS, userName);
+        editor.commit();
     }
 
     private void fetchUserProfile() {
@@ -144,9 +211,15 @@
         if (userProfile != null) {
             SharedPreferences.Editor editor = mPrefs.edit();
             editor.putString(PREF_USER_PROFILE_JSON, userProfile.toString());
+            try {
+                if (userProfile.has("name") && !userProfile.getString("name").isEmpty()) {
+                    editor.putString(PREF_USER_NAME_FROM_PROFILE, userProfile.getString("name"));
+                }
+            } catch (JSONException e) {
+                Log.e(TAG, "Couldn't read user name from user profile: " + e.getMessage());
+            }
             editor.commit();
         }
-
         finishActivity();
     }
 
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListAdapter.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListAdapter.java
index aeb1ed6..9afae0d 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListAdapter.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListAdapter.java
@@ -36,7 +36,7 @@
             @Override
             public void onClick(View v) {
                 int position = mRecyclerView.getChildAdapterPosition(v);
-                ((PresentationActivity) v.getContext()).showNavigateFragmentWithBackStack(position);
+                ((PresentationActivity) v.getContext()).navigateToSlide(position);
             }
         });
         return new ViewHolder(v);
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/CurrentSlideWatcher.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/CurrentSlideWatcher.java
index a221d0a..beab3d1 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/CurrentSlideWatcher.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/CurrentSlideWatcher.java
@@ -101,7 +101,7 @@
         Log.i(TAG, "notifyListeners " + slide);
         mCurrentSlide = slide;
         for (DB.CurrentSlideListener listener : mListeners) {
-            Log.i(TAG, "notifying listner " + listener);
+            Log.i(TAG, "notifying listener " + listener);
             listener.onChange(mCurrentSlide.getNum());
         }
     }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DB.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DB.java
index 668b7e0..e84b42d 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DB.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DB.java
@@ -4,7 +4,6 @@
 
 package io.v.android.apps.syncslides.db;
 
-import android.app.Activity;
 import android.content.Context;
 
 import java.util.List;
@@ -39,14 +38,9 @@
     }
 
     /**
-     * Perform initialization steps.  This method must be called early in the
-     * lifetime of the activity.  As part of the initialization, it might send
-     * an intent to another activity.
-     *
-     * @param activity implements onActivityResult() to call into
-     *                 DB.onActivityResult.
+     * Perform initialization steps.
      */
-    void init(Activity activity);
+    void init();
 
     /**
      * Provides a list of elements via an API that fits well with
@@ -190,6 +184,15 @@
      */
     void setCurrentSlide(String deckId, String presentationId, int slideNum);
 
+    /**
+     * Saves notes for the specified slide.
+     *
+     * @param deckId     the deck being presented
+     * @param slideNum   the current slide number
+     * @param slideNotes the text of the slide's notes
+     */
+    void setSlideNotes(String deckId, int slideNum, String slideNotes);
+
     interface CurrentSlideListener {
         /**
          * Called whenever the current slide of a live presentation changes.
@@ -254,9 +257,52 @@
      *
      * @param deckId         the deck used in the presentation
      * @param presentationId the presentation identifier
-     * @param firstName      the user's first name
-     * @param lastName       the user's last name
+     * @param name           the user's full name
      */
-    void askQuestion(String deckId, String presentationId,
-                     String firstName, String lastName);
+    void askQuestion(String deckId, String presentationId, String name);
+
+    /**
+     * Give control of the presentation to the questioner.  Mark the question as answered.
+     *
+     * @param deckId         the deck used in the presentation
+     * @param presentationId the presentation identifier
+     * @param questionId     the question being answered
+     */
+    void handoffQuestion(String deckId, String presentationId, String questionId);
+
+    /**
+     * Take back control of the presentation from the questioner.
+     *
+     * @param deckId         the deck used in the presentation
+     * @param presentationId the presentation identifier
+     */
+    void resumeControl(String deckId, String presentationId);
+
+    interface DriverListener {
+        /**
+         * Called whenever the presentation's driver changes.
+         *
+         * @param driver the temporary driver of the presentation.  null means that the
+         *               original presenter has control.
+         */
+        void onChange(VPerson driver);
+    }
+
+    /**
+     * Set the listener for changes to the driver of the presentation.
+     *
+     * @param deckId         the deck used in the presentation
+     * @param presentationId the presentation identifier
+     * @param listener       notified of changes
+     */
+    void setDriverListener(String deckId, String presentationId, DriverListener listener);
+
+    /**
+     * Remove the listener that was previously passed to setDriverListener().
+     *
+     * @param deckId         the deck used in the presentation
+     * @param presentationId the presentation identifier
+     * @param listener       previously passed to setDriverListener()
+     */
+    void removeDriverListener(String deckId, String presentationId, DriverListener listener);
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DriverWatcher.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DriverWatcher.java
new file mode 100644
index 0000000..0e29070
--- /dev/null
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DriverWatcher.java
@@ -0,0 +1,103 @@
+// 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.android.apps.syncslides.db;
+
+import android.util.Log;
+
+import io.v.impl.google.naming.NamingUtil;
+import io.v.v23.syncbase.nosql.BatchDatabase;
+import io.v.v23.syncbase.nosql.ChangeType;
+import io.v.v23.syncbase.nosql.Stream;
+import io.v.v23.syncbase.nosql.Table;
+import io.v.v23.syncbase.nosql.WatchChange;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+
+/**
+ * Watches for a new driver (person controlling the slides) in a given presentation.
+ */
+class DriverWatcher {
+    private static final String TAG = "DriverWatcher";
+    private final WatcherState mState;
+    private final DB.DriverListener mListener;
+    private boolean mIsDiscarded;
+
+    /**
+     * Creates a new watcher for the presentation in state.
+     *
+     * @param state    objects necessary to do the watching
+     * @param listener notified whenever the driver changes
+     */
+    public DriverWatcher(WatcherState state, DB.DriverListener listener) {
+        mState = state;
+        mListener = listener;
+        mState.thread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                watch();
+            }
+        });
+        mState.thread.start();
+        mIsDiscarded = false;
+    }
+
+    /**
+     * Stops watching the presentation for a new driver.
+     */
+    public void discard() {
+        mState.vContext.cancel();  // this will cause the watcher thread to exit
+        mState.handler.removeCallbacksAndMessages(null);
+        // We've canceled all the pending callbacks, but the handler might be just about
+        // to execute put()/get() and those messages wouldn't get canceled.  So we mark
+        // the list as discarded and count on put()/get() checking for it.
+        mIsDiscarded = true;
+    }
+
+    private void watch() {
+        try {
+            String row = NamingUtil.join(mState.deckId, mState.presentationId);
+            Log.i(TAG, "Watching driver: " + row);
+            BatchDatabase batch = mState.db.beginBatch(mState.vContext, null);
+            Table presentations = batch.getTable(SyncbaseDB.PRESENTATIONS_TABLE);
+            if (presentations.getRow(row).exists(mState.vContext)) {
+                VPresentation presentation = (VPresentation) presentations.get(
+                        mState.vContext, row, VPresentation.class);
+                postInUiThread(presentation.getDriver().getElem());
+            }
+
+            Stream<WatchChange> watch = mState.db.watch(
+                    mState.vContext, SyncbaseDB.PRESENTATIONS_TABLE, row,
+                    batch.getResumeMarker(mState.vContext));
+            for (WatchChange change : watch) {
+                Log.i(TAG, "Found change " + change.getChangeType());
+                if (!change.getRowName().equals(row)) {
+                    continue;
+                }
+                if (change.getChangeType().equals(ChangeType.PUT_CHANGE)) {
+                    final VPresentation presentation = (VPresentation) VomUtil.decode(
+                            change.getVomValue(), VPresentation.class);
+                    postInUiThread(presentation.getDriver().getElem());
+                } else { // ChangeType.DELETE_CHANGE
+                    postInUiThread(null);
+                }
+            }
+        } catch (VException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void postInUiThread(final VPerson driver) {
+        mState.handler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (!mIsDiscarded) {
+                    mListener.onChange(driver);
+                }
+            }
+        });
+
+    }
+}
+
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/FakeDB.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/FakeDB.java
index cd41a7d..acf1be2 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/FakeDB.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/FakeDB.java
@@ -4,7 +4,6 @@
 
 package io.v.android.apps.syncslides.db;
 
-import android.app.Activity;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -105,9 +104,9 @@
         for (int i = 0; i < 5; i++) {
             Question question = new Question(
                     "question" + i,
-                    "Questioner",
-                    "#" + i,
-                    DateTime.now().minus(Period.minutes(random.nextInt(5))));
+                    "Questioner #" + i,
+                    DateTime.now().minus(Period.minutes(random.nextInt(5)))
+                            .toInstant().getMillis());
             mQuestions.add(question);
         }
         mQuestionListeners = Lists.newArrayList();
@@ -121,7 +120,7 @@
     }
 
     private static class FakeSlide implements Slide {
-        private final String mSlideNotes;
+        private String mSlideNotes;
         private final Bitmap mSlideImage;
 
         FakeSlide(Bitmap slideImage, String slideNotes) {
@@ -138,6 +137,11 @@
         public String getNotes() {
             return mSlideNotes;
         }
+
+        @Override
+        public void setNotes(String notes) {
+            mSlideNotes = notes;
+        }
     }
 
     private static class FakeDeckList implements DBList<Deck> {
@@ -217,17 +221,36 @@
 
 
     @Override
-    public void init(Activity activity) {
+    public void init() {
         // Nothing to do.
     }
 
     @Override
-    public void askQuestion(String deckId, String presentationId,
-                            String firstName, String lastName) {
+    public void askQuestion(String deckId, String presentationId, String name) {
         // Nothing to do.
     }
 
     @Override
+    public void handoffQuestion(String deckId, String presentationId, String questionId) {
+        // Not implemented.
+    }
+
+    @Override
+    public void resumeControl(String deckId, String presentationId) {
+        // Not implemented.
+    }
+
+    @Override
+    public void setDriverListener(String deckId, String presentationId, DriverListener listener) {
+        // Not implemented.
+    }
+
+    @Override
+    public void removeDriverListener(String deckId, String presentationId, DriverListener listener) {
+        // Not implemented.
+    }
+
+    @Override
     public DBList<Deck> getDecks() {
         return mDecks;
     }
@@ -262,6 +285,11 @@
     }
 
     @Override
+    public void setSlideNotes(String deckId, int slideNum, String slideNotes) {
+
+     }
+
+    @Override
     public void addCurrentSlideListener(String deckId, String presentationId,
                                         CurrentSlideListener listener) {
         mCurrentSlideListeners.add(listener);
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/QuestionWatcher.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/QuestionWatcher.java
index 7689969..2c82486 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/QuestionWatcher.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/QuestionWatcher.java
@@ -76,11 +76,13 @@
             Stream<KeyValue> stream = presentations.scan(mState.vContext, RowRange.prefix(prefix));
             for (KeyValue keyValue : stream) {
                 VQuestion value = (VQuestion) VomUtil.decode(keyValue.getValue(), VQuestion.class);
+                if (value.getAnswered()) {
+                    continue;
+                }
                 final Question question = new Question(
-                        keyValue.getKey(),
-                        value.getQuestioner().getFirstName(),
-                        value.getQuestioner().getLastName(),
-                        new DateTime(value.getTime()));
+                        lastPart(keyValue.getKey()),
+                        value.getQuestioner().getName(),
+                        value.getTime());
                 mState.handler.post(new Runnable() {
                     @Override
                     public void run() {
@@ -94,15 +96,25 @@
                     batch.getResumeMarker(mState.vContext));
             for (WatchChange change : watch) {
                 Log.i(TAG, "Found change " + change.getChangeType());
+                final String id = lastPart(change.getRowName());
                 if (change.getChangeType().equals(ChangeType.PUT_CHANGE)) {
                     VQuestion vQuestion = (VQuestion) VomUtil.decode(
                             change.getVomValue(), VQuestion.class);
                     Log.i(TAG, "Change " + change);
+                    if (vQuestion.getAnswered()) {
+                        mState.handler.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                Log.i(TAG, "Question was answered");
+                                delete(id);
+                            }
+                        });
+                        continue;
+                    }
                     final Question question = new Question(
-                            change.getRowName(),
-                            vQuestion.getQuestioner().getFirstName(),
-                            vQuestion.getQuestioner().getLastName(),
-                            new DateTime(vQuestion.getTime()));
+                            id,
+                            vQuestion.getQuestioner().getName(),
+                            vQuestion.getTime());
                     mState.handler.post(new Runnable() {
                         @Override
                         public void run() {
@@ -110,7 +122,6 @@
                         }
                     });
                 } else { // ChangeType.DELETE_CHANGE
-                    final String id = change.getRowName();
                     mState.handler.post(new Runnable() {
                         @Override
                         public void run() {
@@ -156,8 +167,17 @@
             if (mQuestions.get(i).getId().equals(id)) {
                 mQuestions.remove(i);
                 mListener.onChange(Lists.newArrayList(mQuestions));
+                return;
             }
         }
+        Log.i(TAG, "Could not find question " + id);
     }
 
+    /**
+     * Splits the name into parts and returns the last one.
+     */
+    private static String lastPart(String name) {
+        List<String> split = NamingUtil.split(name);
+        return split.get(split.size() - 1);
+    }
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java
index a599bdb..a8a8039 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java
@@ -4,7 +4,6 @@
 
 package io.v.android.apps.syncslides.db;
 
-import android.app.Activity;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -45,11 +44,13 @@
 import io.v.v23.context.VContext;
 import io.v.v23.rpc.Server;
 import io.v.v23.security.BlessingPattern;
+import io.v.v23.security.Blessings;
 import io.v.v23.security.access.AccessList;
 import io.v.v23.security.access.Constants;
 import io.v.v23.security.access.Permissions;
+import io.v.v23.services.syncbase.nosql.PrefixPermissions;
 import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
-import io.v.v23.services.syncbase.nosql.SyncgroupPrefix;
+import io.v.v23.services.syncbase.nosql.TableRow;
 import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
 import io.v.v23.services.watch.ResumeMarker;
 import io.v.v23.syncbase.Syncbase;
@@ -65,6 +66,7 @@
 import io.v.v23.syncbase.nosql.Table;
 import io.v.v23.syncbase.nosql.WatchChange;
 import io.v.v23.vdl.VdlAny;
+import io.v.v23.vdl.VdlOptional;
 import io.v.v23.verror.Errors;
 import io.v.v23.verror.VException;
 import io.v.v23.vom.VomUtil;
@@ -92,6 +94,7 @@
     private Table mPresentations;
     private final Map<String, CurrentSlideWatcher> mCurrentSlideWatchers;
     private final Map<String, QuestionWatcher> mQuestionWatchers;
+    private final Map<String, DriverWatcher> mDriverWatchers;
     private Server mSyncbaseServer;
     private final DeckFactory mDeckFactory;
 
@@ -99,25 +102,18 @@
         mContext = context;
         mCurrentSlideWatchers = Maps.newHashMap();
         mQuestionWatchers = Maps.newHashMap();
+        mDriverWatchers = Maps.newHashMap();
         mDeckFactory = DeckFactory.Singleton.get(context);
     }
 
     @Override
-    public void init(Activity activity) {
+    public void init() {
         Log.d(TAG, "init");
         if (mInitialized) {
             Log.d(TAG, "already initialized");
             return;
         }
         mHandler = new Handler(Looper.getMainLooper());
-        // TODO(kash): Set proper ACLs.
-        AccessList acl = new AccessList(
-                ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of());
-        mPermissions = new Permissions(ImmutableMap.of(
-                Constants.RESOLVE.getValue(), acl,
-                Constants.READ.getValue(), acl,
-                Constants.WRITE.getValue(), acl,
-                Constants.ADMIN.getValue(), acl));
 
         // If blessings aren't in place, the fragment that called this
         // initialization may continue to load and use DB, but nothing will
@@ -134,6 +130,19 @@
 
     // TODO(kash): Run this in an AsyncTask so it doesn't block the UI.
     private void setupSyncbase() {
+        Blessings blessings = V23Manager.Singleton.get().getBlessings();
+        AccessList everyoneAcl = new AccessList(
+                ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of());
+        AccessList justMeAcl = new AccessList(
+                ImmutableList.of(new BlessingPattern(blessings.toString())),
+                ImmutableList.<String>of());
+
+        mPermissions = new Permissions(ImmutableMap.of(
+                Constants.RESOLVE.getValue(), everyoneAcl,
+                Constants.READ.getValue(), justMeAcl,
+                Constants.WRITE.getValue(), justMeAcl,
+                Constants.ADMIN.getValue(), justMeAcl));
+
         // Prepare the syncbase storage directory.
         File storageDir = new File(mContext.getFilesDir(), "syncbase");
         storageDir.mkdirs();
@@ -180,7 +189,7 @@
             if (!mPresentations.exists(mVContext)) {
                 mPresentations.create(mVContext, mPermissions);
             }
-            importDecks();
+            //importDecks();
         } catch (VException e) {
             handleError("Couldn't setup syncbase service: " + e.getMessage());
             return;
@@ -201,10 +210,27 @@
 
     private void createPresentationRunnable(final String deckId,
                                             final Callback<CreatePresentationResult> callback) {
-        //final String presentationId = UUID.randomUUID().toString();
-        final String presentationId = "randomPresentationId1";
+        final String presentationId = UUID.randomUUID().toString();
         String prefix = NamingUtil.join(deckId, presentationId);
         try {
+            Blessings blessings = V23Manager.Singleton.get().getBlessings();
+            AccessList everyoneAcl = new AccessList(
+                    ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of());
+            AccessList justMeAcl = new AccessList(
+                    ImmutableList.of(new BlessingPattern(blessings.toString())),
+                    ImmutableList.<String>of());
+
+            Permissions groupReadPermissions = new Permissions(ImmutableMap.of(
+                    Constants.RESOLVE.getValue(), everyoneAcl,
+                    Constants.READ.getValue(), everyoneAcl,
+                    Constants.WRITE.getValue(), justMeAcl,
+                    Constants.ADMIN.getValue(), justMeAcl));
+            Permissions groupWritePermissions = new Permissions(ImmutableMap.of(
+                    Constants.RESOLVE.getValue(), everyoneAcl,
+                    Constants.READ.getValue(), everyoneAcl,
+                    Constants.WRITE.getValue(), justMeAcl,
+                    Constants.ADMIN.getValue(), justMeAcl));
+
             // Add rows to Presentations table.
             VPresentation presentation = new VPresentation();  // Empty for now.
             mPresentations.put(mVContext, prefix, presentation, VPresentation.class);
@@ -213,15 +239,19 @@
                     current, VCurrentSlide.class);
 
             mPresentations.setPrefixPermissions(mVContext, RowRange.prefix(prefix),
-                    mPermissions);
-            mDecks.setPrefixPermissions(mVContext, RowRange.prefix(deckId), mPermissions);
+                    groupReadPermissions);
+            // Anybody can add a question.
+            mPresentations.setPrefixPermissions(
+                    mVContext,
+                    RowRange.prefix(NamingUtil.join(prefix, QUESTIONS)),
+                    groupWritePermissions);
+            mDecks.setPrefixPermissions(mVContext, RowRange.prefix(deckId), groupReadPermissions);
 
             // Create the syncgroup.
             final String syncgroupName = NamingUtil.join(
                     mSyncbaseServer.getStatus().getMounts()[0].getName(),
                     "%%sync/syncslides",
                     prefix);
-            //final String syncgroupName = STATIC_SYNCGROUP;
             Log.i(TAG, "Creating syncgroup " + syncgroupName);
             Syncgroup syncgroup = mDB.getSyncgroup(syncgroupName);
             CancelableVContext context = mVContext.withTimeout(Duration.millis(5000));
@@ -230,11 +260,10 @@
                         context,
                         new SyncgroupSpec(
                                 SYNCGROUP_PRESENTATION_DESCRIPTION,
-                                // TODO(kash): Use real permissions.
-                                mPermissions,
+                                groupReadPermissions,
                                 Arrays.asList(
-                                        new SyncgroupPrefix(PRESENTATIONS_TABLE, prefix),
-                                        new SyncgroupPrefix(DECKS_TABLE, deckId)),
+                                        new TableRow(PRESENTATIONS_TABLE, prefix),
+                                        new TableRow(DECKS_TABLE, deckId)),
                                 Arrays.asList(V23Manager.syncName("sg")),
                                 false
                         ),
@@ -248,8 +277,6 @@
             }
             Log.i(TAG, "Finished creating syncgroup");
 
-            V23Manager.Singleton.get().scan("...");
-
             // TODO(kash): Create a syncgroup for Notes?  Not sure if we should do that
             // here or somewhere else.  We're not going to demo sync across a user's
             // devices right away, so we'll figure this out later.
@@ -283,11 +310,11 @@
                 try {
                     Log.i(TAG, "Joining: " + syncgroupName);
                     Syncgroup syncgroup = mDB.getSyncgroup(syncgroupName);
+                    Log.d(TAG, "syncgroup = " + syncgroup);
                     syncgroup.join(mVContext, new SyncgroupMemberInfo((byte) 1));
                     for (String member : syncgroup.getMembers(mVContext).keySet()) {
                         Log.i(TAG, "Member: " + member);
                     }
-                    V23Manager.Singleton.get().scan("...");
                     Handler handler = new Handler(Looper.getMainLooper());
                     handler.post(new Runnable() {
                         @Override
@@ -522,11 +549,8 @@
                         String key = (String) row.get(0).getElem();
                         Log.i(TAG, "Fetched slide " + key);
                         VSlide slide = (VSlide) row.get(1).getElem();
-                        VNote note = (VNote) table.get(mVContext, key, VNote.class);
-                        slides.add(new SlideImpl(
-                                BitmapFactory.decodeByteArray(
-                                        slide.getThumbnail(), 0, slide.getThumbnail().length),
-                                note.getText()));
+                        String note = notesForSlide(mVContext, table, key);
+                        slides.add(new SlideImpl(slide.getThumbnail(), note));
                     }
                     Handler handler = new Handler(Looper.getMainLooper());
                     handler.post(new Runnable() {
@@ -600,11 +624,8 @@
                     final String key = (String) row.get(0).getElem();
                     Log.i(TAG, "Fetched slide " + key);
                     VSlide slide = (VSlide) row.get(1).getElem();
-                    String notes = notesForSlide(notesTable, key);
-                    final SlideImpl newSlide = new SlideImpl(
-                            BitmapFactory.decodeByteArray(
-                                    slide.getThumbnail(), 0, slide.getThumbnail().length),
-                            notes);
+                    String notes = notesForSlide(mVContext, notesTable, key);
+                    final SlideImpl newSlide = new SlideImpl(slide.getThumbnail(), notes);
                     mHandler.post(new Runnable() {
                         @Override
                         public void run() {
@@ -648,11 +669,8 @@
                     } catch (VException e) {
                         Log.e(TAG, "Couldn't decode slide: " + e.toString());
                     }
-                    String notes = notesForSlide(notesTable, key);
-                    final SlideImpl slide = new SlideImpl(
-                            BitmapFactory.decodeByteArray(
-                                    vSlide.getThumbnail(), 0, vSlide.getThumbnail().length),
-                            notes);
+                    String notes = notesForSlide(mVContext, notesTable, key);
+                    final SlideImpl slide = new SlideImpl(vSlide.getThumbnail(), notes);
                     mHandler.post(new Runnable() {
                         @Override
                         public void run() {
@@ -747,12 +765,15 @@
             }
         }
 
-        private String notesForSlide(Table notesTable, String key) {
-            try {
-                return ((VNote) notesTable.get(mVContext, key, VNote.class)).getText();
-            } catch (VException e) {
-                return "";
-            }
+    }
+
+    private static String notesForSlide(VContext context, Table notesTable, String key) {
+        try {
+            return ((VNote) notesTable.get(context, key, VNote.class)).getText();
+        } catch (VException e) {
+            // TODO(kash): Should really differentiate between row not existing
+            // and some server error.
+            return "";
         }
     }
 
@@ -775,6 +796,27 @@
     }
 
     @Override
+    public void setSlideNotes(final String deckId, final int slideNum, final String slideNotes) {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    String rowKey = slideRowKey(deckId, slideNum);
+                    Log.i(TAG, "Saving notes " + rowKey + " with " + slideNotes);
+                    Table notesTable = mDB.getTable(NOTES_TABLE);
+                    notesTable.put(mVContext, rowKey, new VNote(slideNotes), VNote.class);
+                } catch (VException e) {
+                    handleError(e.toString());
+                }
+            }
+        }).start();
+    }
+
+    private String slideRowKey(String deckId, int slideNum) {
+        return NamingUtil.join(deckId, "slides", String.format("%04d", slideNum));
+    }
+
+    @Override
     public void addCurrentSlideListener(String deckId, String presentationId,
                                         CurrentSlideListener listener) {
         String key = NamingUtil.join(deckId, presentationId);
@@ -827,21 +869,47 @@
 
     @Override
     public void askQuestion(final String deckId, final String presentationId,
-                            final String firstName, final String lastName) {
+                            final String personName) {
+        final Blessings blessings = V23Manager.Singleton.get().getBlessings();
         new Thread(new Runnable() {
             @Override
             public void run() {
                 try {
                     String rowKey = NamingUtil.join(deckId, presentationId, QUESTIONS,
                             UUID.randomUUID().toString());
-                    Log.i(TAG, "Writing row " + rowKey + " with " + firstName + " " + lastName);
+                    Log.i(TAG, "Writing row " + rowKey + " with " + personName);
+                    BatchDatabase batch = mDB.beginBatch(mVContext, null);
+                    Table presentations = batch.getTable(PRESENTATIONS_TABLE);
                     VQuestion question = new VQuestion(
-                            new VPerson(firstName, lastName),
+                            new VPerson(blessings.toString(), personName),
                             System.currentTimeMillis(),
                             false // Not yet answered.
                     );
-                    mPresentations.put(mVContext, rowKey, question, VQuestion.class);
-                    // TODO(kash): Set the ACL so nobody else can modify the question.
+                    presentations.put(mVContext, rowKey, question, VQuestion.class);
+
+                    // There should be PrefixPermissions on
+                    //    <deckId>/<presentationId>/questions
+                    //    <deckId>/<presentationId>
+                    //    ""
+                    // We want to copy <deckId>/<presentationId> for our question but also
+                    // add ourselves.
+                    PrefixPermissions[] permissions =
+                            presentations.getPrefixPermissions(mVContext, rowKey);
+                    for (PrefixPermissions prefix : permissions) {
+                        Log.i(TAG, "Found permissions: " + prefix.toString());
+                    }
+                    if (permissions.length < 3) {
+                        handleError("Missing permissions for questions: "
+                                + Arrays.toString(permissions));
+                        batch.abort(mVContext);
+                        return;
+                    }
+                    Permissions perms = permissions[1].getPerms();
+                    perms.get(Constants.WRITE.getValue()).getIn().add(
+                            new BlessingPattern(blessings.toString()));
+                    presentations.setPrefixPermissions(mVContext, RowRange.prefix(rowKey), perms);
+
+                    batch.commit(mVContext);
                 } catch (VException e) {
                     handleError(e.toString());
                 }
@@ -849,13 +917,142 @@
         }).start();
     }
 
-    private void handleError(String msg) {
+    @Override
+    public void handoffQuestion(final String deckId, final String presentationId,
+                                final String questionId) {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    BatchDatabase batch = mDB.beginBatch(mVContext, null);
+                    Table presentations = batch.getTable(PRESENTATIONS_TABLE);
+
+                    // Mark the question as answered so it disappears from the list.
+                    String questionKey =
+                            NamingUtil.join(deckId, presentationId, QUESTIONS, questionId);
+                    VQuestion question =
+                            (VQuestion) presentations.get(mVContext, questionKey, VQuestion.class);
+                    if (question.getAnswered()) {
+                        // Already answered.  Nothing to do.
+                        batch.abort(mVContext);
+                        return;
+                    }
+                    question.setAnswered(true);
+                    presentations.put(mVContext, questionKey, question, VQuestion.class);
+
+                    // Set the questioner as the driver of the presentation.
+                    String presentationKey = NamingUtil.join(deckId, presentationId);
+                    VPresentation presentation = new VPresentation();
+                    presentation.setDriver(VdlOptional.of(question.getQuestioner()));
+                    presentations.put(mVContext, presentationKey, presentation,
+                            VPresentation.class);
+
+                    // Give the questioner permission to set the current slide.
+                    String currentSlideKey = NamingUtil.join(deckId, presentationId, CURRENT_SLIDE);
+                    Blessings blessings = V23Manager.Singleton.get().getBlessings();
+                    AccessList everyoneAcl = new AccessList(
+                            ImmutableList.of(new BlessingPattern("...")),
+                            ImmutableList.<String>of());
+                    AccessList questionerAcl = new AccessList(
+                            ImmutableList.of(
+                                    new BlessingPattern(question.getQuestioner().getBlessing())),
+                            ImmutableList.<String>of());
+                    AccessList justMeAcl = new AccessList(
+                            ImmutableList.of(new BlessingPattern(blessings.toString())),
+                            ImmutableList.<String>of());
+                    Permissions permissions = new Permissions(ImmutableMap.of(
+                            Constants.RESOLVE.getValue(), everyoneAcl,
+                            Constants.READ.getValue(), everyoneAcl,
+                            Constants.WRITE.getValue(), questionerAcl,
+                            Constants.ADMIN.getValue(), justMeAcl));
+                    presentations.setPrefixPermissions(mVContext, RowRange.prefix(currentSlideKey),
+                            permissions);
+
+                    batch.commit(mVContext);
+                } catch (VException e) {
+                    handleError(e.toString());
+                }
+            }
+        }).start();
+    }
+
+    @Override
+    public void resumeControl(final String deckId, final String presentationId) {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    BatchDatabase batch = mDB.beginBatch(mVContext, null);
+                    Table presentations = batch.getTable(PRESENTATIONS_TABLE);
+
+                    // Clear the driver of the presentation.
+                    String presentationKey = NamingUtil.join(deckId, presentationId);
+                    VPresentation presentation = new VPresentation();
+                    presentations.put(mVContext, presentationKey, presentation,
+                            VPresentation.class);
+
+                    // Give the questioner permission to set the current slide.
+                    String currentSlideKey = NamingUtil.join(deckId, presentationId, CURRENT_SLIDE);
+                    Blessings blessings = V23Manager.Singleton.get().getBlessings();
+                    AccessList everyoneAcl = new AccessList(
+                            ImmutableList.of(new BlessingPattern("...")),
+                            ImmutableList.<String>of());
+                    AccessList justMeAcl = new AccessList(
+                            ImmutableList.of(new BlessingPattern(blessings.toString())),
+                            ImmutableList.<String>of());
+                    Permissions permissions = new Permissions(ImmutableMap.of(
+                            Constants.RESOLVE.getValue(), everyoneAcl,
+                            Constants.READ.getValue(), everyoneAcl,
+                            Constants.WRITE.getValue(), justMeAcl,
+                            Constants.ADMIN.getValue(), justMeAcl));
+                    presentations.setPrefixPermissions(mVContext, RowRange.prefix(currentSlideKey),
+                            permissions);
+
+                    batch.commit(mVContext);
+                } catch (VException e) {
+                    handleError(e.toString());
+                }
+            }
+        }).start();
+    }
+
+    @Override
+    public void setDriverListener(String deckId, String presentationId, DriverListener listener) {
+        String key = NamingUtil.join(deckId, presentationId);
+        DriverWatcher oldWatcher = mDriverWatchers.get(key);
+        if (oldWatcher != null) {
+            oldWatcher.discard();
+        }
+        DriverWatcher watcher = new DriverWatcher(
+                new WatcherState(mVContext, mDB, deckId, presentationId),
+                listener);
+        mDriverWatchers.put(key, watcher);
+    }
+
+    @Override
+    public void removeDriverListener(String deckId, String presentationId,
+                                     DriverListener listener) {
+        String key = NamingUtil.join(deckId, presentationId);
+        DriverWatcher oldWatcher = mDriverWatchers.get(key);
+        if (oldWatcher != null) {
+            mDriverWatchers.remove(oldWatcher);
+            oldWatcher.discard();
+        }
+    }
+
+
+    private void handleError(final String msg) {
         Log.e(TAG, msg);
-        Toast.makeText(mContext, msg, Toast.LENGTH_LONG).show();
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                Toast.makeText(mContext, msg, Toast.LENGTH_LONG).show();
+            }
+        });
     }
 
     private void importDecks() {
-        importDeckFromResources("deckId1", "Car Business", R.drawable.thumb_deck1);
+        importDeckFromResources(UUID.randomUUID().toString(), "Car Business", R.drawable.thumb_deck1);
         // These slow down app startup, so skip these for now.
 //        importDeckFromResources("deckId2", "Baku Discovery", R.drawable.thumb_deck2);
 //        importDeckFromResources("deckId3", "Vanadium", R.drawable.thumb_deck3);
@@ -942,7 +1139,7 @@
     }
 
     private void putSlide(String prefix, int idx, byte[] thumbData, String note) throws VException {
-        String key = NamingUtil.join(prefix, "slides", String.format("%04d", idx));
+        String key = slideRowKey(prefix, idx);
         Log.i(TAG, "Adding slide " + key);
         if (!mDecks.getRow(key).exists(mVContext)) {
             mDecks.put(
@@ -966,7 +1163,9 @@
         try {
             vDeck = (VDeck) mDecks.get(mVContext, deckId, VDeck.class);
         } catch (VException e) {
-            handleError(e.toString());
+            // TODO(kash): Uncomment this when PresentationActivity.onCreate no
+            // longer needs to loop on getDeck(), waiting for it to return non-null.
+            //handleError(e.toString());
         }
         if (vDeck != null) {
             return mDeckFactory.make(vDeck, deckId);
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/schema.vdl b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/schema.vdl
index f6c0077..6e3ac4b 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/schema.vdl
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/schema.vdl
@@ -33,8 +33,9 @@
 
 // Person represents either an audience member or the presenter.
 type VPerson struct{
-    FirstName string
-    LastName string
+    Blessing string
+    // The person's full name.
+    Name string
 }
 
 // CurrentSlide contains state for the live presentation.  It is separate from the
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/DiscoveryManager.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/DiscoveryManager.java
index 57321c8..4aed438 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/DiscoveryManager.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/DiscoveryManager.java
@@ -109,9 +109,13 @@
         return mParticipants.size();
     }
 
+    public Participant getParticipant(int i) {
+        return mParticipants.get(i);
+    }
+
     @Override
     public Deck get(int i) {
-        return mParticipants.get(i).getDeck();
+        return getParticipant(i).getDeck();
     }
 
     @Override
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantPeer.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantPeer.java
index ff7e70c..b491a99 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantPeer.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantPeer.java
@@ -8,12 +8,15 @@
 import android.util.Log;
 
 import org.joda.time.DateTime;
+import org.joda.time.Duration;
 import org.joda.time.format.DateTimeFormat;
 import org.joda.time.format.DateTimeFormatter;
 
 import java.io.ByteArrayOutputStream;
 
 import io.v.android.apps.syncslides.db.VDeck;
+import io.v.android.apps.syncslides.db.VPerson;
+import io.v.android.apps.syncslides.discovery.Presentation;
 import io.v.android.apps.syncslides.misc.V23Manager;
 import io.v.android.apps.syncslides.model.Deck;
 import io.v.android.apps.syncslides.model.DeckFactory;
@@ -43,19 +46,22 @@
             DateTimeFormat.forPattern("hh_mm_ss_SSSS");
     // V23 name of the V23 service representing the participant.
     private final String mServiceName;
-    // Visible name of human presenter.
-    // TODO(jregan): Switch to VPerson or the model equivalent.
-    private String mUserName;
+    // Person presenting.
+    private VPerson mUser;
     // When did we last grab data from the endPoint?
     private DateTime mRefreshTime;
     // Deck the user is presenting.  Can only present one at a time.
     private Deck mDeck;
+    // The presentation ID.
+    private String mPresentationId;
+    // The syncgroup name.
+    private String mSyncgroupName;
     // Used to make decks after RPCs.
     private final DeckFactory mDeckFactory;
 
     private ParticipantPeer(
-            String userName, Deck deck, String serviceName, DeckFactory deckFactory) {
-        mUserName = userName;
+            VPerson user, Deck deck, String serviceName, DeckFactory deckFactory) {
+        mUser = user;
         mDeck = deck;
         mServiceName = serviceName;
         mDeckFactory = deckFactory;
@@ -63,11 +69,13 @@
 
     public static ParticipantPeer makeWithServiceName(
             String serviceName, DeckFactory deckFactory) {
-        return new ParticipantPeer(Unknown.USER_NAME, null, serviceName, deckFactory);
+        return new ParticipantPeer(
+                // The person and deck are obtained from the given service.
+                null, null, serviceName, deckFactory);
     }
 
-    public static ParticipantPeer makeWithKnownDeck(String userName, Deck deck) {
-        return new ParticipantPeer(userName, deck, Unknown.SERVER_NAME, null);
+    public static ParticipantPeer makeWithKnownDeck(VPerson user, Deck deck) {
+        return new ParticipantPeer(user, deck, Unknown.SERVER_NAME, null);
     }
 
     @Override
@@ -77,8 +85,8 @@
     }
 
     @Override
-    public String getUserName() {
-        return mUserName;
+    public VPerson getUser() {
+        return mUser;
     }
 
     @Override
@@ -87,8 +95,18 @@
     }
 
     @Override
+    public String getPresentationId() {
+        return mPresentationId;
+    }
+
+    @Override
+    public String getSyncgroupName() {
+        return mSyncgroupName;
+    }
+
+    @Override
     public String toString() {
-        return "[userName=\"" + mUserName +
+        return "[user=\"" + mUser +
                 "\", deck=" + mDeck +
                 ", time=" + getStringRefreshtime() + "]";
     }
@@ -132,20 +150,18 @@
                 ParticipantClientFactory.getParticipantClient(mServiceName);
         Log.d(TAG, "Got client = " + client.toString());
         try {
-            Log.d(TAG, "Calling get...");
-            VDeck vDeck = client.get(
-                    V23Manager.Singleton.get().getVContext());
-            Log.d(TAG, "Back with vDeck = "+ vDeck.toString());
-            byte[] bytes = vDeck.getThumbnail();
-            if (bytes != null && bytes.length > 0) {
-                Log.d(TAG, " Seem to have a thumb");
-            } else {
-                Log.d(TAG, " No thumb");
-            }
-            Deck newDeck = mDeckFactory.make(vDeck, "whatShouldTheIdBe");
+            Log.d(TAG, "Starting RPC to service \"" + mServiceName + "\"...");
+            Presentation p = client.get(
+                    V23Manager.Singleton.get().getVContext().withTimeout(
+                            Duration.standardSeconds(5)));
+            mDeck = mDeckFactory.make(p.getDeck(), p.getDeckId());
+            mSyncgroupName = p.getSyncgroupName();
+            mPresentationId = p.getPresentationId();
             mRefreshTime = DateTime.now();
-            mDeck = newDeck;
-            Log.d(TAG, "  Got deck = " + mDeck);
+            Log.d(TAG, "   Discovered:");
+            Log.d(TAG, "               mDeck = " + mDeck);
+            Log.d(TAG, "      mSyncgroupName = " + mSyncgroupName);
+            Log.d(TAG, "     mPresentationId = " + mPresentationId);
             return true;
         } catch (VException e) {
             Log.d(TAG, "RPC failed, leaving current deck in place.");
@@ -164,27 +180,24 @@
      */
     public static class Server implements ParticipantServer {
         private static final String TAG = "ParticipantServer";
-        private final Deck mDeck;
+        private final Presentation mPresentation;
 
-        public Server(Deck d) {
-            mDeck = d;
+        public Server(
+                VDeck d, String deckId, VPerson user,
+                String syncGroupName, String presentationId) {
+            Presentation p = new Presentation();
+            p.setDeck(d);
+            p.setDeckId(deckId);
+            p.setPerson(user);
+            p.setSyncgroupName(syncGroupName);
+            p.setPresentationId(presentationId);
+            mPresentation = p;
         }
 
-        public VDeck get(VContext ctx, ServerCall call)
+        public Presentation get(VContext ctx, ServerCall call)
                 throws VException {
             Log.d(TAG, "Responding to Get RPC.");
-            Log.d(TAG, "  Sending mDeck = " + mDeck);
-            VDeck d = new VDeck();
-            d.setTitle(mDeck.getTitle());
-            if (mDeck.getThumb() == null) {
-                Log.d(TAG, "  The response deck has no thumb.");
-            } else {
-                ByteArrayOutputStream stream = new ByteArrayOutputStream();
-                Bitmap bitmap = mDeck.getThumb();
-                bitmap.compress(Bitmap.CompressFormat.JPEG, 60, stream);
-                d.setThumbnail(stream.toByteArray());
-            }
-            return d;
+            return mPresentation;
         }
     }
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerFake.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerFake.java
index 7db977e..0e94f5b 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerFake.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerFake.java
@@ -9,6 +9,7 @@
 import java.util.HashSet;
 import java.util.Set;
 
+import io.v.android.apps.syncslides.db.VPerson;
 import io.v.android.apps.syncslides.model.DeckFactory;
 import io.v.android.apps.syncslides.model.Participant;
 
@@ -29,16 +30,22 @@
         if (mCounter >= 2 && mCounter <= 8) {
             participants.add(
                     ParticipantPeer.makeWithKnownDeck(
-                            "Alice",
+                            new VPerson(
+                                    "dev.v.io/u/liz.lemon@gmail.com/android/io.v" +
+                                            ".android.apps.syncslides",
+                                    "Liz Lemon"),
                             mDeckFactory.make(
                                     "Kale - Just eat it.",
                                     "deckByAlice")));
         }
-        // Bob has less to say than Alice.
+        // Jack has less to say than Liz.
         if (mCounter >= 4 && mCounter <= 6) {
             participants.add(
                     ParticipantPeer.makeWithKnownDeck(
-                            "Bob",
+                            new VPerson(
+                                    "dev.v.io/u/jack.donaghy@gmail.com/android/io.v" +
+                                            ".android.apps.syncslides",
+                                    "Jack Donaghy"),
                             mDeckFactory.make(
                                     "Java - Object deluge.",
                                     "deckByBob")));
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/participant_service.vdl b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/participant_service.vdl
index 6b011c9..dd41728 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/participant_service.vdl
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/participant_service.vdl
@@ -8,6 +8,15 @@
     "io/v/android/apps/syncslides/db"
 )
 
+// Who's presenting, and what are they presenting?
+type Presentation struct {
+    Deck db.VDeck
+    Person db.VPerson
+    DeckId string
+    PresentationId string
+    SyncgroupName string
+}
+
 type Participant interface {
-	Get() (db.VDeck | error)
+	Get() (Presentation | error)
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/Config.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/Config.java
index ee26d1f..465d74b 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/Config.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/Config.java
@@ -29,7 +29,7 @@
         /**
          * If true, enable use of syncbase as the DB, else use a fake.
          */
-        public static final boolean ENABLE = false;
+        public static final boolean ENABLE = true;
     }
 
     public static class MtDiscovery {
@@ -45,7 +45,7 @@
          * Every v23 service will be mounted in the namespace with a name
          * prefixed by this.
          */
-        private static String LIVE_PRESENTATION_PREFIX = "liveDeck";
+        private static String LIVE_PRESENTATION_PREFIX = "happyDeck";
 
         /**
          * TODO(jregan): Assure legal mount name (remove blanks and such).
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
index 734d79e..cd51312 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
@@ -23,6 +23,7 @@
 import java.util.concurrent.Executors;
 
 import io.v.android.apps.syncslides.SignInActivity;
+import io.v.android.apps.syncslides.db.DB;
 import io.v.android.libs.security.BlessingsManager;
 import io.v.android.v23.V;
 import io.v.android.v23.services.blessing.BlessingCreationException;
@@ -100,6 +101,7 @@
             Log.d(TAG, "unpacking blessing");
             Blessings blessings = unpackBlessings(androidCtx, resultCode, data);
             Singleton.get().configurePrincipal(blessings);
+            DB.Singleton.get(androidCtx).init();
         } catch (BlessingCreationException e) {
             throw new IllegalStateException(e);
         } catch (VException e) {
@@ -176,14 +178,14 @@
                         "Cannot get blessings without an activity to return to.");
             }
             // Get the signed-in user's email to generate the blessings from.
-            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(androidCtx);
-            String userEmail = prefs.getString(SignInActivity.PREF_USER_ACCOUNT_NAME, "");
+            String userEmail = SignInActivity.getUserEmail(androidCtx);
             activity.startActivityForResult(
                     BlessingService.newBlessingIntent(androidCtx, userEmail),
                     BLESSING_REQUEST);
             return;
         }
-        asyncConfigurePrincipal(blessings);
+        configurePrincipal(blessings);
+        DB.Singleton.get(mAndroidCtx).init();
     }
 
     public void flushServerFromCache(String name) {
@@ -206,6 +208,13 @@
         return mBlessings != null;
     }
 
+    /**
+     * Returns the blessings for this process.
+     */
+    public Blessings getBlessings() {
+        return mBlessings;
+    }
+
     private void configurePrincipal(final Blessings blessings) {
         Log.d(TAG, "configurePrincipal: blessings=" +
                 (blessings == null ? "null" : blessings.toString()));
@@ -224,15 +233,6 @@
                 (mBlessings == null ? "NONE!" : mBlessings.toString()));
     }
 
-    private void asyncConfigurePrincipal(final Blessings blessings) {
-        mExecutor.execute(new Runnable() {
-            @Override
-            public void run() {
-                configurePrincipal(blessings);
-            }
-        });
-    }
-
     public void shutdown(Behavior behavior) {
         Log.d(TAG, "Shutdown");
         if (mAndroidCtx == null) {
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckFactory.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckFactory.java
index 1dec64c..4e8d0f2 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckFactory.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckFactory.java
@@ -7,8 +7,11 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.util.Base64;
 import android.util.Log;
 
+import java.io.ByteArrayOutputStream;
+
 import io.v.android.apps.syncslides.R;
 import io.v.android.apps.syncslides.db.VDeck;
 
@@ -58,6 +61,13 @@
                 id);
     }
 
+    public VDeck make(Deck deck) {
+        VDeck vd = new VDeck();
+        vd.setTitle(deck.getTitle());
+        vd.setThumbnail(makeBytesFromBitmap(deck.getThumb()));
+        return vd;
+    }
+
     public Deck make(String title, Bitmap thumb, String id) {
         title = (title == null || title.isEmpty()) ? Unknown.TITLE : title;
         thumb = (thumb == null) ? mDefaultThumb : thumb;
@@ -65,6 +75,16 @@
         return new DeckImpl(title, thumb, id);
     }
 
+    public static byte[] makeBytesFromBitmap(Bitmap thumb) {
+        if (thumb == null) {
+            return new byte[0];
+        }
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        thumb.compress(
+                Bitmap.CompressFormat.JPEG, 60 /* quality */, stream);
+        return stream.toByteArray();
+    }
+
     public static class Singleton {
         private static volatile DeckFactory instance;
 
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Participant.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Participant.java
index fe83e5c..c59f92e 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Participant.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Participant.java
@@ -4,6 +4,8 @@
 
 package io.v.android.apps.syncslides.model;
 
+import io.v.android.apps.syncslides.db.VPerson;
+
 /**
  * Someone taking part in a presentation.
  *
@@ -12,14 +14,18 @@
  */
 public interface Participant {
 
-    // Name of the user participating, intended to be visible to others. This
-    // can be a colloquial name as opposed to a 'real' name or email address
-    // extracted from a device or blessing.
-    String getUserName();
+    // User presenting.
+    VPerson getUser();
 
     // Deck the user may be presenting.
     Deck getDeck();
 
+    // The presentation ID.
+    String getPresentationId();
+
+    // The syncgroup name.
+    String getSyncgroupName();
+
     // Name of a service with participant information.
     String getServiceName();
 
@@ -40,8 +46,15 @@
      * Keys for Bundle fields.
      */
     class B {
+        public static final String SYNCGROUP_NAME = "participant_syncgroup_name";
+        public static final String PRESENTATION_ID = "participant_pres_id";
         public static final String PARTICIPANT_ROLE = "participant_role";
         public static final String PARTICIPANT_SHOULD_ADV = "participant_is_advertising";
         public static final String PARTICIPANT_SYNCED = "participant_synced";
     }
+
+    class Unknown {
+        public static final String SYNCGROUP_NAME = "unknown_syncgroup";
+        public static final String PRESENTATION_ID = "unknown_pres_id";
+    }
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Question.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Question.java
index 36b9c66..0298477 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Question.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Question.java
@@ -4,30 +4,40 @@
 
 package io.v.android.apps.syncslides.model;
 
+import android.os.Parcel;
+import android.os.Parcelable;
+
 import org.joda.time.DateTime;
 
 /**
  * Represents a question asked by the audience.
  */
-public class Question {
+public class Question implements Parcelable {
     private String mId;
-    private String mFirstName;
-    private String mLastName;
-    private DateTime mTime;
+    private String mName;
+    /**
+     * Time at which the question was asked in ms since the epoch.  Stored as
+     * a long rather than DateTime because it is easier to serialize this way.
+     */
+    private long mTime;
 
     /**
      * @param id a uuid
-     * @param firstName the first name of the questioner
-     * @param lastName the last name of the questioner
-     * @param time the time at which the question was asked
+     * @param name the name of the questioner
+     * @param time the time at which the question was asked in ms since the epoch
      */
-    public Question(String id, String firstName, String lastName, DateTime time) {
+    public Question(String id, String name, long time) {
         mId = id;
-        mFirstName = firstName;
-        mLastName = lastName;
+        mName = name;
         mTime = time;
     }
 
+    private Question(Parcel source) {
+        mId = source.readString();
+        mName = source.readString();
+        mTime = source.readLong();
+    }
+
     /**
      * Returns the unique id for the question.
      */
@@ -36,24 +46,43 @@
     }
 
    /**
-     * Returns the first name of the questioner.
+     * Returns the name of the questioner.
      */
-    public String getFirstName() {
-        return mFirstName;
+    public String getName() {
+        return mName;
 
     }
 
     /**
-     * Returns the last name of the questioner.
-     */
-    public String getLastName() {
-        return mLastName;
-    }
-
-    /**
      * Returns the time at which the question was asked.
      */
     public DateTime getTime() {
-        return mTime;
+        return new DateTime(mTime);
     }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mId);
+        dest.writeString(mName);
+        dest.writeLong(mTime);
+    }
+
+    public static final Parcelable.Creator<Question> CREATOR =
+            new Parcelable.Creator<Question>() {
+
+                @Override
+                public Question createFromParcel(Parcel source) {
+                    return new Question(source);
+                }
+
+                @Override
+                public Question[] newArray(int size) {
+                    return new Question[size];
+                }
+            };
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Slide.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Slide.java
index ec0ec7e..ae1e29e 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Slide.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Slide.java
@@ -19,4 +19,9 @@
      * Returns the slide notes.
      */
     String getNotes();
+
+    /**
+     * Sets the slide notes.
+     */
+    void setNotes(String notes);
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/SlideImpl.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/SlideImpl.java
index 8baff6f..987b168 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/SlideImpl.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/SlideImpl.java
@@ -5,26 +5,33 @@
 package io.v.android.apps.syncslides.model;
 
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 
 /**
  * Application impl of Slide.
  */
 public class SlideImpl implements Slide {
-    private final Bitmap mThumbnail;
-    private final String mNotes;
+    private final byte[] mThumbnail;
+    private String mNotes;
 
-    public SlideImpl(Bitmap thumbnail, String notes) {
+    public SlideImpl(byte[] thumbnail, String notes) {
         mThumbnail = thumbnail;
         mNotes = notes;
     }
 
     @Override
     public Bitmap getImage() {
-        return mThumbnail;
+        return BitmapFactory.decodeByteArray(
+                mThumbnail, 0 /* offset */, mThumbnail.length);
     }
 
     @Override
     public String getNotes() {
         return mNotes;
     }
+
+    @Override
+    public void setNotes(String notes) {
+        mNotes = notes;
+    }
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/notify/NotifierNative.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/notify/NotifierNative.java
index 818933d..1f701ea 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/notify/NotifierNative.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/notify/NotifierNative.java
@@ -53,7 +53,7 @@
                 .setFullScreenIntent(contentIntent, false)
                 .setAutoCancel(true)
                 .setWhen(System.currentTimeMillis())
-                .setContentTitle(p.getUserName())
+                .setContentTitle(p.getUser().getName())
                 .setContentText(p.getDeck().getTitle())
                 // TODO(jregan): Need a better icon.
                 .setSmallIcon(R.drawable.orange_circle)
diff --git a/projects/syncslides/app/src/main/res/layout/deck_card.xml b/projects/syncslides/app/src/main/res/layout/deck_card.xml
index 72ecd2c..66302b8 100644
--- a/projects/syncslides/app/src/main/res/layout/deck_card.xml
+++ b/projects/syncslides/app/src/main/res/layout/deck_card.xml
@@ -16,7 +16,7 @@
         <ImageView
             android:id="@+id/deck_thumb"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
+            android:layout_height="@dimen/deck_thumb_height"
             android:scaleType="centerCrop"/>
 
         <!-- Display the title of the deck and a menu to configure it. -->
diff --git a/projects/syncslides/app/src/main/res/mipmap-hdpi/vanadium_launcher.png b/projects/syncslides/app/src/main/res/mipmap-hdpi/vanadium_launcher.png
new file mode 100644
index 0000000..9d5e0d6
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/mipmap-hdpi/vanadium_launcher.png
Binary files differ
diff --git a/projects/syncslides/app/src/main/res/mipmap-mdpi/vanadium_launcher.png b/projects/syncslides/app/src/main/res/mipmap-mdpi/vanadium_launcher.png
new file mode 100644
index 0000000..66f1514
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/mipmap-mdpi/vanadium_launcher.png
Binary files differ
diff --git a/projects/syncslides/app/src/main/res/mipmap-xhdpi/vanadium_launcher.png b/projects/syncslides/app/src/main/res/mipmap-xhdpi/vanadium_launcher.png
new file mode 100644
index 0000000..9dfa8d7
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/mipmap-xhdpi/vanadium_launcher.png
Binary files differ
diff --git a/projects/syncslides/app/src/main/res/mipmap-xxhdpi/vanadium_launcher.png b/projects/syncslides/app/src/main/res/mipmap-xxhdpi/vanadium_launcher.png
new file mode 100644
index 0000000..3c7d066
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/mipmap-xxhdpi/vanadium_launcher.png
Binary files differ
diff --git a/projects/syncslides/app/src/main/res/values/dimens.xml b/projects/syncslides/app/src/main/res/values/dimens.xml
index a5f280c..03925e3 100644
--- a/projects/syncslides/app/src/main/res/values/dimens.xml
+++ b/projects/syncslides/app/src/main/res/values/dimens.xml
@@ -10,8 +10,9 @@
     <dimen name="fab_margin">16dp</dimen>
     <dimen name="fab_elevation">4dp</dimen>
 
-    <dimen name="deck_card_width">150dp</dimen>
+    <dimen name="deck_card_width">200dp</dimen>
     <dimen name="deck_card_margin">5dp</dimen>
+    <dimen name="deck_thumb_height">115dp</dimen>
     <dimen name="card_toolbar_height">60dp</dimen>
     <dimen name="toolbar_and_statusbar_height">80dp</dimen>
     <dimen name="toolbar_inset">10dp</dimen>
diff --git a/projects/syncslides/app/src/main/res/values/strings.xml b/projects/syncslides/app/src/main/res/values/strings.xml
index 48013cd..c6af291 100644
--- a/projects/syncslides/app/src/main/res/values/strings.xml
+++ b/projects/syncslides/app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
 <resources>
-    <string name="app_name">aaaSyncSlides</string>
+    <string name="app_name">SyncSlides</string>
 
     <string name="title_account1">Account 1</string>
     <string name="title_account2">Account 2</string>