Merge branch 'master' into ble

Change-Id: I7083bedd33abf32e18e965bf7b7f4c6da08655fe
diff --git a/lib/src/main/java/io/v/v23/V.java b/lib/src/main/java/io/v/v23/V.java
index 0b00a45..464825e 100644
--- a/lib/src/main/java/io/v/v23/V.java
+++ b/lib/src/main/java/io/v/v23/V.java
@@ -52,6 +52,14 @@
     private static volatile VRuntime runtime = null;
     private static volatile boolean initOnceDone = false;
 
+    private static boolean isDarwin() {
+        return System.getProperty("os.name").toLowerCase().contains("os x");
+    }
+
+    private static boolean isLinux() {
+        return System.getProperty("os.name").toLowerCase().contains("linux");
+    }
+
     private static synchronized void initOnce() {
         if (initOnceDone) {
             return;
@@ -64,8 +72,19 @@
             // Thrown if the library does not exist. In this case, try to find it in our classpath.
             errors.add(new RuntimeException("loadLibrary attempt failed", ule));
             try {
-                URL resource = Resources.getResource("libv23.so");
-                File file = File.createTempFile("libv23-", ".so");
+                URL resource = null;
+                File file = null;
+                if (isLinux()) {
+                    resource = Resources.getResource("libv23.so");
+                    file = File.createTempFile("libv23-", ".so");
+                } else if (isDarwin()) {
+                    resource = Resources.getResource("libv23.dylib");
+                    file = File.createTempFile("libv23-", ".dylib");
+                } else {
+                    String os = System.getProperty("os.name");
+                    errors.add(new RuntimeException("unsupported OS: " + os));
+                    throw new RuntimeException("Unsupported OS: " + os, new VLoaderException(errors));
+                }
                 file.deleteOnExit();
                 ByteStreams.copy(resource.openStream(), new FileOutputStream(file));
                 System.load(file.getAbsolutePath());
diff --git a/projects/syncslidepresenter/build.gradle b/projects/syncslidepresenter/build.gradle
new file mode 100644
index 0000000..e13c1f1
--- /dev/null
+++ b/projects/syncslidepresenter/build.gradle
@@ -0,0 +1,44 @@
+apply plugin: 'application'
+apply plugin: 'java'
+apply plugin: 'io.v.vdl'
+
+buildscript {
+    repositories {
+        maven {
+            url 'https://maven.v.io'
+        }
+    }
+    dependencies {
+        classpath 'io.v:gradle-plugin:0.1-SNAPSHOT'
+    }
+}
+
+mainClassName = "io.v.syncslidepresenter"
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+    compile project(':lib')
+    compile 'com.google.guava:guava:18'
+    compile 'com.beust:jcommander:1.48'
+}
+
+task copyLib(type: Copy) {
+    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))
+}
+
+clean {
+    delete 'src/main/resources/libv23.so'
+    delete 'src/main/resources/libv23.dylib'
+}
+
+vdl {
+    inputPaths += [project(':projects:syncslides').projectDir.absolutePath + '/app/src/main/java']
+}
+
+sourceCompatibility = '1.8'
+targetCompatibility = '1.8'
\ No newline at end of file
diff --git a/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java b/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java
new file mode 100644
index 0000000..4f23fa5
--- /dev/null
+++ b/projects/syncslidepresenter/src/main/java/io/v/syncslidepresenter/Main.java
@@ -0,0 +1,304 @@
+// 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.syncslidepresenter;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.joda.time.Duration;
+
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Image;
+import java.awt.Window;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.imageio.ImageIO;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.WindowConstants;
+
+import io.v.android.apps.syncslides.db.VCurrentSlide;
+import io.v.android.apps.syncslides.db.VSlide;
+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.naming.Endpoint;
+import io.v.v23.rpc.Server;
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.security.access.AccessList;
+import io.v.v23.security.access.Permissions;
+import io.v.v23.services.syncbase.nosql.KeyValue;
+import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
+import io.v.v23.services.watch.ResumeMarker;
+import io.v.v23.syncbase.Syncbase;
+import io.v.v23.syncbase.SyncbaseApp;
+import io.v.v23.syncbase.SyncbaseService;
+import io.v.v23.syncbase.nosql.BatchDatabase;
+import io.v.v23.syncbase.nosql.Database;
+import io.v.v23.syncbase.nosql.RowRange;
+import io.v.v23.syncbase.nosql.Stream;
+import io.v.v23.syncbase.nosql.Syncgroup;
+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;
+
+/**
+ * The entry point for syncslidepresenter.
+ */
+public class Main {
+    private static final Logger logger = Logger.getLogger(Main.class.getName());
+    private static final String SYNCBASE_APP = "syncslides";
+    private static final String SYNCBASE_DB = "syncslides";
+    private static final String PRESENTATIONS_TABLE = "Presentations";
+    private static final String DECKS_TABLE = "Decks";
+    private final Table presentations;
+    private final Table decks;
+    private final ImageViewer viewer;
+
+    private Database db;
+
+    private VContext context;
+
+    public static void main(String[] args) throws SyncbaseServer.StartException, VException, IOException {
+        Options options = new Options();
+        JCommander commander = new JCommander(options);
+        try {
+            commander.parse(args);
+        } catch (ParameterException e) {
+            logger.warning("Could not parse parameters: " + e.getMessage());
+            commander.usage();
+            return;
+        }
+
+        if (options.help) {
+            commander.usage();
+            return;
+        }
+
+        // Make command-Q do the same as closing the main frame (i.e. exit).
+        System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
+
+        JFrame frame = new JFrame();
+        enableOSXFullscreen(frame);
+        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+        frame.setVisible(true);
+
+        VContext baseContext = V.init();
+
+        AccessList acl = new AccessList(ImmutableList.of(new BlessingPattern("...")),
+                ImmutableList.<String>of());
+        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);
+        if (server.getStatus().getEndpoints().length > 0) {
+            logger.info("Mounted syncbase server at the following endpoints: ");
+            for (Endpoint e : server.getStatus().getEndpoints()) {
+                logger.info("\t" + e);
+            }
+            logger.info("End of endpoint list");
+
+            SyncbaseService service
+                    = Syncbase.newService("/" + server.getStatus().getEndpoints()[0]);
+            SyncbaseApp app = service.getApp(SYNCBASE_APP);
+            if (!app.exists(baseContext)) {
+                app.create(baseContext, permissions);
+            }
+            Database db = app.getNoSqlDatabase(SYNCBASE_DB, null);
+            if (!db.exists(baseContext)) {
+                db.create(baseContext, permissions);
+            }
+            Table decks = db.getTable(DECKS_TABLE);
+            if (!decks.exists(baseContext)) {
+                decks.create(baseContext, permissions);
+            }
+            Table presentations = db.getTable(PRESENTATIONS_TABLE);
+            if (!presentations.exists(baseContext)) {
+                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, presentationPanel, db, decks, presentations);
+            m.joinPresentation(options.presentationName, options.joinTimeoutSeconds,
+                    options.currentSlideKey, options.slideRowFormat);
+        }
+    }
+
+    public Main(VContext context, ImageViewer viewer, Database db, Table decks,
+                Table presentations) throws VException {
+        this.context = context;
+        this.db = db;
+        this.presentations = presentations;
+        this.decks = decks;
+        this.viewer = viewer;
+    }
+
+    public void joinPresentation(final String syncgroupName,
+                                 int joinTimeoutSeconds,
+                                 String currentSlideKey,
+                                 String slideRowFormat) throws VException {
+        Syncgroup syncgroup = db.getSyncgroup(syncgroupName);
+        syncgroup.join(context.withTimeout(Duration.standardSeconds(joinTimeoutSeconds)),
+                new SyncgroupMemberInfo((byte) 1));
+        for (String member : syncgroup.getMembers(context).keySet()) {
+            logger.info("Member: " + member);
+        }
+
+        for (KeyValue keyValue : presentations.scan(context, RowRange.prefix(""))) {
+            System.out.println("Presentation: " + keyValue);
+        }
+        BatchDatabase batch = db.beginBatch(context, null);
+        ResumeMarker marker = batch.getResumeMarker(context);
+        Stream<WatchChange> watchStream = db.watch(context, presentations.name(), currentSlideKey,
+                marker);
+
+        for (WatchChange w : watchStream) {
+            logger.info("Change detected in " + w.getRowName());
+            logger.info("Type: " + w.getChangeType());
+            try {
+                VCurrentSlide currentSlide = (VCurrentSlide) VomUtil.decode(w.getVomValue(),
+                        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);
+            } catch (IOException | VException e) {
+                logger.log(Level.WARNING, "exception encountered while handling change event", e);
+            }
+        }
+    }
+
+    private static void enableOSXFullscreen(Window window) {
+        Preconditions.checkNotNull(window);
+        try {
+            // This class may not be present on the system (e.g. if we're not on MacOSX),
+            // use reflection so that we can make this an optional dependency.
+            Class util = Class.forName("com.apple.eawt.FullScreenUtilities");
+            Class params[] = new Class[]{Window.class, Boolean.TYPE};
+
+            @SuppressWarnings({"unchecked", "rawtypes"})
+            Method method = util.getMethod("setWindowCanFullScreen", params);
+            method.invoke(util, window, true);
+        } catch (ClassNotFoundException e) {
+            // Probably not on Mac OS X
+        } catch (Exception e) {
+            logger.log(Level.WARNING, "Couldn't enable fullscreen on Mac OS X", e);
+        }
+    }
+
+    public static class Options {
+        @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";
+
+        @Parameter(names = {"-h", "--help"}, description = "display this help message", help = true)
+        private boolean help = false;
+    }
+
+    private static class ScaleToFitJPanel extends JPanel implements ImageViewer {
+        private Image image;
+
+        public ScaleToFitJPanel() {
+            super();
+            setPreferredSize(new Dimension(250, 250));
+            addComponentListener(new ComponentAdapter() {
+                @Override
+                public void componentResized(ComponentEvent e) {
+                    repaint();
+                }
+            });
+        }
+
+        @Override
+        protected void paintComponent(Graphics g) {
+            super.paintComponent(g);
+
+            if (image != null) {
+                int width;
+                int height;
+                double containerRatio = 1.0d * getWidth() / getHeight();
+                double imageRatio = 1.0d * image.getWidth(null) / image.getHeight(null);
+
+                if (containerRatio < imageRatio) {
+                    width = getWidth();
+                    height = (int) (getWidth() / imageRatio);
+                } else {
+                    width = (int) (getHeight() * imageRatio);
+                    height = getHeight();
+                }
+
+                // Center the image in the container.
+                int x = (int) (((double) getWidth() / 2) - ((double) width / 2));
+                int y = (int) (((double) getHeight()/ 2) - ((double) height / 2));
+
+                g.drawImage(image, x, y, width, height, this);
+            }
+        }
+
+        @Override
+        public void setImage(Image image) {
+            this.image = image;
+            setPreferredSize(new Dimension(image.getWidth(null), image.getHeight(null)));
+            repaint();
+        }
+    }
+
+    private interface ImageViewer {
+        void setImage(Image image);
+    }
+}
diff --git a/projects/syncslides/app/build.gradle b/projects/syncslides/app/build.gradle
index 81966f8..c8c0063 100644
--- a/projects/syncslides/app/build.gradle
+++ b/projects/syncslides/app/build.gradle
@@ -48,7 +48,7 @@
 
     defaultConfig {
         applicationId "io.v.android.apps.syncslides"
-        minSdkVersion 21
+        minSdkVersion 22
         targetSdkVersion 23
         versionCode 1
         versionName "1.0"
diff --git a/projects/syncslides/app/src/main/AndroidManifest.xml b/projects/syncslides/app/src/main/AndroidManifest.xml
index 1719535..6030afe 100644
--- a/projects/syncslides/app/src/main/AndroidManifest.xml
+++ b/projects/syncslides/app/src/main/AndroidManifest.xml
@@ -1,29 +1,37 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest package="io.v.android.apps.syncslides"
-          xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.v.android.apps.syncslides" >
+
+    <uses-sdk android:minSdkVersion="22" />
+
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
 
     <application
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
-        android:theme="@style/AppTheme">
+        android:theme="@style/AppTheme" >
         <activity
-            android:name=".DeckChooserActivity"
-            android:label="@string/app_name">
+            android:name=".SignInActivity"
+            android:label="@string/app_name" >
             <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-
-                <category android:name="android.intent.category.LAUNCHER"/>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-        <activity android:name=".PresentationActivity"/>
+        <activity
+            android:name=".DeckChooserActivity"
+            android:label="@string/app_name" >
+        </activity>
+        <activity android:name=".PresentationActivity" />
         <service
             android:name=".discovery.ParticipantPeer"
             android:exported="false"
             android:label="Location Service"
-            android:process=":ParticipantPeer">
+            android:process=":ParticipantPeer" >
         </service>
     </application>
 
-    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
 </manifest>
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 2790330..36304cb 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
@@ -5,25 +5,24 @@
 package io.v.android.apps.syncslides;
 
 import android.content.Intent;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v4.app.FragmentManager;
 import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.AppCompatActivity;
 import android.util.Log;
 import android.view.Menu;
-import android.support.v4.widget.DrawerLayout;
 
 import io.v.android.apps.syncslides.db.DB;
-import io.v.android.apps.syncslides.discovery.V23Manager;
-import io.v.android.v23.services.blessing.BlessingCreationException;
-import io.v.v23.security.Blessings;
-import io.v.v23.verror.VException;
+import io.v.android.apps.syncslides.misc.V23Manager;
+import io.v.android.apps.syncslides.model.DeckFactory;
 
 public class DeckChooserActivity extends AppCompatActivity
         implements NavigationDrawerFragment.NavigationDrawerCallbacks {
 
     private static final String TAG = "DeckChooser";
     /**
-     * Fragment managing the behaviors, interactions and deck of the navigation drawer.
+     * Fragment managing the behaviors, interactions and deck of the navigation
+     * drawer.
      */
     private NavigationDrawerFragment mNavigationDrawerFragment;
     private DB mDB;
@@ -32,6 +31,8 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Log.d(TAG, "onCreate");
+        // Initialize the DeckFactory.
+        DeckFactory.Singleton.get(getApplicationContext());
         // Immediately initialize V23, possibly sending user to the
         // AccountManager to get blessings.
         V23Manager.Singleton.get().init(getApplicationContext(), this);
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 19ebbfd..6c25a93 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
@@ -36,7 +36,7 @@
 
 import io.v.android.apps.syncslides.db.DB;
 import io.v.android.apps.syncslides.model.Deck;
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
 import io.v.android.apps.syncslides.model.Slide;
 import io.v.android.apps.syncslides.model.SlideImpl;
 
@@ -77,7 +77,6 @@
                 onImportDeck();
             }
         });
