Merge "Java: remove a 1-day caveat from Android blessings"
diff --git a/android-lib/build.gradle b/android-lib/build.gradle
index ef28354..d4397b1 100644
--- a/android-lib/build.gradle
+++ b/android-lib/build.gradle
@@ -67,11 +67,11 @@
     return System.properties['os.name'].toLowerCase()
 }
 
-def v23Root = VanadiumEnvironment.getVanadiumEnvironment().v23Root.getAbsolutePath()
+def jiriRoot = VanadiumEnvironment.getVanadiumEnvironment().jiriRoot.getAbsolutePath()
 
 class VanadiumEnvironment {
-    File v23Root;
-    File v23Bin;
+    File jiriRoot;
+    File jiriBin;
     File thirdPartyGoBinDir;
 
     public static getVanadiumEnvironment() {
@@ -83,32 +83,32 @@
                     + "https://github.com/vanadium/docs/blob/master/installation.md")
         }
 
-        result.v23Root = new File(System.getenv()['JIRI_ROOT'])
-        result.v23Bin = new File(result.v23Root, ['devtools', 'bin', 'v23'].join(File.separator))
-        if (!result.v23Bin.exists() || !result.v23Bin.isFile() || !result.v23Bin.canExecute()) {
+        result.jiriRoot = new File(System.getenv()['JIRI_ROOT'])
+        result.jiriBin = new File(result.jiriRoot, ['devtools', 'bin', 'jiri'].join(File.separator))
+        if (!result.jiriBin.exists() || !result.jiriBin.isFile() || !result.jiriBin.canExecute()) {
             throw new InvalidUserDataException(
-                    result.v23Bin.toString() + " does not exist or is not an executable file. "
+                    result.jiriBin.toString() + " does not exist or is not an executable file. "
                     + "Please follow the Vanadium installation instructions at "
                     + "https://github.com/vanadium/docs/blob/master/installation.md")
         }
 
-        result.thirdPartyGoBinDir = new File(result.v23Root,
+        result.thirdPartyGoBinDir = new File(result.jiriRoot,
                 ['third_party', 'android', 'go', 'bin'].join(File.separator))
         def thirdPartyGoBin = new File(result.thirdPartyGoBinDir, 'go')
         if (!thirdPartyGoBin.exists() || !thirdPartyGoBin.isFile() || !thirdPartyGoBin.canExecute()) {
             throw new InvalidUserDataException(
                     thirdPartyGoBin.toString() + " does not exist or is not an executable file. "
                     + "You probably didn't install the android profile. Try running\n\n"
-                    + "v23 profile install android\n\nand then try building again.")
+                    + "jiri profile install android\n\nand then try building again.")
         }
 
-        def syncbaseOutputDir = new File(result.v23Root,
+        def syncbaseOutputDir = new File(result.jiriRoot,
                 ['third_party', 'cout', 'android_arm', 'leveldb'].join(File.separator))
         if (!syncbaseOutputDir.exists() || !syncbaseOutputDir.isDirectory()) {
             throw new InvalidUserDataException(
                     syncbaseOutputDir.toString() + " does not exist or is not a directory. "
                     + "You probably didn't install the android syncbase profile. Try running\n\n"
-                    + "GOOS=android GOARCH=arm v23 profile install syncbase"
+                    + "GOOS=android GOARCH=arm jiri profile install syncbase"
                     + "\n\nand then try building again"
             )
         }
@@ -139,13 +139,13 @@
     environment 'JIRI_PROFILE': 'android'
     environment 'PATH': [env.thirdPartyGoBinDir.getAbsolutePath(), existingPath].join(File.pathSeparator)
 
-    commandLine env.v23Bin.getAbsolutePath(), 'go', 'install',
+    commandLine env.jiriBin.getAbsolutePath(), 'go', 'install',
             '-buildmode=c-shared', '-v', '-tags', 'android', 'v.io/x/jni/main'
 }
 
 // Copy the shared library to its ultimate destination.
 task copyVanadiumLib(type: Copy, dependsOn: goBuildVanadiumLib) {
-    from v23Root + '/release/go/pkg/android_arm/v.io/x/jni'
+    from jiriRoot + '/release/go/pkg/android_arm/v.io/x/jni'
     into 'src/main/jniLibs/armeabi-v7a'
     include 'main.a'
     rename 'main.a', 'libv23.so'
diff --git a/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/Bluetooth.java b/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/Bluetooth.java
index 299da8f..83c9da9 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/Bluetooth.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/Bluetooth.java
@@ -15,7 +15,6 @@
 import org.joda.time.Duration;
 
 import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
diff --git a/gradle-plugin/build.gradle b/gradle-plugin/build.gradle
index 0ab89cc..419a774 100644
--- a/gradle-plugin/build.gradle
+++ b/gradle-plugin/build.gradle
@@ -36,7 +36,7 @@
 task buildVdl(type: Exec) {
     description 'Build the VDL tool'
     group 'Build'
-    commandLine 'v23', 'go', 'build', '-o', 'build/vdltool/vdl-' + getOsName(), 'v.io/x/ref/cmd/vdl'
+    commandLine 'jiri', 'go', 'build', '-o', 'build/vdltool/vdl-' + getOsName(), 'v.io/x/ref/cmd/vdl'
 }
 
 task natives(type: Jar, dependsOn: buildVdl) {
diff --git a/lib/build.gradle b/lib/build.gradle
index c11d684..640b2ff 100644
--- a/lib/build.gradle
+++ b/lib/build.gradle
@@ -40,16 +40,16 @@
     return System.properties['os.name'].toLowerCase()
 }
 
-def v23Root = VanadiumEnvironment.getVanadiumEnvironment().v23Root.getAbsolutePath()
-def v23Bin = VanadiumEnvironment.getVanadiumEnvironment().v23Bin.getAbsolutePath()
-def vdlPath = [v23Root, 'release', 'go', 'src'].join(File.separator)
-def vdlBin = [v23Root, 'release', 'go', 'bin', 'vdl'].join(File.separator)
+def jiriRoot = VanadiumEnvironment.getVanadiumEnvironment().jiriRoot.getAbsolutePath()
+def jiriBin = VanadiumEnvironment.getVanadiumEnvironment().jiriBin.getAbsolutePath()
+def vdlPath = [jiriRoot, 'release', 'go', 'src'].join(File.separator)
+def vdlBin = [jiriRoot, 'release', 'go', 'bin', 'vdl'].join(File.separator)
 
 sourceSets.main.java.srcDirs += 'generated-src/vdl'
 
 class VanadiumEnvironment {
-    File v23Root;
-    File v23Bin;
+    File jiriRoot;
+    File jiriBin;
     File thirdPartyGoBinDir;
 
     public static getVanadiumEnvironment() {
@@ -61,30 +61,30 @@
                     + "https://github.com/vanadium/docs/blob/master/installation.md")
         }
 
-        result.v23Root = new File(System.getenv()['JIRI_ROOT'])
-        result.v23Bin = new File(result.v23Root, ['devtools', 'bin', 'v23'].join(File.separator))
-        if (!result.v23Bin.exists() || !result.v23Bin.isFile() || !result.v23Bin.canExecute()) {
+        result.jiriRoot = new File(System.getenv()['JIRI_ROOT'])
+        result.jiriBin = new File(result.jiriRoot, ['devtools', 'bin', 'jiri'].join(File.separator))
+        if (!result.jiriBin.exists() || !result.jiriBin.isFile() || !result.jiriBin.canExecute()) {
             throw new InvalidUserDataException(
-                    result.v23Bin.toString() + " does not exist or is not an executable file. "
+                    result.jiriBin.toString() + " does not exist or is not an executable file. "
                     + "Please follow the Vanadium installation instructions at "
                     + "https://github.com/vanadium/docs/blob/master/installation.md")
         }
 
-        result.thirdPartyGoBinDir = new File(result.v23Root,
+        result.thirdPartyGoBinDir = new File(result.jiriRoot,
                 ['third_party', 'java', 'go', 'bin'].join(File.separator))
         def thirdPartyGoBin = new File(result.thirdPartyGoBinDir, 'go')
         if (!thirdPartyGoBin.exists() || !thirdPartyGoBin.isFile() || !thirdPartyGoBin.canExecute()) {
             throw new InvalidUserDataException(
                     thirdPartyGoBin.toString() + " does not exist or is not an executable file. "
                     + "You probably didn't install the java profile. Try running\n\n"
-                    + "v23 profile install java\n\nand then try building again.")
+                    + "jiri profile install java\n\nand then try building again.")
         }
         return result
     }
 }
 
 task buildVdlTool(type: Exec) {
-    commandLine v23Bin, 'go', 'install', 'v.io/x/ref/cmd/vdl'
+    commandLine jiriBin, 'go', 'install', 'v.io/x/ref/cmd/vdl'
 }
 
 task generateVdl(type: Exec, dependsOn: buildVdlTool) {
@@ -101,7 +101,7 @@
         throw new InvalidUserDataException("The JAVA_HOME environment variable is not set. "
                 + "Please set it to the root of a JDK installation directory.  If JDK isn't "
                 + "installed, you probably didn't install the java profile: try running\n\n"
-                + "v23 profile install java\n\nand then try building again.")
+                + "jiri profile install java\n\nand then try building again.")
     }
     if (!isAmd64()) {
         throw new InvalidUserDataException("Java Vanadium builds only enabled on amd64 "
@@ -123,21 +123,21 @@
     environment 'JIRI_PROFILE': 'java'
     environment 'PATH': [env.thirdPartyGoBinDir.getAbsolutePath(), existingPath].join(File.pathSeparator)
 
-    commandLine env.v23Bin.getAbsolutePath(), 'go', 'install',
+    commandLine env.jiriBin.getAbsolutePath(), 'go', 'install',
             '-buildmode=c-shared', '-v', '-tags', 'java', 'v.io/x/jni/main'
 }
 
 // Copy the shared library to its ultimate destination.
 if (isLinux()) {
     task copyVanadiumLib(type: Copy, dependsOn: goBuildVanadiumLib) {
-        from v23Root + '/release/go/pkg/linux_amd64_shared/v.io/x/jni'
+        from jiriRoot + '/release/go/pkg/linux_amd64_shared/v.io/x/jni'
         into 'build/libs'
         include 'main.a'
         rename 'main.a', 'libv23.so'
     }
 } else {  // darwin
     task copyVanadiumLib(type: Copy, dependsOn: goBuildVanadiumLib) {
-        from v23Root + '/release/go/pkg/darwin_amd64/v.io/x/jni'
+        from jiriRoot + '/release/go/pkg/darwin_amd64/v.io/x/jni'
         into 'build/libs'
         include 'main.a'
         rename 'main.a', 'libv23.dylib'
diff --git a/lib/src/main/java/io/v/v23/security/Blessings.java b/lib/src/main/java/io/v/v23/security/Blessings.java
index 302b003..e0f6c91 100644
--- a/lib/src/main/java/io/v/v23/security/Blessings.java
+++ b/lib/src/main/java/io/v/v23/security/Blessings.java
@@ -6,6 +6,9 @@
 
 import com.google.common.base.Joiner;
 
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.io.Serializable;
 import java.security.interfaces.ECPublicKey;
 import java.util.ArrayList;
@@ -27,35 +30,36 @@
  * <p>
  * See also: <a href="https://github.com/vanadium/docs/blob/master/glossary.md#blessing">https://github.com/vanadium/docs/blob/master/glossary.md#blessing</a>.
  */
-public class Blessings implements Serializable {
+public final class Blessings implements Serializable {
     private static final long serialVersionUID = 1L;
 
-    private static native Blessings nativeCreate(WireBlessings wire) throws VException;
+    private static native long nativeCreate(WireBlessings wire) throws VException;
     private static native Blessings nativeCreateUnion(Blessings[] blessings) throws VException;
 
     public static Blessings create(WireBlessings wire) {
         try {
-            return nativeCreate(wire);
+            return new Blessings(nativeCreate(wire), wire);
         } catch (VException e) {
-            throw new RuntimeException(e);
+            throw new RuntimeException("Couldn't create blessings from WireBlessings", e);
         }
     }
 
-    public static Blessings create(List<List<VCertificate>> certChains) {
-        return create(new WireBlessings(certChains));
-    }
-
     static Blessings createUnion(Blessings... blessings) throws VException {
         return nativeCreateUnion(blessings);
     }
 
-    private final long nativePtr;
-    private final WireBlessings wire;  // non-null
+    private long nativePtr;
+    private volatile WireBlessings wire;  // can be null
 
     private native ECPublicKey nativePublicKey(long nativePtr) throws VException;
     private native Blessings nativeSigningBlessings(long nativePtr) throws VException;
+    private native WireBlessings nativeWireFormat(long nativePtr) throws VException;
+
     private native void nativeFinalize(long nativePtr);
-    // TODO(sjayanti): a method to get all the non-signing blessings in a given blessings object.
+
+    private Blessings(long nativePtr) {
+        this.nativePtr = nativePtr;
+    }
 
     private Blessings(long nativePtr, WireBlessings wire) {
         this.nativePtr = nativePtr;
@@ -68,7 +72,7 @@
      */
     public ECPublicKey publicKey() {
         try {
-            return nativePublicKey(this.nativePtr);
+            return nativePublicKey(nativePtr);
         } catch (VException e) {
             throw new RuntimeException("Couldn't get public key", e);
         }
@@ -80,8 +84,8 @@
      * The return value may be {@code null} if the blessings are empty.
      */
     public Blessings signingBlessings() {
-      try {
-          return nativeSigningBlessings(this.nativePtr);
+        try {
+            return nativeSigningBlessings(nativePtr);
       } catch (VException e) {
           throw new RuntimeException("Couldn't get signing blessings", e);
       }
@@ -91,14 +95,55 @@
      * Returns the blessings in the wire format.
      */
     public WireBlessings wireFormat() {
-        return this.wire;
+        // Check the cache first as nativeWireFormat() is an expensive operation.
+        synchronized (this) {
+            if (wire != null) {
+                return wire;
+            }
+        }
+        WireBlessings ret = null;
+        try {
+            ret = nativeWireFormat(nativePtr);
+        } catch (VException e) {
+            throw new RuntimeException("Couldn't get wire blessings representation.", e);
+        }
+        synchronized (this) {
+            if (wire == null) {
+                wire = ret;
+            }
+        }
+        return ret;
     }
 
     /**
      * Returns {@code true} iff the blessings are empty.
      */
     public boolean isEmpty() {
-        return this.wire.getCertificateChains().isEmpty();
+        return wireFormat().getCertificateChains().isEmpty();
+    }
+
+    /**
+     * Returns the certificate chains stored inside the blessings.
+     */
+    public List<List<VCertificate>> getCertificateChains() {
+        return wireFormat().getCertificateChains();
+    }
+
+    long nativePtr() {
+        return nativePtr;
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        out.writeObject(wireFormat());
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        wire = (WireBlessings) in.readObject();
+        try {
+            nativePtr = nativeCreate(wire);
+        } catch (VException e) {
+            throw new IOException("Couldn't create native blessings.", e);
+        }
     }
 
     @Override
@@ -107,17 +152,20 @@
         if (obj == null) return false;
         if (!(obj instanceof Blessings)) return false;
         Blessings other = (Blessings) obj;
-        return this.wireFormat().equals(other.wireFormat());
+        return wireFormat().equals(other.wireFormat());
     }
+
     @Override
     public int hashCode() {
-        return this.wire.hashCode();
+        return wireFormat().hashCode();
     }
+
     @Override
     public String toString() {
-        List<String> chains = new ArrayList<>(getCertificateChains().size());
+        List<String> chains = new ArrayList<String>(getCertificateChains().size());
         for (List<VCertificate> certificateChain : getCertificateChains()) {
-            List<String> certificateNames = new ArrayList<>(certificateChain.size());
+            List<String> certificateNames =
+                    new ArrayList<String>(certificateChain.size());
             for (VCertificate certificate : certificateChain) {
                 certificateNames.add(certificate.getExtension());
             }
@@ -127,10 +175,6 @@
     }
     @Override
     protected void finalize() {
-        nativeFinalize(this.nativePtr);
-    }
-
-    public List<List<VCertificate>> getCertificateChains() {
-        return wire.getCertificateChains();
+        nativeFinalize(nativePtr);
     }
 }
diff --git a/lib/src/test/java/io/v/v23/security/BlessingsTest.java b/lib/src/test/java/io/v/v23/security/BlessingsTest.java
index cc6e884..ca14cc7 100644
--- a/lib/src/test/java/io/v/v23/security/BlessingsTest.java
+++ b/lib/src/test/java/io/v/v23/security/BlessingsTest.java
@@ -12,19 +12,21 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import io.v.v23.V;
-import io.v.v23.verror.VException;
 import io.v.v23.vom.VomUtil;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.util.Arrays;
 import java.security.interfaces.ECPublicKey;
-import java.util.ArrayList;
 import java.util.List;
 
 /**
  * Tests the default Blessings implementation.
  */
 public class BlessingsTest extends TestCase {
-    public void testPublicKey() throws VException {
+    public void testPublicKey() throws Exception {
         V.init();
         VPrincipal p1 = VSecurity.newPrincipal();
         VPrincipal p2 = VSecurity.newPrincipal();
@@ -41,7 +43,7 @@
         }
     }
 
-    public void testVomEncodeDecode() throws VException {
+    public void testVomEncodeDecode() throws Exception {
         V.init();
         VPrincipal p = VSecurity.newPrincipal();
         Blessings alice = p.blessSelf("alice");
@@ -52,7 +54,28 @@
         }
     }
 
-    public void testSigningBlessings() throws VException {
+    public void testSerialization() throws Exception {
+        V.init();
+        VPrincipal p = VSecurity.newPrincipal();
+        Blessings blessings = p.blessSelf("alice");
+
+        // Write
+        ByteArrayOutputStream data = new ByteArrayOutputStream();
+        ObjectOutputStream out = new ObjectOutputStream(data);
+        out.writeObject(blessings);
+        out.close();
+
+        // Read
+        final ObjectInputStream in =
+                new ObjectInputStream(new ByteArrayInputStream(data.toByteArray()));
+
+        // Verify
+        final Object copy = in.readObject();
+        assertThat(copy).isEqualTo(blessings);
+        assertThat(copy.hashCode()).isEqualTo(copy.hashCode());
+    }
+
+    public void testSigningBlessings() throws Exception {
         V.init();
         VPrincipal p = VSecurity.newPrincipal();
         ECPublicKey pk = p.publicKey();
@@ -71,5 +94,4 @@
 
         assertThat(union.signingBlessings().getCertificateChains().size()).isEqualTo(1);
     }
-
 }
diff --git a/lib/src/test/java/io/v/v23/security/CaveatTest.java b/lib/src/test/java/io/v/v23/security/CaveatTest.java
index 1263717..3e78c3e 100644
--- a/lib/src/test/java/io/v/v23/security/CaveatTest.java
+++ b/lib/src/test/java/io/v/v23/security/CaveatTest.java
@@ -25,7 +25,7 @@
     public void testMethodCaveat() throws VException {
         VContext context = V.init();
         VPrincipal p1 = VSecurity.newPrincipal();
-        Blessings alice = p1.blessSelf("alice", VSecurity.newMethodCaveat("succeed"));
+        Blessings alice = p1.blessSelf("alice", VSecurity.newMethodCaveat("Succeed"));
         p1.addToRoots(alice);
         {
             Call call = VSecurity.newCall(
@@ -35,7 +35,7 @@
         }
         {
             Call call = VSecurity.newCall(
-                    new CallParams().withLocalPrincipal(p1).withMethod("fail"));
+                    new CallParams().withLocalPrincipal(p1).withMethod("Fail"));
             String[] result = VSecurity.getRemoteBlessingNames(context, call);
             assertThat(result != null).isTrue();
             assertThat(Arrays.asList(result)).containsExactly();
diff --git a/lib/src/test/java/io/v/v23/vdl/SerializableTest.java b/lib/src/test/java/io/v/v23/vdl/SerializableTest.java
index ec27f15..c6d27ac 100644
--- a/lib/src/test/java/io/v/v23/vdl/SerializableTest.java
+++ b/lib/src/test/java/io/v/v23/vdl/SerializableTest.java
@@ -19,21 +19,21 @@
 public class SerializableTest extends junit.framework.TestCase {
     public void testSerializable() throws IOException, ClassNotFoundException {
         for (TestCase test : Constants.TESTS) {
-            final Object value = test.getValue().getElem();
+            Object value = test.getValue().getElem();
             if (!(value instanceof VdlValue)) continue;
 
             // Write
-            final ByteArrayOutputStream data = new ByteArrayOutputStream();
-            final ObjectOutputStream out = new ObjectOutputStream(data);
+            ByteArrayOutputStream data = new ByteArrayOutputStream();
+            ObjectOutputStream out = new ObjectOutputStream(data);
             out.writeObject(value);
             out.close();
 
             // Read
-            final ObjectInputStream in =
+            ObjectInputStream in =
                     new ObjectInputStream(new ByteArrayInputStream(data.toByteArray()));
 
             // Verify
-            final Object copy = in.readObject();
+            Object copy = in.readObject();
             assertEquals(value, copy);
             assertEquals(value.hashCode(), copy.hashCode());
         }
diff --git a/projects/syncslides/app/build.gradle b/projects/syncslides/app/build.gradle
index df1637f..c00daee 100644
--- a/projects/syncslides/app/build.gradle
+++ b/projects/syncslides/app/build.gradle
@@ -6,7 +6,7 @@
 
     defaultConfig {
         applicationId "io.v.android.apps.syncslides"
-        minSdkVersion 19
+        minSdkVersion 21
         targetSdkVersion 23
         versionCode 1
         versionName "1.0"
@@ -23,4 +23,6 @@
     compile fileTree(dir: 'libs', include: ['*.jar'])
     compile 'com.android.support:appcompat-v7:23.0.1'
     compile 'com.android.support:design:23.0.1'
+    compile 'com.android.support:recyclerview-v7:23.0.1'
+    compile 'com.android.support:cardview-v7:23.0.1'
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationChooserActivity.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationChooserActivity.java
index 89cda47..73e857b 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationChooserActivity.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationChooserActivity.java
@@ -4,17 +4,11 @@
 
 package io.v.android.apps.syncslides;
 
-import android.app.Activity;
-import android.support.design.widget.FloatingActionButton;
 import android.support.v7.app.AppCompatActivity;
-import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.os.Bundle;
 import android.util.Log;
-import android.view.LayoutInflater;
 import android.view.Menu;
-import android.view.View;
-import android.view.ViewGroup;
 import android.support.v4.widget.DrawerLayout;
 
 public class PresentationChooserActivity extends AppCompatActivity
@@ -74,62 +68,4 @@
         return super.onCreateOptionsMenu(menu);
     }
 
-    /**
-     * This fragment contains the list of presentations as well as the FAB to create a new
-     * presentation.
-     */
-    public static class PresentationChooserFragment extends Fragment {
-        /**
-         * The fragment argument representing the section number for this
-         * fragment.
-         */
-        private static final String ARG_SECTION_NUMBER = "section_number";
-
-        /**
-         * Returns a new instance of this fragment for the given section
-         * number.
-         */
-        public static PresentationChooserFragment newInstance(int sectionNumber) {
-            PresentationChooserFragment fragment = new PresentationChooserFragment();
-            Bundle args = new Bundle();
-            args.putInt(ARG_SECTION_NUMBER, sectionNumber);
-            fragment.setArguments(args);
-            return fragment;
-        }
-
-        public PresentationChooserFragment() {
-        }
-
-        @Override
-        public View onCreateView(LayoutInflater inflater, ViewGroup container,
-                                 Bundle savedInstanceState) {
-            View rootView = inflater.inflate(R.layout.fragment_presentation_chooser, container,
-                    false);
-            FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(
-                    R.id.new_presentation_fab);
-            fab.setOnClickListener(new View.OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    newPresentation();
-                }
-            });
-            return rootView;
-        }
-
-        @Override
-        public void onAttach(Activity activity) {
-            super.onAttach(activity);
-            ((PresentationChooserActivity) activity).onSectionAttached(
-                    getArguments().getInt(ARG_SECTION_NUMBER));
-        }
-
-        /**
-         * Import a presentation so it shows up in the list of all presentations.
-         */
-        private void newPresentation() {
-            // TODO(afergan): Hook up new presentation screen here.
-            Log.i(TAG, "newPresentation");
-        }
-    }
-
 }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationChooserFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationChooserFragment.java
new file mode 100644
index 0000000..1a4ab43
--- /dev/null
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationChooserFragment.java
@@ -0,0 +1,86 @@
+// 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.app.Activity;
+import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.app.Fragment;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * This fragment contains the list of presentations as well as the FAB to create a new
+ * presentation.
+ */
+public class PresentationChooserFragment extends Fragment {
+    /**
+     * The fragment argument representing the section number for this
+     * fragment.
+     */
+    private static final String ARG_SECTION_NUMBER = "section_number";
+    private static final String TAG = "ChooserFragment";
+    private RecyclerView mRecyclerView;
+    private GridLayoutManager mLayoutManager;
+    private PresentationListAdapter mAdapter;
+
+    /**
+     * Returns a new instance of this fragment for the given section
+     * number.
+     */
+    public static PresentationChooserFragment newInstance(int sectionNumber) {
+        PresentationChooserFragment fragment = new PresentationChooserFragment();
+        Bundle args = new Bundle();
+        args.putInt(ARG_SECTION_NUMBER, sectionNumber);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        View rootView = inflater.inflate(R.layout.fragment_presentation_chooser, container,
+                false);
+        FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(
+                R.id.new_presentation_fab);
+        fab.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                newPresentation();
+            }
+        });
+
+        mRecyclerView = (RecyclerView) rootView.findViewById(R.id.presentation_grid);
+        mRecyclerView.setHasFixedSize(true);
+
+        // TODO(kash): Dynamically set the span based on the screen width.
+        mLayoutManager = new GridLayoutManager(getContext(), 2);
+        mRecyclerView.setLayoutManager(mLayoutManager);
+
+        mAdapter = new PresentationListAdapter();
+        mRecyclerView.setAdapter(mAdapter);
+        return rootView;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        ((PresentationChooserActivity) activity).onSectionAttached(
+                getArguments().getInt(ARG_SECTION_NUMBER));
+    }
+
+    /**
+     * Import a presentation so it shows up in the list of all presentations.
+     */
+    private void newPresentation() {
+        // TODO(afergan): Hook up new presentation screen here.
+        Log.i(TAG, "newPresentation");
+    }
+
+}
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationListAdapter.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationListAdapter.java
new file mode 100644
index 0000000..b14c3e5
--- /dev/null
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/PresentationListAdapter.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;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.Toolbar;
+
+/**
+ * Provides a list of presentations to be shown in the RecyclerView of the
+ * PresentationChooserFragment.
+ */
+public class PresentationListAdapter
+        extends RecyclerView.Adapter<PresentationListAdapter.ViewHolder> {
+    // TODO(kash): Replace this static data with syncbase.
+    private static final int[] THUMBS = {
+            R.drawable.thumb_presentation1,
+            R.drawable.thumb_presentation2,
+            R.drawable.thumb_presentation3
+    };
+    private static final String[] TITLES = {"Presentation 1", "Presentation 2", "Presentation 3"};
+
+
+    @Override
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int i) {
+        View v = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.presentation_card, parent, false);
+        // TODO(kash): Add a menu that allows the user to delete a presentation.
+        return new ViewHolder(v);
+    }
+
+    @Override
+    public void onBindViewHolder(ViewHolder holder, int i) {
+        holder.mToolbar.setTitle(TITLES[i]);
+        // TODO(kash): We need to say when the user last viewed the presentation or show
+        // that the presentation is active.  Either use the subtitle for this or create
+        // a custom view for both the title and subtitle.
+        holder.mThumb.setImageResource(THUMBS[i]);
+    }
+
+    @Override
+    public int getItemCount() {
+        return TITLES.length;
+    }
+
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+        public final ImageView mThumb;
+        public final Toolbar mToolbar;
+
+        public ViewHolder(View itemView) {
+            super(itemView);
+            mThumb = (ImageView) itemView.findViewById(R.id.presentation_thumb);
+            mToolbar = (Toolbar) itemView.findViewById(R.id.presentation_card_toolbar);
+        }
+    }
+}
diff --git a/projects/syncslides/app/src/main/res/drawable/thumb_presentation1.png b/projects/syncslides/app/src/main/res/drawable/thumb_presentation1.png
new file mode 100644
index 0000000..851a751
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/drawable/thumb_presentation1.png
Binary files differ
diff --git a/projects/syncslides/app/src/main/res/drawable/thumb_presentation2.png b/projects/syncslides/app/src/main/res/drawable/thumb_presentation2.png
new file mode 100644
index 0000000..4fff1a8
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/drawable/thumb_presentation2.png
Binary files differ
diff --git a/projects/syncslides/app/src/main/res/drawable/thumb_presentation3.png b/projects/syncslides/app/src/main/res/drawable/thumb_presentation3.png
new file mode 100644
index 0000000..8708a4e
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/drawable/thumb_presentation3.png
Binary files differ
diff --git a/projects/syncslides/app/src/main/res/layout/fragment_presentation_chooser.xml b/projects/syncslides/app/src/main/res/layout/fragment_presentation_chooser.xml
index f109682..8526cca 100644
--- a/projects/syncslides/app/src/main/res/layout/fragment_presentation_chooser.xml
+++ b/projects/syncslides/app/src/main/res/layout/fragment_presentation_chooser.xml
@@ -1,9 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- TODO(kash): Use colors from the theme once we have one. -->
 <android.support.design.widget.CoordinatorLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:background="#eee9e9">
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/presentation_grid"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scrollbars="vertical"/>
 
     <android.support.design.widget.FloatingActionButton
         android:id="@+id/new_presentation_fab"
diff --git a/projects/syncslides/app/src/main/res/layout/presentation_card.xml b/projects/syncslides/app/src/main/res/layout/presentation_card.xml
new file mode 100644
index 0000000..0618da7
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/layout/presentation_card.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.CardView
+    android:id="@+id/presentation_card"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/presentation_card_width"
+    android:layout_height="wrap_content"
+    android:layout_margin="@dimen/presentation_card_margin"
+    android:layout_gravity="center">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <!-- A thumbnail of the presentation title slide. -->
+        <ImageView
+            android:id="@+id/presentation_thumb"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:scaleType="centerCrop"/>
+
+        <!-- Display the title of the presentation and a menu to configure it. -->
+        <Toolbar
+            android:id="@+id/presentation_card_toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="48dp"></Toolbar>
+
+    </LinearLayout>
+
+</android.support.v7.widget.CardView>
diff --git a/projects/syncslides/app/src/main/res/values/dimens.xml b/projects/syncslides/app/src/main/res/values/dimens.xml
index 6d38062..222e45b 100644
--- a/projects/syncslides/app/src/main/res/values/dimens.xml
+++ b/projects/syncslides/app/src/main/res/values/dimens.xml
@@ -9,4 +9,7 @@
 
     <dimen name="fab_margin">16dp</dimen>
     <dimen name="fab_elevation">4dp</dimen>
+
+    <dimen name="presentation_card_width">200dp</dimen>
+    <dimen name="presentation_card_margin">5dp</dimen>
 </resources>