Merge "syncslides: UI fixes"
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/DeckListAdapter.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/DeckListAdapter.java
index 51fdd89..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
@@ -87,13 +87,13 @@
     }
 
     @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);
@@ -101,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);
@@ -141,24 +141,23 @@
                             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/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 8943ccd..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,12 +13,11 @@
 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.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.DeckFactory;
-import io.v.android.apps.syncslides.model.DeckImpl;
 import io.v.android.apps.syncslides.model.Participant;
 import io.v.android.apps.syncslides.model.Role;
 
@@ -62,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) {
@@ -79,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.
@@ -114,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);
     }
@@ -175,7 +182,7 @@
         if (shouldUseV23()) {
             V23Manager.Singleton.get().mount(
                     Config.MtDiscovery.makeMountName(mDeck),
-                    new ParticipantServerImpl(mDeck));
+                    new ParticipantPeer.Server(mDeck));
             Log.d(TAG, "MT advertising started.");
         } else {
             Log.d(TAG, "No means to start advertising.");
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/db/DB.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/DB.java
index b331bb6..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
@@ -126,6 +126,13 @@
     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
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 1df52f2..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
@@ -326,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 dcea924..32341af 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
@@ -281,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()) {
@@ -309,15 +310,15 @@
         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, DeckFactory df) {
@@ -434,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;
             }
@@ -472,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;
             }
@@ -547,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.
@@ -679,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;
             }
@@ -710,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;
             }
@@ -944,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/ParticipantPeer.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantPeer.java
index 2fedf4b..5454690 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,17 +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.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;
 
 /**
@@ -149,4 +154,33 @@
         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/ParticipantServerImpl.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantServerImpl.java
deleted file mode 100644
index 2e5a83c..0000000
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/discovery/ParticipantServerImpl.java
+++ /dev/null
@@ -1,44 +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.graphics.Bitmap;
-import android.util.Log;
-
-import java.io.ByteArrayOutputStream;
-
-import io.v.android.apps.syncslides.db.VDeck;
-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 VDeck get(VContext ctx, ServerCall call)
-            throws io.v.v23.verror.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/misc/V23Manager.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
index 5bc8a95..734d79e 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
@@ -7,6 +7,8 @@
 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;
@@ -172,8 +175,11 @@
                 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;
         }
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/DeckImpl.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/model/DeckImpl.java
index 07e0d31..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
@@ -42,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 for intent use.
-        // Could store 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;
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 90fbadf..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
@@ -42,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/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/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 b511423..d9d2af5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include 'lib', 'tests', 'android-lib', 'benchmarks:syncbench', 'projects:syncslides', 'projects:syncslides:app'
+include 'lib', 'tests', 'android-lib', 'benchmarks:syncbench', 'projects:syncslides', 'projects:syncslides:app', 'projects:syncslidepresenter'