-
         mRecyclerView = (RecyclerView) rootView.findViewById(R.id.deck_grid);
         mRecyclerView.setHasFixedSize(true);
 
@@ -208,7 +207,7 @@
             String id = UUID.randomUUID().toString();
             String title = metadata.getString("Title");
             Bitmap thumb = readImage(dir, metadata.getString("Thumb"));
-            Deck deck = new DeckImpl(title, thumb, id);
+            Deck deck = DeckFactory.Singleton.get().make(title, thumb, id);
             Slide[] slides = readSlides(dir, metadata);
             // TODO(spetrovic): Do this asynchronously.
             DB.Singleton.get(getActivity().getApplicationContext()).importDeck(deck, slides);
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 3555157..75fbe0b 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
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 package io.v.android.apps.syncslides;
+
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
@@ -15,11 +16,13 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 import android.widget.Toolbar;
+
 import io.v.android.apps.syncslides.db.DB;
 import io.v.android.apps.syncslides.discovery.DiscoveryManager;
 import io.v.android.apps.syncslides.model.Deck;
 import io.v.android.apps.syncslides.model.Listener;
 import io.v.android.apps.syncslides.model.Participant;
+import io.v.android.apps.syncslides.model.Role;
 
 /**
  * Provides a list of decks to be shown in the RecyclerView of the
@@ -41,7 +44,7 @@
             throw new IllegalStateException("Wrong lifecycle.");
         }
         Log.d(TAG, "Starting.");
-        DiscoveryManager dm = DiscoveryManager.make();
+        DiscoveryManager dm = DiscoveryManager.make(context);
         // Listening stops below in mLiveDecks.discard.
         dm.setListener(this);
         dm.start(context);
@@ -52,16 +55,19 @@
             public void notifyItemChanged(int position) {
                 DeckListAdapter.this.notifyItemChanged(mLiveDecks.getItemCount() + position);
             }
+
             @Override
             public void notifyItemInserted(int position) {
                 DeckListAdapter.this.notifyItemInserted(mLiveDecks.getItemCount() + position);
             }
+
             @Override
             public void notifyItemRemoved(int position) {
                 DeckListAdapter.this.notifyItemRemoved(mLiveDecks.getItemCount() + position);
             }
         });
     }
+
     /**
      * Stops any background monitoring of the underlying data.
      */
@@ -72,20 +78,22 @@
         mDecks.discard();
         mDecks = null;
     }
+
     @Override
     public ViewHolder onCreateViewHolder(ViewGroup parent, int i) {
         View v = LayoutInflater.from(parent.getContext())
                 .inflate(R.layout.deck_card, parent, false);
         return new ViewHolder(v);
     }
+
     @Override
-    public void onBindViewHolder(final ViewHolder holder, int i) {
+    public void onBindViewHolder(final ViewHolder holder, int deckIndex) {
         final Deck deck;
         final Role role;
         // If the position is less than the number of live presentation decks, get deck card from
         // there (and don't allow the user to delete the deck). If not, get the card from the DB.
-        if (i < mLiveDecks.getItemCount()) {
-            deck = mLiveDecks.get(i);
+        if (deckIndex < mLiveDecks.getItemCount()) {
+            deck = mLiveDecks.get(deckIndex);
             holder.mToolbarLiveNow
                     .setText(holder.itemView.getResources().getString(R.string.presentation_live));
             holder.mToolbarLiveNow.setVisibility(View.VISIBLE);
@@ -93,7 +101,7 @@
             holder.mToolbar.getMenu().clear();
             role = Role.AUDIENCE;
         } else {
-            deck = mDecks.get(i - mLiveDecks.getItemCount());
+            deck = mDecks.get(deckIndex - mLiveDecks.getItemCount());
             // TODO(afergan): Set actual date here.
             holder.mToolbarLastOpened.setText("Opened on Oct 26, 2015");
             holder.mToolbarLastOpened.setVisibility(View.VISIBLE);
@@ -128,28 +136,28 @@
                     String deviceId = "355499060490393"; // Nexus 6 ZX1G22MLNL
                     DB.Singleton.get(context).joinPresentation(
                             // TODO(kash): Use the real syncgroup name.
-                            "/192.168.86.254:8101/"+deviceId+"/%%sync/syncslides/" +
+                            "/192.168.86.254:8101/" + deviceId + "/%%sync/syncslides/" +
                                     "deckId1/randomPresentationId1",
                             new DB.Callback<Void>() {
                                 @Override
                                 public void done(Void aVoid) {
-                                    // Intent for the activity to open when user selects the thumbnail.
-                                    Intent intent = new Intent(context, PresentationActivity.class);
-                                    intent.putExtras(deck.toBundle(null));
-                                    intent.putExtra(Participant.B.PARTICIPANT_ROLE, role);
-                                    context.startActivity(intent);
+                                    showSlides(context, deck, role);
                                 }
                             });
                 } else {
-                    // Intent for the activity to open when user selects the thumbnail.
-                    Intent intent = new Intent(context, PresentationActivity.class);
-                    intent.putExtras(deck.toBundle(null));
-                    intent.putExtra(Participant.B.PARTICIPANT_ROLE, role);
-                    context.startActivity(intent);
+                    showSlides(context, deck, role);
                 }
             }
         });
     }
+
+    private void showSlides(Context context, Deck deck, Role role) {
+        Intent intent = new Intent(context, PresentationActivity.class);
+        intent.putExtra(Deck.B.DECK_ID, deck.getId());
+        intent.putExtra(Participant.B.PARTICIPANT_ROLE, role);
+        context.startActivity(intent);
+    }
+
     @Override
     public int getItemCount() {
         return mLiveDecks.getItemCount() + mDecks.getItemCount();
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/FullscreenSlideFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/FullscreenSlideFragment.java
index 2676ca5..db21323 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/FullscreenSlideFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/FullscreenSlideFragment.java
@@ -14,6 +14,7 @@
 import java.util.List;
 
 import io.v.android.apps.syncslides.db.DB;
+import io.v.android.apps.syncslides.model.Role;
 import io.v.android.apps.syncslides.model.Slide;
 
 public class FullscreenSlideFragment extends Fragment {
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 b12c4ce..46dfc86 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
@@ -31,6 +31,7 @@
 
 import io.v.android.apps.syncslides.db.DB;
 import io.v.android.apps.syncslides.model.Question;
+import io.v.android.apps.syncslides.model.Role;
 import io.v.android.apps.syncslides.model.Slide;
 
 /**
@@ -127,16 +128,18 @@
             mFabSync.setVisibility(View.VISIBLE);
         }
 
-        mFabSync.setOnClickListener(new View.OnClickListener() {
+        mFabSync.setOnClickListener(new NavigateClickListener() {
             @Override
             public void onClick(View v) {
+                super.onClick(v);
                 sync();
                 mFabSync.setVisibility(View.INVISIBLE);
             }
         });
-        View.OnClickListener previousSlideListener = new View.OnClickListener() {
+        View.OnClickListener previousSlideListener = new NavigateClickListener() {
             @Override
             public void onClick(View v) {
+                super.onClick(v);
                 previousSlide();
             }
         };
@@ -145,9 +148,10 @@
         mPrevThumb = (ImageView) rootView.findViewById(R.id.prev_thumb);
         mPrevThumb.setOnClickListener(previousSlideListener);
 
-        View.OnClickListener nextSlideListener = new View.OnClickListener() {
+        View.OnClickListener nextSlideListener = new NavigateClickListener() {
             @Override
             public void onClick(View v) {
+                super.onClick(v);
                 nextSlide();
             }
         };
@@ -165,16 +169,18 @@
         mNextThumb.setOnClickListener(nextSlideListener);
         mQuestions = (ImageView) rootView.findViewById(R.id.questions);
         // TODO(kash): Hide the mQuestions button if mRole == BROWSER.
-        mQuestions.setOnClickListener(new View.OnClickListener() {
+        mQuestions.setOnClickListener(new NavigateClickListener() {
             @Override
             public void onClick(View v) {
+                super.onClick(v);
                 questionButton();
             }
         });
         mCurrentSlide = (ImageView) rootView.findViewById(R.id.slide_current_medium);
-        mCurrentSlide.setOnClickListener(new View.OnClickListener() {
+        mCurrentSlide.setOnClickListener(new NavigateClickListener() {
             @Override
             public void onClick(View v) {
+                super.onClick(v);
                 if (mRole == Role.AUDIENCE || mRole == Role.BROWSER) {
                     ((PresentationActivity) getActivity()).showFullscreenSlide(mUserSlideNum);
                 }
@@ -192,6 +198,7 @@
                 unsync();
             }
         });
+
         // The parent of mNotes needs to be focusable in order to clear focus
         // from mNotes when done editing.  We set the attributes in code rather
         // than in XML because it is too easy to add an extra level of layout
@@ -202,9 +209,10 @@
         parent.setFocusableInTouchMode(true);
 
         View slideListIcon = rootView.findViewById(R.id.slide_list);
-        slideListIcon.setOnClickListener(new View.OnClickListener() {
+        slideListIcon.setOnClickListener(new NavigateClickListener() {
             @Override
             public void onClick(View v) {
+                super.onClick(v);
                 if (mRole == Role.AUDIENCE) {
                     ((PresentationActivity) getActivity()).showSlideList();
                 } else {
@@ -298,20 +306,26 @@
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case R.id.action_save:
-                Toast.makeText(getContext(), "Saving notes", Toast.LENGTH_SHORT).show();
-                mNotes.clearFocus();
-                InputMethodManager inputManager =
-                        (InputMethodManager) getContext().
-                                getSystemService(Context.INPUT_METHOD_SERVICE);
-                inputManager.hideSoftInputFromWindow(
-                        getActivity().getCurrentFocus().getWindowToken(),
-                        InputMethodManager.HIDE_NOT_ALWAYS);
-                ((PresentationActivity) getActivity()).setUiImmersive(true);
+                saveNotes();
                 return true;
         }
         return false;
     }
 
+    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);
+            inputManager.hideSoftInputFromWindow(
+                    getActivity().getCurrentFocus().getWindowToken(),
+                    InputMethodManager.HIDE_NOT_ALWAYS);
+            ((PresentationActivity) getActivity()).setUiImmersive(true);
+        }
+    }
+
     private void unsync() {
         if (mRole == Role.AUDIENCE && mSynced) {
             mSynced = false;
@@ -456,9 +470,10 @@
      */
     private void handoffControl() {
         //TODO(afergan): Change slide presenter to the audience member at mQuestionerPosition.
-        View.OnClickListener snackbarClickListener = new View.OnClickListener() {
+        View.OnClickListener snackbarClickListener = new NavigateClickListener() {
             @Override
             public void onClick(View v) {
+                super.onClick(v);
                 //TODO(afergan): End handoff, presenter regains control of presentation.
             }
         };
@@ -504,4 +519,11 @@
             thumbParams.height = (int) ((9 / 16.0) * grandparent.getMeasuredWidth());
         }
     }
-}
+
+    public class NavigateClickListener implements View.OnClickListener {
+        @Override
+        public void onClick(View v) {
+            saveNotes();
+        }
+    }
+}
\ No newline at end of file
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 61fc9b4..2f5d437 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
@@ -15,6 +15,7 @@
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -24,6 +25,10 @@
 import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.ListView;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
 
 /**
  * Fragment used for managing interactions for and presentation of a navigation drawer.
@@ -31,6 +36,7 @@
  * design guidelines</a> for a complete explanation of the behaviors implemented here.
  */
 public class NavigationDrawerFragment extends Fragment {
+    private static final String TAG = "NavigationDrawer";
 
     /**
      * Remember the position of the selected item.
@@ -56,6 +62,7 @@
     private DrawerLayout mDrawerLayout;
     private ListView mDrawerListView;
     private View mFragmentContainerView;
+    private JSONObject mUserProfile;
 
     private int mCurrentSelectedPosition = 0;
     private boolean mFromSavedInstanceState;
@@ -70,8 +77,14 @@
 
         // Read in the flag indicating whether or not the user has demonstrated awareness of the
         // drawer. See PREF_USER_LEARNED_DRAWER for details.
-        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
-        mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false);
+        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);
+        }
 
         if (savedInstanceState != null) {
             mCurrentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION);
@@ -100,15 +113,33 @@
                 selectItem(position);
             }
         });
-        mDrawerListView.setAdapter(new ArrayAdapter<String>(
+        mDrawerListView.setAdapter(new ArrayAdapter<JSONObject>(
                 getActionBar().getThemedContext(),
-                android.R.layout.simple_list_item_activated_1,
+                android.R.layout.simple_list_item_activated_2,
                 android.R.id.text1,
-                new String[]{
-                        getString(R.string.title_account1),
-                        getString(R.string.title_account2),
-                        getString(R.string.title_account3),
-                }));
+                new JSONObject[]{mUserProfile}) {
+            @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);
+                return view;
+            }
+        });
         mDrawerListView.setItemChecked(mCurrentSelectedPosition, true);
         return mDrawerListView;
     }
@@ -269,7 +300,7 @@
     /**
      * Callbacks interface that all activities using this fragment must implement.
      */
-    public static interface NavigationDrawerCallbacks {
+    public interface NavigationDrawerCallbacks {
         /**
          * Called when an item in the navigation drawer is selected.
          */
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 48fafc0..9ff1fa9 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
@@ -13,11 +13,13 @@
 import android.widget.Toast;
 
 import io.v.android.apps.syncslides.db.DB;
-import io.v.android.apps.syncslides.discovery.ParticipantServerImpl;
-import io.v.android.apps.syncslides.discovery.V23Manager;
+import io.v.android.apps.syncslides.discovery.ParticipantPeer;
+import io.v.android.apps.syncslides.misc.Config;
+import io.v.android.apps.syncslides.misc.V23Manager;
 import io.v.android.apps.syncslides.model.Deck;
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
 import io.v.android.apps.syncslides.model.Participant;
+import io.v.android.apps.syncslides.model.Role;
 
 public class PresentationActivity extends AppCompatActivity {
     private static final String TAG = "PresentationActivity";
@@ -32,7 +34,7 @@
      */
     private Role mRole;
     // TODO(kash): Replace this with the presentation id.
-    private String mPresentationId = "randomPresentation1";
+    private String mPresentationId = "randomPresentationId1";
     private boolean mSynced;
 
     /**
@@ -49,6 +51,8 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Log.d(TAG, "onCreate");
+        // Initialize the DeckFactory.
+        DeckFactory.Singleton.get(getApplicationContext());
         // Immediately initialize V23, possibly sending user to the
         // AccountManager to get blessings.
         V23Manager.Singleton.get().init(getApplicationContext(), this);
@@ -57,16 +61,17 @@
 
         mShouldBeAdvertising = false;
         mIsAdvertising = false;
+        String deckId;
         if (savedInstanceState == null) {
             Log.d(TAG, "savedInstanceState is null");
-            mDeck = DeckImpl.fromBundle(getIntent().getExtras());
+            deckId = getIntent().getStringExtra(Deck.B.DECK_ID);
             mRole = (Role) getIntent().getSerializableExtra(
                     Participant.B.PARTICIPANT_ROLE);
             mSynced = true;
         } else {
             Log.d(TAG, "savedInstanceState is NOT null");
-            mDeck = DeckImpl.fromBundle(savedInstanceState);
             mRole = (Role) savedInstanceState.get(Participant.B.PARTICIPANT_ROLE);
+            deckId = savedInstanceState.getString(Deck.B.DECK_ID);
             mSynced = savedInstanceState.getBoolean(Participant.B.PARTICIPANT_SYNCED);
             mShouldBeAdvertising = savedInstanceState.getBoolean(Participant.B.PARTICIPANT_SHOULD_ADV);
             if (mShouldBeAdvertising) {
@@ -74,6 +79,13 @@
             }
         }
 
+        Log.d(TAG, "Role = " + mRole);
+        mDeck = DB.Singleton.get(getApplicationContext()).getDeck(deckId);
+        if (mDeck == null) {
+            throw new IllegalArgumentException("Unusable deckId: "+ deckId);
+        }
+        Log.d(TAG, "Using deck: " + mDeck);
+
         // TODO(jregan): This appears to be an attempt to avoid fragment
         // re-inflation, possibly the right thing to do is move the code
         // below to another flow step, e.g. onRestoreInstanceState.
@@ -81,7 +93,7 @@
             return;
         }
 
-        if (mShouldBeAdvertising){
+        if (mShouldBeAdvertising) {
             startAdvertising();
         }
 
@@ -109,10 +121,10 @@
 
     @Override
     protected void onSaveInstanceState(Bundle b) {
-        Log.d(TAG, "onSaveInstanceState");
         super.onSaveInstanceState(b);
-        mDeck.toBundle(b);
+        Log.d(TAG, "onSaveInstanceState");
         b.putSerializable(Participant.B.PARTICIPANT_ROLE, mRole);
+        b.putString(Deck.B.DECK_ID, mDeck.getId());
         b.putBoolean(Participant.B.PARTICIPANT_SYNCED, mSynced);
         b.putBoolean(Participant.B.PARTICIPANT_SHOULD_ADV, mShouldBeAdvertising);
     }
@@ -121,7 +133,7 @@
     protected void onStart() {
         super.onStart();
         Log.d(TAG, "onStart");
-        if (mShouldBeAdvertising){
+        if (mShouldBeAdvertising) {
             startAdvertising();
         }
     }
@@ -157,7 +169,7 @@
     }
 
     private boolean shouldUseV23() {
-        return Participant.ENABLE_MT_DISCOVERY && V23Manager.Singleton.get().isBlessed();
+        return Config.MtDiscovery.ENABLE && V23Manager.Singleton.get().isBlessed();
     }
 
     private void startAdvertising() {
@@ -169,8 +181,8 @@
         }
         if (shouldUseV23()) {
             V23Manager.Singleton.get().mount(
-                    Participant.Mt.makeMountName(mDeck),
-                    new ParticipantServerImpl(mDeck));
+                    Config.MtDiscovery.makeMountName(mDeck),
+                    new ParticipantPeer.Server(mDeck));
             Log.d(TAG, "MT advertising started.");
         } else {
             Log.d(TAG, "No means to start advertising.");
@@ -211,7 +223,7 @@
             }
         });
         mRole = Role.PRESENTER;
-        showNavigateFragment(0);
+        showNavigateFragmentWithBackStack(0);
     }
 
     /**
@@ -240,8 +252,8 @@
     }
 
     /**
-     * Shows the navigate fragment where the user can see the current slide
-     * and navigate to other components of the slide presentation. This version
+     * 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.
      *
@@ -268,8 +280,8 @@
     }
 
     /**
-     * Return if the device is synced with the presenter (true if the device
-     * is the presenter).
+     * Return if the device is synced with the presenter (true if the device is
+     * the presenter).
      */
     public boolean getSynced() {
         return mSynced;
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 1a4a9c5..539e081 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
@@ -43,6 +43,12 @@
         return builder.create();
     }
 
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        ((PresentationActivity) getActivity()).setUiImmersive(true);
+    }
+
     // Send back the position of the questioner to the NavigateFragment.
     private void sendResult(int position) {
         Intent intent = new 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
new file mode 100644
index 0000000..3d225bc
--- /dev/null
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java
@@ -0,0 +1,234 @@
+// 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;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.CharStreams;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Signs in the user into one of his Gmail accounts.
+ */
+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 int REQUEST_CODE_PICK_ACCOUNT = 1000;
+    private static final int REQUEST_CODE_FETCH_USER_PROFILE_APPROVAL = 1001;
+
+    private static final String OAUTH_PROFILE = "email";
+    private static final String OAUTH_SCOPE = "oauth2:" + OAUTH_PROFILE;
+    private static final String OAUTH_USERINFO_URL =
+            "https://www.googleapis.com/oauth2/v2/userinfo";
+
+    private SharedPreferences mPrefs;
+    private String mAccountName;
+    private ProgressDialog mProgressDialog;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_sign_in);
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+        mAccountName = mPrefs.getString(SignInActivity.PREF_USER_ACCOUNT_NAME, "");
+        mProgressDialog = new ProgressDialog(this);
+        if (mAccountName.isEmpty()) {
+            mProgressDialog.setMessage("Signing in...");
+            mProgressDialog.show();
+            pickAccount();
+        } else {
+            finishActivity();
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_CODE_PICK_ACCOUNT: {
+                if (resultCode != RESULT_OK) {
+                    Toast.makeText(this, "Must pick account", Toast.LENGTH_LONG).show();
+                    pickAccount();
+                    break;
+                }
+                pickAccountDone(data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME));
+                break;
+            }
+            case REQUEST_CODE_FETCH_USER_PROFILE_APPROVAL:
+                if (resultCode != RESULT_OK) {
+                    Log.e(TAG, "User didn't approve oauth2 request");
+                    break;
+                }
+                fetchUserProfile();
+                break;
+        }
+    }
+
+    private void pickAccount() {
+        Intent chooseIntent = AccountManager.newChooseAccountIntent(
+                null, null, new String[]{"com.google"}, false, null, null, null, null);
+        startActivityForResult(chooseIntent, REQUEST_CODE_PICK_ACCOUNT);
+    }
+
+    private void pickAccountDone(String accountName) {
+        mAccountName = accountName;
+        SharedPreferences.Editor editor = mPrefs.edit();
+        editor.putString(PREF_USER_ACCOUNT_NAME, accountName);
+        editor.commit();
+
+        fetchUserProfile();
+    }
+
+    private void fetchUserProfile() {
+        AccountManager manager = (AccountManager) getSystemService(Context.ACCOUNT_SERVICE);
+        Account[] accounts = manager.getAccountsByType("com.google");
+        Account account = null;
+        for (int i = 0; i < accounts.length; i++) {
+            if (accounts[i].name.equals(mAccountName)) {
+                account = accounts[i];
+                break;
+            }
+        }
+        if (account == null) {
+            Log.e(TAG, "Couldn't find Google account with name: " + mAccountName);
+            pickAccount();
+            return;
+        }
+        manager.getAuthToken(account,
+                OAUTH_SCOPE,
+                new Bundle(),
+                false,
+                new OnTokenAcquired(),
+                new Handler(new Handler.Callback() {
+                    @Override
+                    public boolean handleMessage(Message msg) {
+                        Log.e(TAG, "Error getting auth token: " + msg.toString());
+                        fetchUserProfileDone(null);
+                        return true;
+                    }
+                }));
+    }
+
+    private void fetchUserProfileDone(JSONObject userProfile) {
+        if (userProfile != null) {
+            SharedPreferences.Editor editor = mPrefs.edit();
+            editor.putString(PREF_USER_PROFILE_JSON, userProfile.toString());
+            editor.commit();
+        }
+
+        finishActivity();
+    }
+
+
+    private void finishActivity() {
+        mProgressDialog.dismiss();
+        startActivity(new Intent(this, DeckChooserActivity.class));
+        finish();
+    }
+
+    private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
+        @Override
+        public void run(AccountManagerFuture<Bundle> result) {
+            try {
+                Bundle bundle = result.getResult();
+                Intent launch = (Intent) bundle.get(AccountManager.KEY_INTENT);
+                if (launch != null) {  // Needs user approval.
+                    // NOTE(spetrovic): The returned intent has the wrong flag value
+                    // FLAG_ACTIVITY_NEW_TASK set, which results in the launched intent replying
+                    // immediately with RESULT_CANCELED.  Hence, we clear the flag here.
+                    launch.setFlags(0);
+                    startActivityForResult(launch, REQUEST_CODE_FETCH_USER_PROFILE_APPROVAL);
+                    return;
+                }
+                String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
+                new ProfileInfoFetcher().execute(token);
+            } catch (AuthenticatorException e) {
+                Log.e(TAG, "Couldn't authorize: " + e.getMessage());
+                fetchUserProfileDone(null);
+            } catch (OperationCanceledException e) {
+                Log.e(TAG, "Authorization cancelled: " + e.getMessage());
+                fetchUserProfileDone(null);
+            } catch (IOException e) {
+                Log.e(TAG, "Unexpected error: " + e.getMessage());
+                fetchUserProfileDone(null);
+            }
+        }
+    }
+
+    private class ProfileInfoFetcher extends AsyncTask<String, Void, JSONObject> {
+        @Override
+        protected JSONObject doInBackground(String... params) {
+            try {
+                URL url = new URL(OAUTH_USERINFO_URL + "?access_token=" + params[0]);
+                return new JSONObject(CharStreams.toString(
+                        new InputStreamReader(url.openConnection().getInputStream(),
+                                Charsets.US_ASCII)));
+            } catch (MalformedURLException e) {
+                Log.e(TAG, "Error fetching user's profile info" + e.getMessage());
+            } catch (JSONException e) {
+                Log.e(TAG, "Error fetching user's profile info" + e.getMessage());
+            } catch (IOException e) {
+                Log.e(TAG, "Error fetching user's profile info" + e.getMessage());
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(JSONObject userProfile) {
+            fetchUserProfileDone(userProfile);
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // Inflate the menu; this adds items to the action bar if it is present.
+        getMenuInflater().inflate(R.menu.menu_sign_in, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle action bar item clicks here. The action bar will
+        // automatically handle clicks on the Home/Up button, so long
+        // as you specify a parent activity in AndroidManifest.xml.
+        int id = item.getItemId();
+
+        //noinspection SimplifiableIfStatement
+        if (id == R.id.action_settings) {
+            return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListFragment.java
index 06299c9..ba4070d 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SlideListFragment.java
@@ -9,11 +9,13 @@
 import android.support.v4.app.Fragment;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
 import io.v.android.apps.syncslides.db.DB;
+import io.v.android.apps.syncslides.model.Role;
 
 public class SlideListFragment extends Fragment {
     private static final String DECK_ID_KEY = "deck_id";
@@ -62,6 +64,8 @@
                 bundle = getArguments();
             }
             mDeckId = bundle.getString(DECK_ID_KEY);
+            Log.d(TAG, "onCreateView - Got deckId = " + mDeckId);
+
             mRole = (Role) bundle.get(ROLE_KEY);
         }
 
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 d3c193a..668b7e0 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
@@ -6,10 +6,10 @@
 
 import android.app.Activity;
 import android.content.Context;
-import android.content.Intent;
 
 import java.util.List;
 
+import io.v.android.apps.syncslides.misc.Config;
 import io.v.android.apps.syncslides.model.Deck;
 import io.v.android.apps.syncslides.model.Listener;
 import io.v.android.apps.syncslides.model.Question;
@@ -22,15 +22,15 @@
 public interface DB {
     class Singleton {
         private static volatile DB instance;
+
         public static DB get(Context context) {
             DB result = instance;
             if (instance == null) {
                 synchronized (Singleton.class) {
                     result = instance;
                     if (result == null) {
-                        // Switch between FakeDB and SyncbaseDB by commenting out one.
-                        instance = result = new FakeDB(context);
-                        // instance = result = new SyncbaseDB(context);
+                        instance = result = Config.Syncbase.ENABLE ?
+                                new SyncbaseDB(context) : new FakeDB(context);
                     }
                 }
             }
@@ -39,16 +39,18 @@
     }
 
     /**
-     * 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.
+     * 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.
+     * @param activity implements onActivityResult() to call into
+     *                 DB.onActivityResult.
      */
     void init(Activity activity);
 
     /**
-     * Provides a list of elements via an API that fits well with RecyclerView.Adapter.
+     * Provides a list of elements via an API that fits well with
+     * RecyclerView.Adapter.
      */
     interface DBList<E> {
 
@@ -63,12 +65,14 @@
         E get(int i);
 
         /**
-         * Sets the listener for changes to the list.  There can only be one listener.
+         * Sets the listener for changes to the list.  There can only be one
+         * listener.
          */
         void setListener(Listener listener);
 
         /**
-         * Indicates that the list is no longer needed and should stop notifying its listener.
+         * Indicates that the list is no longer needed and should stop notifying
+         * its listener.
          */
         void discard();
     }
@@ -99,7 +103,7 @@
     /**
      * Asynchronously fetch the slides for the given deck.
      *
-     * @param deckId the deck to fetch
+     * @param deckId   the deck to fetch
      * @param callback runs on the UI thread when the slide data is loaded
      */
     void getSlides(String deckId, Callback<List<Slide>> callback);
@@ -122,29 +126,36 @@
     void importDeck(Deck deck, Slide[] slides, Callback<Void> callback);
 
     /**
+     * Synchronously gets a deck by Id.  Returns null if not found.
+     *
+     * @param deckId id of the deck to get
+     */
+    Deck getDeck(String deckId);
+
+    /**
      * Asynchronously deletes the deck and all of its slides.
      *
-     * @param deckId  id of the deck to delete
+     * @param deckId id of the deck to delete
      */
     void deleteDeck(String deckId);
 
     /**
      * Asynchronously deletes the deck and all of its slides.
      *
-     * @param deckId    id of the deck to delete
-     * @param callback  runs on the UI thread when the deck has been deleted
+     * @param deckId   id of the deck to delete
+     * @param callback runs on the UI thread when the deck has been deleted
      */
     void deleteDeck(String deckId, Callback<Void> callback);
 
     class CreatePresentationResult {
         /**
-         * A unique ID for the presentation.  All methods that deal with live presentation
-         * data (e.g. the current slide) use this ID.
+         * A unique ID for the presentation.  All methods that deal with live
+         * presentation data (e.g. the current slide) use this ID.
          */
         public String presentationId;
         /**
-         * This is the name of the syncgroup that was created for this presentation instance.
-         * Audience members must join this syncgroup.
+         * This is the name of the syncgroup that was created for this
+         * presentation instance. Audience members must join this syncgroup.
          */
         public String syncgroupName;
 
@@ -157,7 +168,7 @@
     /**
      * Creates a new presentation by creating a syncgroup.
      *
-     * @param deckId the deck to use in the presentation
+     * @param deckId   the deck to use in the presentation
      * @param callback called when the presentation is created
      */
     void createPresentation(String deckId, Callback<CreatePresentationResult> callback);
@@ -166,16 +177,16 @@
      * Joins an existing presentation.
      *
      * @param syncgroupName the syncgroup to join
-     * @param callback called when the syncgroup is joined
+     * @param callback      called when the syncgroup is joined
      */
     void joinPresentation(String syncgroupName, Callback<Void> callback);
 
     /**
      * Sets the current slide so any audience members can switch to it.
      *
-     * @param deckId the deck being presented
+     * @param deckId         the deck being presented
      * @param presentationId the instance of the live presentation
-     * @param slideNum the new slide number
+     * @param slideNum       the new slide number
      */
     void setCurrentSlide(String deckId, String presentationId, int slideNum);
 
@@ -191,9 +202,9 @@
     /**
      * Add a listener for changes to the current slide of a live presentation.
      *
-     * @param deckId the deck used in the presentation
+     * @param deckId         the deck used in the presentation
      * @param presentationId the presentation to watch for changes
-     * @param listener notified of changes
+     * @param listener       notified of changes
      */
     void addCurrentSlideListener(String deckId, String presentationId,
                                  CurrentSlideListener listener);
@@ -201,9 +212,9 @@
     /**
      * Remove a listener that was previously passed to addCurrentSlideListener().
      *
-     * @param deckId the deck used in the presentation
+     * @param deckId         the deck used in the presentation
      * @param presentationId the presentation being watched for changes
-     * @param listener previously passed to addCurrentSlideListener()
+     * @param listener       previously passed to addCurrentSlideListener()
      */
     void removeCurrentSlideListener(String deckId, String presentationId,
                                     CurrentSlideListener listener);
@@ -218,12 +229,12 @@
     }
 
     /**
-     * Set the listener for changes to the set of questions for a live presentation.
-     * There can be only one listener at a time.
+     * Set the listener for changes to the set of questions for a live
+     * presentation. There can be only one listener at a time.
      *
-     * @param deckId the deck used in the presentation
+     * @param deckId         the deck used in the presentation
      * @param presentationId the presentation to watch for changes
-     * @param listener notified of changes
+     * @param listener       notified of changes
      */
     void setQuestionListener(String deckId, String presentationId,
                              QuestionListener listener);
@@ -231,9 +242,9 @@
     /**
      * Remove the listener that was previously passed to setQuestionListener().
      *
-     * @param deckId the deck used in the presentation
+     * @param deckId         the deck used in the presentation
      * @param presentationId the presentation being watched for changes
-     * @param listener previously passed to setQuestionListener()
+     * @param listener       previously passed to setQuestionListener()
      */
     void removeQuestionListener(String deckId, String presentationId,
                                 QuestionListener listener);
@@ -241,10 +252,10 @@
     /**
      * Add user to presenter's question queue.
      *
-     * @param deckId the deck used in the presentation
+     * @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 firstName      the user's first name
+     * @param lastName       the user's last name
      */
     void askQuestion(String deckId, String presentationId,
                      String firstName, String lastName);
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 7eaddeb..cd41a7d 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
@@ -6,32 +6,28 @@
 
 import android.app.Activity;
 import android.content.Context;
-import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Log;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 
-import java.util.Collections;
-import java.util.List;
-
-import com.google.common.collect.ImmutableList;
-
 import org.joda.time.DateTime;
-import org.joda.time.Duration;
 import org.joda.time.Period;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Random;
 
 import io.v.android.apps.syncslides.R;
 import io.v.android.apps.syncslides.model.Deck;
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
 import io.v.android.apps.syncslides.model.Listener;
 import io.v.android.apps.syncslides.model.Question;
 import io.v.android.apps.syncslides.model.Slide;
@@ -91,10 +87,8 @@
         }
         mHandler = new Handler(Looper.getMainLooper());
         for (int i = 0; i < DECKTHUMBS.length; ++i) {
-            mDecks.add(new DeckImpl(
-                    DECKTITLES[i],
-                    BitmapFactory.decodeResource(context.getResources(), DECKTHUMBS[i]),
-                    String.valueOf(i)));
+            mDecks.add(DeckFactory.Singleton.get(context).make(
+                    DECKTITLES[i], DECKTHUMBS[i], i));
             mSlides.put(String.valueOf(i), new FakeSlideList(slides));
         }
         mCurrentSlideListeners = Lists.newArrayList();
@@ -110,9 +104,9 @@
         Random random = new Random();
         for (int i = 0; i < 5; i++) {
             Question question = new Question(
-                    "queston"+i,
+                    "question" + i,
                     "Questioner",
-                    "#"+i,
+                    "#" + i,
                     DateTime.now().minus(Period.minutes(random.nextInt(5))));
             mQuestions.add(question);
         }
@@ -332,6 +326,17 @@
     }
 
     @Override
+    public Deck getDeck(String deckId) {
+        for (int i = 0; i < mDecks.getItemCount(); i++) {
+            Deck result = mDecks.get(i);
+            if (result.getId().equals(deckId)) {
+                return result;
+            }
+        }
+        return null;
+    }
+
+    @Override
     public void deleteDeck(String deckId) {
         mDecks.delete(deckId);
     }
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 e6b487a..a599bdb 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
@@ -31,9 +31,9 @@
 import java.util.UUID;
 
 import io.v.android.apps.syncslides.R;
-import io.v.android.apps.syncslides.discovery.V23Manager;
+import io.v.android.apps.syncslides.misc.V23Manager;
 import io.v.android.apps.syncslides.model.Deck;
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
 import io.v.android.apps.syncslides.model.Listener;
 import io.v.android.apps.syncslides.model.NoopList;
 import io.v.android.apps.syncslides.model.Slide;
@@ -93,11 +93,13 @@
     private final Map<String, CurrentSlideWatcher> mCurrentSlideWatchers;
     private final Map<String, QuestionWatcher> mQuestionWatchers;
     private Server mSyncbaseServer;
+    private final DeckFactory mDeckFactory;
 
     SyncbaseDB(Context context) {
         mContext = context;
         mCurrentSlideWatchers = Maps.newHashMap();
         mQuestionWatchers = Maps.newHashMap();
+        mDeckFactory = DeckFactory.Singleton.get(context);
     }
 
     @Override
@@ -122,7 +124,7 @@
         // work so DB methods should return noop values.  It's assumed that
         // the calling fragment will send the user to the AccountManager,
         // accept blessings on return, then re-call this init.
-        if (V23Manager.Singleton.get().isBlessed()) {
+        if (!V23Manager.Singleton.get().isBlessed()) {
             Log.d(TAG, "no blessings.");
             return;
         }
@@ -279,6 +281,7 @@
             @Override
             public void run() {
                 try {
+                    Log.i(TAG, "Joining: " + syncgroupName);
                     Syncgroup syncgroup = mDB.getSyncgroup(syncgroupName);
                     syncgroup.join(mVContext, new SyncgroupMemberInfo((byte) 1));
                     for (String member : syncgroup.getMembers(mVContext).keySet()) {
@@ -304,22 +307,24 @@
         if (!mInitialized) {
             return new NoopList<>();
         }
-        return new DeckList(mVContext, mDB);
+        return new DeckList(mVContext, mDB, mDeckFactory);
     }
 
-    private static class DeckList implements DBList {
+    private static class DeckList implements DBList<Deck> {
 
         private final CancelableVContext mVContext;
         private final Database mDB;
+        private final DeckFactory mDeckFactory;
         private final Handler mHandler;
         private ResumeMarker mWatchMarker;
-        private volatile boolean mIsDiscarded;
-        private volatile Listener mListener;
+        private boolean mIsDiscarded;
+        private Listener mListener;
         private List<Deck> mDecks;
 
-        public DeckList(VContext vContext, Database db) {
+        public DeckList(VContext vContext, Database db, DeckFactory df) {
             mVContext = vContext.withCancel();
             mDB = db;
+            mDeckFactory = df;
             mIsDiscarded = false;
             mDecks = Lists.newArrayList();
             mHandler = new Handler(Looper.getMainLooper());
@@ -345,12 +350,7 @@
                     }
                     String key = (String) row.get(0).getElem();
                     Log.i(TAG, "Fetched deck " + key);
-                    VDeck vDeck = (VDeck) row.get(1).getElem();
-                    final Deck deck = new DeckImpl(
-                            vDeck.getTitle(),
-                            BitmapFactory.decodeByteArray(
-                                    vDeck.getThumbnail(), 0, vDeck.getThumbnail().length),
-                            key);
+                    final Deck deck = mDeckFactory.make((VDeck) row.get(1).getElem(), key);
                     mHandler.post(new Runnable() {
                         @Override
                         public void run() {
@@ -393,11 +393,7 @@
                     } catch (VException e) {
                         Log.e(TAG, "Couldn't decode deck: " + e.toString());
                     }
-                    final Deck deck =
-                            new DeckImpl(vDeck.getTitle(),
-                                    BitmapFactory.decodeByteArray(
-                                            vDeck.getThumbnail(), 0, vDeck.getThumbnail().length),
-                                    key);
+                    final Deck deck = mDeckFactory.make(vDeck, key);
                     mHandler.post(new Runnable() {
                         @Override
                         public void run() {
@@ -439,18 +435,20 @@
         }
 
         @Override
-        public synchronized void discard() {
+        public void discard() {
             Log.i(TAG, "Discarding deck list.");
+            mIsDiscarded = true;
             mVContext.cancel();  // this will cause the watcher thread to exit
             mHandler.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. (Note that
-            // put()/get() are synchronized along with this method.)
-            mIsDiscarded = true;
         }
 
-        private synchronized void put(Deck deck) {
+        private void put(Deck deck) {
+            // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+            // been called), because that method doesn't prevent future post()s being made on
+            // the handler.  So the following scenario is possible:
+            //    - fetcher thread is about to execute post().
+            //    - discard clears all pending messages from the handler.
+            //    - fetcher executes the post().
             if (mIsDiscarded) {
                 return;
             }
@@ -477,7 +475,13 @@
             }
         }
 
-        private synchronized void delete(String deckId) {
+        private void delete(String deckId) {
+            // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+            // been called), because that method doesn't prevent future post()s being made on
+            // the handler.  So the following scenario is possible:
+            //    - fetcher thread is about to execute post().
+            //    - discard clears all pending messages from the handler.
+            //    - fetcher executes the post().
             if (mIsDiscarded) {
                 return;
             }
@@ -552,7 +556,7 @@
         private final Handler mHandler;
         private final String mDeckId;
         private ResumeMarker mWatchMarker;
-        private volatile boolean mIsDiscarded;
+        private boolean mIsDiscarded;
         // Storage for slides, mirroring the slides in the Syncbase.  Since slide numbers can
         // have "holes" in them (e.g., 1, 2, 4, 6, 8), we maintain a map from slide key
         // to the slide, as well as an ordered list which is returned to the caller.
@@ -684,18 +688,20 @@
         }
 
         @Override
-        public synchronized void discard() {
+        public void discard() {
             Log.i(TAG, "Discarding slides list");
+            mIsDiscarded = true;
             mVContext.cancel();  // this will cause the watcher thread to exit
             mHandler.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. (Note that
-            // put()/get() are synchronized along with this method.)
-            mIsDiscarded = true;
         }
 
-        private synchronized void put(String key, Slide slide) {
+        private void put(String key, Slide slide) {
+            // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+            // been called), because that method doesn't prevent future post()s being made on
+            // the handler.  So the following scenario is possible:
+            //    - fetcher thread is about to execute post().
+            //    - discard clears all pending messages from the handler.
+            //    - fetcher executes the post().
             if (mIsDiscarded) {
                 return;
             }
@@ -715,7 +721,13 @@
             }
         }
 
-        private synchronized void delete(String key) {
+        private void delete(String key) {
+            // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+            // been called), because that method doesn't prevent future post()s being made on
+            // the handler.  So the following scenario is possible:
+            //    - fetcher thread is about to execute post().
+            //    - discard clears all pending messages from the handler.
+            //    - fetcher executes the post().
             if (mIsDiscarded) {
                 return;
             }
@@ -949,6 +961,20 @@
     }
 
     @Override
+    public Deck getDeck(String deckId) {
+        VDeck vDeck = null;
+        try {
+            vDeck = (VDeck) mDecks.get(mVContext, deckId, VDeck.class);
+        } catch (VException e) {
+            handleError(e.toString());
+        }
+        if (vDeck != null) {
+            return mDeckFactory.make(vDeck, deckId);
+        }
+        return null;
+    }
+
+    @Override
     public void deleteDeck(String deckId) {
         try {
             mDecks.deleteRange(mVContext, RowRange.prefix(deckId));
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 539d5b8..57321c8 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
@@ -13,6 +13,8 @@
 import java.util.List;
 
 import io.v.android.apps.syncslides.db.DB;
+import io.v.android.apps.syncslides.misc.Config;
+import io.v.android.apps.syncslides.misc.V23Manager;
 import io.v.android.apps.syncslides.model.Deck;
 import io.v.android.apps.syncslides.model.Listener;
 import io.v.android.apps.syncslides.model.Participant;
@@ -37,19 +39,19 @@
     private final Handler mHandler;
     private Listener mListener;
 
-    public static DiscoveryManager make() {
+    public static DiscoveryManager make(Context context) {
         // If blessings not in place, use fake data.
         boolean useRealDiscovery =
-                Participant.ENABLE_MT_DISCOVERY &&
+                Config.MtDiscovery.ENABLE &&
                         V23Manager.Singleton.get().isBlessed();
         if (useRealDiscovery) {
             Log.d(TAG, "Using real discovery.");
             return new DiscoveryManager(
-                    new Moderator(new ParticipantScannerMt()));
+                    new Moderator(new ParticipantScannerMt(context)));
         }
         Log.d(TAG, "Using fake discovery.");
         return new DiscoveryManager(
-                new Moderator(new ParticipantScannerFake()));
+                new Moderator(new ParticipantScannerFake(context)));
     }
 
     private DiscoveryManager(Moderator moderator) {
@@ -121,7 +123,6 @@
     public void discard() {
         Log.d(TAG, "Discarding.");
         stop();
-        mListener = null;
     }
 }
 
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/Moderator.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/Moderator.java
index 4e2f6c8..6354634 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/Moderator.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/Moderator.java
@@ -37,8 +37,6 @@
     private final ParticipantScanner mScanner;
     // Notify this guy when task done; make it a list if more needed.
     private Observer mObserver;
-    // Counts runs for debugging.
-    private int mCounter = 0;
 
     public Moderator(ParticipantScanner scanner) {
         mScanner = scanner;
@@ -62,7 +60,6 @@
             throw new IllegalStateException("Must have an observer.");
         }
         try {
-            mCounter++;
             process(mScanner.scan());
             mObserver.onTaskDone();
         } catch (Throwable t) {
@@ -78,11 +75,13 @@
         mSenior.clear();
         mFreshman.clear();
 
+        Set<Participant> potentials = new HashSet<>();
+
         for (Participant p : latest) {
             if (current.contains(p)) {
                 mSenior.add(p);
             } else {
-                mFreshman.add(p);
+                potentials.add(p);
             }
         }
 
@@ -93,8 +92,11 @@
             }
         }
 
-        for (Participant p : mFreshman) {
-            p.refreshData();
+
+        for (Participant p : potentials) {
+            if (p.refreshData()) {
+                mFreshman.add(p);
+            }
         }
     }
 
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 9452f5b..ff7e70c 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
@@ -4,15 +4,22 @@
 
 package io.v.android.apps.syncslides.discovery;
 
+import android.graphics.Bitmap;
 import android.util.Log;
 
 import org.joda.time.DateTime;
 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.misc.V23Manager;
 import io.v.android.apps.syncslides.model.Deck;
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
 import io.v.android.apps.syncslides.model.Participant;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.ServerCall;
 import io.v.v23.verror.VException;
 
 /**
@@ -35,7 +42,7 @@
     private static final DateTimeFormatter TIME_FMT =
             DateTimeFormat.forPattern("hh_mm_ss_SSSS");
     // V23 name of the V23 service representing the participant.
-    private String mServiceName;
+    private final String mServiceName;
     // Visible name of human presenter.
     // TODO(jregan): Switch to VPerson or the model equivalent.
     private String mUserName;
@@ -43,25 +50,30 @@
     private DateTime mRefreshTime;
     // Deck the user is presenting.  Can only present one at a time.
     private Deck mDeck;
-    private ParticipantClient mClient = null;
+    // Used to make decks after RPCs.
+    private final DeckFactory mDeckFactory;
 
-    public ParticipantPeer(String userName, Deck deck, String serviceName) {
+    private ParticipantPeer(
+            String userName, Deck deck, String serviceName, DeckFactory deckFactory) {
         mUserName = userName;
         mDeck = deck;
         mServiceName = serviceName;
+        mDeckFactory = deckFactory;
     }
 
-    public ParticipantPeer(String userName, Deck deck) {
-        this(userName, deck, Unknown.SERVER_NAME);
+    public static ParticipantPeer makeWithServiceName(
+            String serviceName, DeckFactory deckFactory) {
+        return new ParticipantPeer(Unknown.USER_NAME, null, serviceName, deckFactory);
     }
 
-    public ParticipantPeer(String serviceName) {
-        this(Unknown.USER_NAME, DeckImpl.DUMMY, serviceName);
+    public static ParticipantPeer makeWithKnownDeck(String userName, Deck deck) {
+        return new ParticipantPeer(userName, deck, Unknown.SERVER_NAME, null);
     }
 
     @Override
     public String getServiceName() {
-        return mServiceName;
+        return (mServiceName != null && !mServiceName.isEmpty()) ?
+                mServiceName : Unknown.SERVER_NAME;
     }
 
     @Override
@@ -92,41 +104,87 @@
             return false;
         }
         ParticipantPeer p = (ParticipantPeer) obj;
-        return mServiceName.equals(p.mServiceName) && mDeck.equals(p.mDeck);
+        boolean deckEqual = (mDeck == null) ? true : mDeck.equals(p.mDeck);
+        return deckEqual && getServiceName().equals(p.getServiceName());
     }
 
     @Override
     public int hashCode() {
-        return mServiceName.hashCode() + mDeck.hashCode();
+        int deckCode = (mDeck == null) ? 0 : mDeck.hashCode();
+        return deckCode + getServiceName().hashCode();
     }
 
     /**
      * Make an RPC on the mServiceName to get title, snapshot, etc.
      */
     @Override
-    public void refreshData() {
-        Log.d(TAG, "Initiating refresh");
-
-        if (mClient == null) {
-            Log.d(TAG, "Grabbing client.");
-            mClient = ParticipantClientFactory.getParticipantClient(
-                    mServiceName);
-            Log.d(TAG, "Got client.");
+    public boolean refreshData() {
+        if (mServiceName.equals(Unknown.SERVER_NAME)) {
+            // Don't attempt refresh.
+            return true;
         }
+        Log.d(TAG, "refreshData");
+        // Flush, since the server might have died and restarted, invalidating
+        // cached endpoints.
+        Log.d(TAG, "Flushing cache for service " + mServiceName);
+        V23Manager.Singleton.get().flushServerFromCache(mServiceName);
+        ParticipantClient client =
+                ParticipantClientFactory.getParticipantClient(mServiceName);
+        Log.d(TAG, "Got client = " + client.toString());
         try {
-            Log.d(TAG, "Calling get");
-            Description description = mClient.get(
+            Log.d(TAG, "Calling get...");
+            VDeck vDeck = client.get(
                     V23Manager.Singleton.get().getVContext());
-            mDeck = new DeckImpl(description.getTitle());
+            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");
             mRefreshTime = DateTime.now();
-            Log.d(TAG, "Completed refresh.");
+            mDeck = newDeck;
+            Log.d(TAG, "  Got deck = " + mDeck);
+            return true;
         } catch (VException e) {
+            Log.d(TAG, "RPC failed, leaving current deck in place.");
             e.printStackTrace();
         }
+        return false;
     }
 
     private static class Unknown {
         static final String SERVER_NAME = "unknownServerName";
         static final String USER_NAME = "unknownUserName";
     }
+
+    /**
+     * Serves data used in deck discovery.
+     */
+    public static class Server implements ParticipantServer {
+        private static final String TAG = "ParticipantServer";
+        private final Deck mDeck;
+
+        public Server(Deck d) {
+            mDeck = d;
+        }
+
+        public VDeck 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;
+        }
+    }
 }
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 0d6defd..7db977e 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
@@ -4,32 +4,44 @@
 
 package io.v.android.apps.syncslides.discovery;
 
+import android.content.Context;
+
 import java.util.HashSet;
 import java.util.Set;
 
-import io.v.android.apps.syncslides.model.DeckImpl;
+import io.v.android.apps.syncslides.model.DeckFactory;
 import io.v.android.apps.syncslides.model.Participant;
 
 public class ParticipantScannerFake implements ParticipantScanner {
     private static final String TAG = "ParticipantScannerFake";
 
+    protected final DeckFactory mDeckFactory;
+
     private int mCounter = 0;
 
+    public ParticipantScannerFake(Context context) {
+        this.mDeckFactory = DeckFactory.Singleton.get(context);
+    }
+
     public Set<Participant> scan() {
         mCounter = (mCounter + 1) % 10;
         HashSet<Participant> participants = new HashSet<>();
         if (mCounter >= 2 && mCounter <= 8) {
             participants.add(
-                    new ParticipantPeer(
-                            "Alice", new DeckImpl(
-                            "Kale - Just eat it.", null, "deckByAlice")));
+                    ParticipantPeer.makeWithKnownDeck(
+                            "Alice",
+                            mDeckFactory.make(
+                                    "Kale - Just eat it.",
+                                    "deckByAlice")));
         }
         // Bob has less to say than Alice.
         if (mCounter >= 4 && mCounter <= 6) {
             participants.add(
-                    new ParticipantPeer(
-                            "Bob", new DeckImpl(
-                            "Java - Object deluge.", null, "deckByBob")));
+                    ParticipantPeer.makeWithKnownDeck(
+                            "Bob",
+                            mDeckFactory.make(
+                                    "Java - Object deluge.",
+                                    "deckByBob")));
         }
         return participants;
     }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerMt.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerMt.java
index af17b3c..4ba8c32 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerMt.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantScannerMt.java
@@ -4,11 +4,15 @@
 
 package io.v.android.apps.syncslides.discovery;
 
+import android.content.Context;
 import android.util.Log;
 
 import java.util.HashSet;
 import java.util.Set;
 
+import io.v.android.apps.syncslides.misc.Config;
+import io.v.android.apps.syncslides.misc.V23Manager;
+import io.v.android.apps.syncslides.model.DeckFactory;
 import io.v.android.apps.syncslides.model.Participant;
 
 /**
@@ -17,13 +21,19 @@
 public class ParticipantScannerMt implements ParticipantScanner {
     private static final String TAG = "ParticipantScannerMt";
 
+    protected final DeckFactory mDeckFactory;
+
+    public ParticipantScannerMt(Context context) {
+        this.mDeckFactory = DeckFactory.Singleton.get(context);
+    }
+
     @Override
     public Set<Participant> scan() {
         Set<Participant> result = new HashSet<>();
         for (String n : V23Manager.Singleton.get().scan(
-                Participant.Mt.makeScanString())) {
+                Config.MtDiscovery.makeScanString())) {
             Log.d(TAG, "Found: " + n);
-            result.add(new ParticipantPeer(n));
+            result.add(ParticipantPeer.makeWithServiceName(n, mDeckFactory));
         }
         return result;
     }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantServerImpl.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantServerImpl.java
deleted file mode 100644
index fbd251a..0000000
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantServerImpl.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package io.v.android.apps.syncslides.discovery;
-
-import android.util.Log;
-
-import io.v.android.apps.syncslides.model.Deck;
-import io.v.v23.context.VContext;
-import io.v.v23.rpc.ServerCall;
-
-/**
- * Serves data used in deck discovery.
- */
-public class ParticipantServerImpl implements ParticipantServer {
-    private static final String TAG = "PresentationActivity";
-    private final Deck mDeck;
-
-    public ParticipantServerImpl(Deck d) {
-        mDeck = d;
-    }
-
-    public Description get(VContext ctx, ServerCall call)
-            throws io.v.v23.verror.VException {
-        Log.d(TAG, "Responding to Get RPC.");
-        Description d = new Description();
-        d.setTitle(mDeck.getTitle());
-        d.setUserName(mDeck.getId());
-        return d;
-    }
-}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/PeriodicTasker.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/PeriodicTasker.java
index 306c63f..bdd2b21 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/PeriodicTasker.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/PeriodicTasker.java
@@ -8,13 +8,12 @@
 
 import org.joda.time.Duration;
 
-import java.util.Timer;
-import java.util.TimerTask;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
+import io.v.android.apps.syncslides.misc.V23Manager;
+
 /**
  * Repeatedly runs a task in a thread distinct from that which calls start().
  */
@@ -22,10 +21,10 @@
     private static final String TAG = "PeriodicTasker";
 
     private static final Duration DELAY_BEFORE_FIRST_TASK =
-            Duration.standardSeconds(2);
+            Duration.standardSeconds(1);
 
     private static final Duration WAIT_BETWEEN_TASKS =
-            V23Manager.MT_TIMEOUT.plus(Duration.standardSeconds(3));
+            V23Manager.MT_TIMEOUT.plus(Duration.millis(500));
 
     private ScheduledExecutorService mTimer = null;
 
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 7965344..6b011c9 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,20 +8,6 @@
     "io/v/android/apps/syncslides/db"
 )
 
-type Description struct {
-	UserName string
-	Title string
-}
-
 type Participant interface {
-	Get() (Description | error)
-}
-
-// TODO(jregan):  Ditch Description in favor of db.VDeck.
-// so we get thumb automatically.  Rewire the remaining
-// code.  Add whatever else we need.
-// This is just a test of the code generator, and should
-// be removed shortly.
-type ChuckleParticipant interface {
 	Get() (db.VDeck | 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
new file mode 100644
index 0000000..ee26d1f
--- /dev/null
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/Config.java
@@ -0,0 +1,61 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.apps.syncslides.misc;
+
+import io.v.android.apps.syncslides.model.Deck;
+
+/**
+ * Syncslides configuration.
+ */
+public class Config {
+    /**
+     * Which Mounttable to use for everything.
+     */
+    public static final String MT_ADDRESS = Tables.PI_MILK_CRATE;
+
+    /**
+     * Some fixed mount tables to try.
+     */
+    private static class Tables {
+        static final String PI_MILK_CRATE = "192.168.86.254:8101";
+        static final String JR_LAPTOP_AT_HOME = "192.168.2.71:23000";
+        static final String JR_LAPTOP_VEYRON = "192.168.8.106:23000";
+        static final String JR_MOTOX = "192.168.43.136:23000";
+    }
+
+    public static class Syncbase {
+        /**
+         * If true, enable use of syncbase as the DB, else use a fake.
+         */
+        public static final boolean ENABLE = false;
+    }
+
+    public static class MtDiscovery {
+        /**
+         * If true, enable MT-based (mounttable) discovery, else use a fake. If
+         * enabled, DeckChooserActivity will scan a MT to find live
+         * presentations. Clicking play on a presentation will start a service
+         * and try to mount it in a MT so other deck views can list it in the
+         * UX.
+         */
+        public static final boolean ENABLE = true;
+        /**
+         * Every v23 service will be mounted in the namespace with a name
+         * prefixed by this.
+         */
+        private static String LIVE_PRESENTATION_PREFIX = "liveDeck";
+
+        /**
+         * TODO(jregan): Assure legal mount name (remove blanks and such).
+         */
+        public static String makeMountName(Deck deck) {
+            return LIVE_PRESENTATION_PREFIX + "/" + deck.getId();
+        }
+
+        public static String makeScanString() {
+            return LIVE_PRESENTATION_PREFIX + "/*";
+        }
+    }
+}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/V23Manager.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
similarity index 94%
rename from projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/V23Manager.java
rename to projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
index 1966ab3..734d79e 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/V23Manager.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
@@ -2,11 +2,13 @@
 // 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.discovery;
+package io.v.android.apps.syncslides.misc;
 
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
 import android.util.Log;
 import android.widget.Toast;
 
@@ -20,6 +22,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
+import io.v.android.apps.syncslides.SignInActivity;
 import io.v.android.libs.security.BlessingsManager;
 import io.v.android.v23.V;
 import io.v.android.v23.services.blessing.BlessingCreationException;
@@ -56,8 +59,6 @@
     private static final String TAG = "V23Manager";
     private static final ExecutorService mExecutor =
             Executors.newSingleThreadExecutor();
-    private static final String MT_ADDRESS = FixedMt.PI_MILK_CRATE;
-    // private static final String MT_ADDRESS = FixedMt.JR_MOTOX;
     private Context mAndroidCtx;
     private VContext mBaseContext = null;
     private Blessings mBlessings = null;
@@ -124,12 +125,12 @@
      */
     public static List<String> determineNamespaceRoot() {
         List<String> result = new ArrayList<>();
-        result.add("/" + MT_ADDRESS);
+        result.add("/" + Config.MT_ADDRESS);
         return result;
     }
 
     public static String syncName(String id) {
-        return NamingUtil.join("/", MT_ADDRESS, id);
+        return NamingUtil.join("/", Config.MT_ADDRESS, id);
     }
 
     public VContext getVContext() {
@@ -174,14 +175,21 @@
                 throw new IllegalArgumentException(
                         "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, "");
             activity.startActivityForResult(
-                    BlessingService.newBlessingIntent(androidCtx),
+                    BlessingService.newBlessingIntent(androidCtx, userEmail),
                     BLESSING_REQUEST);
             return;
         }
         asyncConfigurePrincipal(blessings);
     }
 
+    public void flushServerFromCache(String name) {
+        V.getNamespace(mBaseContext).flushCacheEntry(mBaseContext, name);
+    }
+
     /**
      * v23 operations that require a blessing (almost everything) will fail if
      * attempted before this is true.
@@ -311,7 +319,7 @@
             @Override
             public void run() {
                 Log.d(TAG, "mounting on name \"" + mountName +
-                        "\" at table " + MT_ADDRESS);
+                        "\" at table " + Config.MT_ADDRESS);
                 try {
                     mLiveServer = makeServer(mountName, server);
                     Log.d(TAG, "  Server status proxies: " +
@@ -379,13 +387,4 @@
         }
     }
 
-    /**
-     * Some fixed mount tables to try.
-     */
-    private static class FixedMt {
-        static final String PI_MILK_CRATE = "192.168.86.254:8101";
-        static final String JR_LAPTOP_AT_HOME = "192.168.2.71:23000";
-        static final String JR_LAPTOP_VEYRON = "192.168.8.106:23000";
-        static final String JR_MOTOX = "192.168.43.136:23000";
-    }
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Deck.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Deck.java
index adf2ec0..642894a 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Deck.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Deck.java
@@ -28,17 +28,9 @@
     String getId();
 
     /**
-     * Returns a bundled form of the instance; pass null for a new bundle,
-     * pass an existing bundle to overwrite its fields.
-     */
-    Bundle toBundle(Bundle b);
-
-    /**
-     * Keys for Bundle fields.
+     * Keys for Bundle/Intent fields.
      */
     class B {
         public static final String DECK_ID = "deck_id";
-        public static final String DECK_TITLE = "deck_title";
-        public static final String DECK_THUMB = "deck_thumb";
     }
 }
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
new file mode 100644
index 0000000..1dec64c
--- /dev/null
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckFactory.java
@@ -0,0 +1,96 @@
+// 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.model;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import io.v.android.apps.syncslides.R;
+import io.v.android.apps.syncslides.db.VDeck;
+
+/**
+ * One place to make consistent decks with known defaults.
+ */
+public class DeckFactory {
+    private static final String TAG = "DeckFactory";
+    protected final Bitmap mDefaultThumb;
+    private final Context mContext;
+
+    // Singleton
+    private DeckFactory(Context c) {
+        mContext = c;
+        mDefaultThumb = makeDefaultThumb(c);
+    }
+
+    private static Bitmap makeDefaultThumb(Context c) {
+        return BitmapFactory.decodeResource(
+                c.getResources(), R.drawable.thumb_deck1);
+    }
+
+    public Deck make() {
+        return make(Unknown.TITLE, Unknown.ID);
+    }
+
+    public Deck make(String title, String id) {
+        return make(title, mDefaultThumb, id);
+    }
+
+    public Deck make(String title, int index, int id) {
+        return make(
+                title,
+                BitmapFactory.decodeResource(mContext.getResources(), index),
+                String.valueOf(id));
+    }
+
+    public Deck make(VDeck vDeck, String id) {
+        if (vDeck.getThumbnail() == null) {
+            Log.d(TAG, "vDeck missing thumb; vdeck = " + vDeck);
+        }
+        return make(
+                vDeck.getTitle(),
+                vDeck.getThumbnail() == null ? null :
+                        BitmapFactory.decodeByteArray(
+                                vDeck.getThumbnail(), 0, vDeck.getThumbnail().length),
+                id);
+    }
+
+    public Deck make(String title, Bitmap thumb, String id) {
+        title = (title == null || title.isEmpty()) ? Unknown.TITLE : title;
+        thumb = (thumb == null) ? mDefaultThumb : thumb;
+        id = (id == null || id.isEmpty()) ? Unknown.ID : id;
+        return new DeckImpl(title, thumb, id);
+    }
+
+    public static class Singleton {
+        private static volatile DeckFactory instance;
+
+        public static DeckFactory get(Context c) {
+            DeckFactory result = instance;
+            if (instance == null) {
+                synchronized (Singleton.class) {
+                    result = instance;
+                    if (result == null) {
+                        instance = result = new DeckFactory(c);
+                    }
+                }
+            }
+            return result;
+        }
+
+        public static DeckFactory get() {
+            if (instance == null) {
+                throw new IllegalStateException("Must initialize with context.");
+            }
+            return instance;
+        }
+    }
+
+    private static class Unknown {
+        static final String TITLE = "unknownTitle";
+        static final String ID = "unknownId";
+    }
+}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckImpl.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckImpl.java
index b90e061..d5993b4 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckImpl.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckImpl.java
@@ -11,9 +11,6 @@
  * Application impl of Deck.
  */
 public class DeckImpl implements Deck {
-    // For demos, debugging.
-    public static final Deck DUMMY = new DeckImpl(
-            Unknown.TITLE, Unknown.THUMB, Unknown.ID);
 
     private final String mTitle;
     private final Bitmap mThumb;
@@ -25,18 +22,10 @@
         mDeckId = deckId;
     }
 
-    public DeckImpl(String title, Bitmap thumb) {
-        this(title, thumb, Unknown.ID);
-    }
-
-    public DeckImpl(String title) {
-        this(title, Unknown.THUMB);
-    }
-
     public String toString() {
-      return "[title=\""+ (mTitle == null ? "unknown" : mTitle) +
-              "\", id=" + (mDeckId == null ? "unknown" : mDeckId) +
-              ", thumb=" + (mThumb == null ? "no" : "yes") + "]";
+        return "[title=\"" + (mTitle == null ? "unknown" : mTitle) +
+                "\", id=" + (mDeckId == null ? "unknown" : mDeckId) +
+                ", thumb=" + (mThumb == null ? "no" : "yes") + "]";
     }
 
     @Override
@@ -53,30 +42,6 @@
         return mDeckId.hashCode();
     }
 
-    public static Deck fromBundle(Bundle b) {
-        if (b == null) {
-            throw new IllegalArgumentException("Need a bundle.");
-        }
-        return new DeckImpl(
-                b.getString(B.DECK_TITLE),
-                (Bitmap) b.getParcelable(B.DECK_THUMB),
-                b.getString(B.DECK_ID));
-    }
-
-    @Override
-    public Bundle toBundle(Bundle b) {
-        if (b == null) {
-            b = new Bundle();
-        }
-        b.putString(B.DECK_TITLE, mTitle);
-        // TODO(jregan): Our thumbnails are too big!  We need to store
-        // them on disk, pass a file handle in the intent instead,
-        // and load them on the other side.
-        // ### b.putParcelable(B.DECK_THUMB, mThumb);
-        b.putString(B.DECK_ID, mDeckId);
-        return b;
-    }
-
     @Override
     public Bitmap getThumb() {
         return mThumb;
@@ -92,9 +57,4 @@
         return mDeckId;
     }
 
-    private static class Unknown {
-        static final String TITLE = "unknownTitle";
-        static final String ID = "unknownId";
-        static final Bitmap THUMB = null;
-    }
 }
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 7266789..fe83e5c 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,8 +4,6 @@
 
 package io.v.android.apps.syncslides.model;
 
-import android.os.Bundle;
-
 /**
  * Someone taking part in a presentation.
  *
@@ -13,33 +11,6 @@
  * syncslides.
  */
 public interface Participant {
-    /**
-     * If true, enable MT-based (mounttable) discovery. Deck view will scan a MT
-     * to find live presentations. Clicking play on a presentation will start a
-     * service and try to mount it in a MT so other deck views can list it in
-     * the UX. MT location determined in
-     * {@link io.v.android.apps.syncslides.discovery.V23Manager}.
-     */
-    boolean ENABLE_MT_DISCOVERY = true;
-
-    public static class Mt {
-        /**
-         * Every v23 service will be mounted in the namespace with a name
-         * prefixed by this.
-         */
-        public static String ROOT_NAME = "liveDeck";
-
-        /**
-         * TODO(jregan): Assure legal mount name (remove blanks and such).
-         */
-        public static String makeMountName(Deck deck) {
-            return ROOT_NAME + "/" + deck.getId();
-        }
-
-        public static String makeScanString() {
-            return ROOT_NAME + "/*";
-        }
-    }
 
     // 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
@@ -53,7 +24,8 @@
     String getServiceName();
 
     // Initially get or refresh data from the endPoint.
-    void refreshData();
+    // Return true if call succeeds, false otherwise.
+    boolean refreshData();
 
     // For debugging.
     String toString();
@@ -70,9 +42,6 @@
     class B {
         public static final String PARTICIPANT_ROLE = "participant_role";
         public static final String PARTICIPANT_SHOULD_ADV = "participant_is_advertising";
-        public static final String PARTICIPANT_SERVICE_NAME = "participant_endPoint";
-        public static final String PARTICIPANT_NAME = "participant_name";
-        public static final String PARTICIPANT_BLESSINGS = "participant_blessings";
         public static final String PARTICIPANT_SYNCED = "participant_synced";
     }
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/Role.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Role.java
similarity index 92%
rename from projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/Role.java
rename to projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Role.java
index 7351e46..790d50c 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/Role.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/Role.java
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package io.v.android.apps.syncslides;
+package io.v.android.apps.syncslides.model;
 
 /**
  * Represents the user's type of participation in the presentation.
diff --git a/projects/syncslides/app/src/main/res/layout-land/fragment_navigate.xml b/projects/syncslides/app/src/main/res/layout-land/fragment_navigate.xml
index e751d5f..41119a1 100644
--- a/projects/syncslides/app/src/main/res/layout-land/fragment_navigate.xml
+++ b/projects/syncslides/app/src/main/res/layout-land/fragment_navigate.xml
@@ -18,10 +18,10 @@
             android:layout_width="match_parent"
             android:layout_height="0dp"
             android:layout_weight="1"
+            android:background="@color/blue_grey_50"
             android:gravity="left"
             android:hint="@string/notes_hint"
-            android:textSize="18sp"
-            android:background="@color/blue_grey_50"/>
+            android:textSize="18sp" />
 
         <RelativeLayout
             android:layout_width="wrap_content"
@@ -33,6 +33,7 @@
                 android:layout_height="wrap_content"
                 android:layout_centerInParent="true"
                 android:adjustViewBounds="true"
+                android:background="@color/blue_grey_50"
                 android:scaleType="fitCenter" />
 
             <TextView
@@ -60,6 +61,7 @@
                 android:layout_height="wrap_content"
                 android:layout_centerInParent="true"
                 android:adjustViewBounds="true"
+                android:background="@color/blue_grey_50"
                 android:scaleType="fitCenter" />
 
             <TextView
diff --git a/projects/syncslides/app/src/main/res/layout/activity_sign_in.xml b/projects/syncslides/app/src/main/res/layout/activity_sign_in.xml
new file mode 100644
index 0000000..7c93107
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/layout/activity_sign_in.xml
@@ -0,0 +1,9 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
+    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    tools:context="io.v.android.apps.syncslides.SignInActivity">
+
+</RelativeLayout>
diff --git a/projects/syncslides/app/src/main/res/layout/fragment_navigate.xml b/projects/syncslides/app/src/main/res/layout/fragment_navigate.xml
index b821dfc..a4d2556 100644
--- a/projects/syncslides/app/src/main/res/layout/fragment_navigate.xml
+++ b/projects/syncslides/app/src/main/res/layout/fragment_navigate.xml
@@ -27,7 +27,7 @@
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:adjustViewBounds="true"
-                    android:scaleType="fitCenter"/>
+                    android:scaleType="fitCenter" />
 
                 <TextView
                     android:id="@+id/slide_num_text"
@@ -39,7 +39,7 @@
                     android:background="@drawable/nav_hint"
                     android:paddingLeft="@dimen/nav_hint_padding"
                     android:paddingRight="@dimen/nav_hint_padding"
-                    android:textColor="@color/nav_hint_text"/>
+                    android:textColor="@color/nav_hint_text" />
 
             </RelativeLayout>
 
@@ -86,10 +86,10 @@
                         android:layout_height="wrap_content"
                         android:layout_alignRight="@id/questions"
                         android:layout_alignTop="@id/questions"
-                        android:gravity="center"
-                        android:textSize="@dimen/nav_question_num_size"
                         android:background="@drawable/orange_circle"
-                        android:textColor="@color/nav_question_num_text"/>
+                        android:gravity="center"
+                        android:textColor="@color/nav_question_num_text"
+                        android:textSize="@dimen/nav_question_num_size" />
                 </RelativeLayout>
 
 
@@ -140,12 +140,12 @@
         android:layout_width="match_parent"
         android:layout_height="0dp"
         android:layout_weight="1"
+        android:background="@color/blue_grey_50"
         android:gravity="left"
         android:hint="@string/notes_hint"
-        android:textSize="18sp"
-        android:scrollbars="vertical"
         android:inputType="textMultiLine|textAutoComplete|textCapSentences"
-        android:background="@color/blue_grey_50"/>
+        android:scrollbars="vertical"
+        android:textSize="18sp" />
 
     <!-- Display the next and previous slide thumbnails. -->
     <LinearLayout
@@ -166,6 +166,7 @@
                 android:layout_height="wrap_content"
                 android:layout_centerInParent="true"
                 android:adjustViewBounds="true"
+                android:background="@color/blue_grey_50"
                 android:scaleType="fitCenter" />
 
             <TextView
@@ -193,6 +194,7 @@
                 android:layout_height="wrap_content"
                 android:layout_centerInParent="true"
                 android:adjustViewBounds="true"
+                android:background="@color/blue_grey_50"
                 android:scaleType="fitCenter" />
 
             <TextView
diff --git a/projects/syncslides/app/src/main/res/menu/menu_sign_in.xml b/projects/syncslides/app/src/main/res/menu/menu_sign_in.xml
new file mode 100644
index 0000000..813fc46
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/menu/menu_sign_in.xml
@@ -0,0 +1,7 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:context="io.v.android.apps.syncslides.SignInActivity">
+    <item android:id="@+id/action_settings" android:title="@string/action_settings"
+        android:orderInCategory="100" app:showAsAction="never" />
+</menu>
diff --git a/projects/syncslides/app/src/main/res/values/strings.xml b/projects/syncslides/app/src/main/res/values/strings.xml
index 7ffb985..48013cd 100644
--- a/projects/syncslides/app/src/main/res/values/strings.xml
+++ b/projects/syncslides/app/src/main/res/values/strings.xml
@@ -22,5 +22,4 @@
     <string name="handoff_message">You handed off to</string>
     <string name="end_handoff">RESUME</string>
     <string name="presentation_live">LIVE NOW</string>
-
 </resources>
diff --git a/settings.gradle b/settings.gradle
index 5b161dd..f626ccc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include 'lib', 'tests', 'android-lib', 'benchmarks:syncbench', 'projects:syncslides', 'projects:syncslides:app', 'projects:discovery_sample', 'projects:discovery_sample:app'
+include 'lib', 'tests', 'android-lib', 'benchmarks:syncbench', 'projects:syncslides', 'projects:syncslides:app', 'projects:syncslidepresenter', 'projects:discovery_sample', 'projects:discovery_sample:app'