syncslides: Implement DB.getDecks().

- Upgrade to vanadium-android:0.2.  Switch to the Futures APIs.
- XML files for displaying decks copied from the old version of
  syncslides.
- WatchedList and Watcher classes to make it easier to watch
  a set of data for changes.  DeckWatcher is the first of many
  uses of these abstractions.
- Copy schema.vdl from the old version of syncslides.

Change-Id: I1b94c6ff6b63ce4a5d289943b7c0d40124eb0308
diff --git a/android/app/.gitignore b/android/app/.gitignore
index 796b96d..be11940 100644
--- a/android/app/.gitignore
+++ b/android/app/.gitignore
@@ -1 +1,3 @@
 /build
+local.properties
+/generated-src/
\ No newline at end of file
diff --git a/android/app/build.gradle b/android/app/build.gradle
index fb2094f..fa1e8f7 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -4,12 +4,17 @@
     }
 
     dependencies {
+        classpath 'com.android.tools.build:gradle:1.3.0'
         classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.+'
+        // We are going to define a custom VDL service. The Vanadium
+        // Gradle plugin makes that easier, so let's use that.
+        classpath 'io.v:gradle-plugin:0.1'
     }
 }
 
 apply plugin: 'android-sdk-manager'
 apply plugin: 'com.android.application'
+apply plugin: 'io.v.vdl'
 
 android {
     compileSdkVersion 23
@@ -39,5 +44,10 @@
     testCompile 'junit:junit:4.12'
     compile 'com.android.support:appcompat-v7:23.1.0'
     compile 'com.android.support:design:23.1.0'
-    compile project(':android-lib')
+    compile 'io.v:vanadium-android:0.2'
+}
+
+vdl {
+    // This is where the VDL tool will look for VDL definitions.
+    inputPaths += 'src/main/java'
 }
diff --git a/android/app/src/main/java/io/v/syncslides/DeckChooserFragment.java b/android/app/src/main/java/io/v/syncslides/DeckChooserFragment.java
index f142371..a2a612d 100644
--- a/android/app/src/main/java/io/v/syncslides/DeckChooserFragment.java
+++ b/android/app/src/main/java/io/v/syncslides/DeckChooserFragment.java
@@ -9,6 +9,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.DocumentsContract;
+import android.support.design.widget.FloatingActionButton;
 import android.support.v4.app.Fragment;
 import android.support.v4.provider.DocumentFile;
 import android.support.v7.widget.GridLayoutManager;
@@ -31,6 +32,8 @@
 import java.io.IOException;
 import java.util.UUID;
 
+import io.v.syncslides.db.DB;
+
 /**
  * This fragment contains the list of decks as well as the FAB to create a new
  * deck.
@@ -42,9 +45,9 @@
     private static final String ARG_SECTION_NUMBER = "section_number";
     private static final String TAG = "DeckChooserFragment";
     private static final int REQUEST_CODE_IMPORT_DECK = 1000;
-//    private RecyclerView mRecyclerView;
-//    private GridLayoutManager mLayoutManager;
-//    private DeckListAdapter mAdapter;
+    private RecyclerView mRecyclerView;
+    private GridLayoutManager mLayoutManager;
+    private DeckListAdapter mAdapter;
 
     /**
      * Returns a new instance of this fragment for the given section number.
@@ -61,34 +64,36 @@
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         View rootView = inflater.inflate(R.layout.fragment_deck_chooser, container, false);
-//        FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id.new_deck_fab);
-//        fab.setOnClickListener(new View.OnClickListener() {
-//            @Override
-//            public void onClick(View v) {
-//                onImportDeck();
-//            }
-//        });
-//        mRecyclerView = (RecyclerView) rootView.findViewById(R.id.deck_grid);
-//        mRecyclerView.setHasFixedSize(true);
-//
-//        // Statically set the span count (i.e. number of columns) for now...  See below.
-//        mLayoutManager = new GridLayoutManager(getContext(), 2);
-//        mRecyclerView.setLayoutManager(mLayoutManager);
-//        // Dynamically set the span based on the screen width.  Cribbed from
-//        // http://stackoverflow.com/questions/26666143/recyclerview-gridlayoutmanager-how-to-auto-detect-span-count
-//        mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
-//                new ViewTreeObserver.OnGlobalLayoutListener() {
-//                    @Override
-//                    public void onGlobalLayout() {
-//                        mRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-//                        int viewWidth = mRecyclerView.getMeasuredWidth();
-//                        float cardViewWidth = getActivity().getResources().getDimension(
-//                                R.dimen.deck_card_width);
-//                        int newSpanCount = (int) Math.floor(viewWidth / cardViewWidth);
-//                        mLayoutManager.setSpanCount(newSpanCount);
-//                        mLayoutManager.requestLayout();
-//                    }
-//                });
+        FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id.new_deck_fab);
+        fab.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                onImportDeck();
+            }
+        });
+        mRecyclerView = (RecyclerView) rootView.findViewById(R.id.deck_grid);
+        // The cards for the decks are always the same size.
+        mRecyclerView.setHasFixedSize(true);
+
+        // Statically set the span count (i.e. number of columns) for now...  See below.
+        mLayoutManager = new GridLayoutManager(getContext(), 2);
+        mRecyclerView.setLayoutManager(mLayoutManager);
+        // Dynamically set the span based on the screen width.  Cribbed from
+        // http://stackoverflow.com/questions/26666143/recyclerview-gridlayoutmanager-how-to-auto-detect-span-count
+        mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        int viewWidth = mRecyclerView.getMeasuredWidth();
+                        float cardViewWidth = getActivity().getResources().getDimension(
+                                R.dimen.deck_card_width);
+                        int newSpanCount = (int) Math.floor(viewWidth / cardViewWidth);
+                        mLayoutManager.setSpanCount(newSpanCount);
+                        mLayoutManager.requestLayout();
+                    }
+                });
+        mAdapter = new DeckListAdapter(DB.Singleton.get());
 
         return rootView;
     }
@@ -120,19 +125,17 @@
     @Override
     public void onStart() {
         super.onStart();
-//        Log.i(TAG, "Starting");
-//        DB db = DB.Singleton.get(getActivity().getApplicationContext());
-//        mAdapter = new DeckListAdapter(db);
-//        mAdapter.start(getActivity().getApplicationContext());
-//        mRecyclerView.setAdapter(mAdapter);
+        Log.i(TAG, "Starting");
+        mAdapter.start();
+        mRecyclerView.setAdapter(mAdapter);
     }
 
     @Override
     public void onStop() {
         super.onStop();
-//        Log.i(TAG, "Stopping");
-//        mAdapter.stop();
-//        mAdapter = null;
+        Log.i(TAG, "Stopping");
+        mAdapter.stop();
+        mRecyclerView.setAdapter(null);
     }
 
     /**
diff --git a/android/app/src/main/java/io/v/syncslides/DeckListAdapter.java b/android/app/src/main/java/io/v/syncslides/DeckListAdapter.java
new file mode 100644
index 0000000..6558118
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/DeckListAdapter.java
@@ -0,0 +1,126 @@
+// 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.syncslides;
+
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toolbar;
+
+import java.util.Calendar;
+import java.util.Locale;
+
+import io.v.syncslides.db.DB;
+import io.v.syncslides.model.Deck;
+import io.v.syncslides.model.DynamicList;
+import io.v.syncslides.model.ListListener;
+
+/**
+ * Provides a list of decks to be shown in the RecyclerView of the
+ * DeckChooserFragment.
+ */
+public class DeckListAdapter extends RecyclerView.Adapter<DeckListAdapter.ViewHolder>
+        implements ListListener {
+    private static final String TAG = "DeckListAdapter";
+    private DynamicList<Deck> mDecks;
+    private DB mDB;
+
+    public DeckListAdapter(DB db) {
+        mDB = db;
+    }
+
+    /**
+     * Starts background monitoring of the underlying data.
+     */
+    public void start() {
+        mDecks = mDB.getDecks();
+        mDecks.addListener(this);
+    }
+
+    /**
+     * Stops any background monitoring of the underlying data.
+     */
+    public void stop() {
+        mDecks.removeListener(this);
+        mDecks = null;
+    }
+
+    @Override
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int i) {
+        View v = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.deck_card, parent, false);
+        return new ViewHolder(v);
+    }
+
+    @Override
+    public void onBindViewHolder(final ViewHolder holder, final int deckIndex) {
+        final Deck deck = mDecks.get(deckIndex);
+
+        // TODO(afergan): Set actual date here.
+        final Calendar cal = Calendar.getInstance();
+        holder.mToolbarLastOpened.setText("Opened on "
+                + cal.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.US) + " "
+                + cal.get(Calendar.DAY_OF_MONTH) + ", " + cal.get(Calendar.YEAR));
+
+        holder.mToolbarLastOpened.setVisibility(View.VISIBLE);
+        holder.mToolbarLiveNow.setVisibility(View.GONE);
+        holder.mToolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
+            @Override
+            public boolean onMenuItemClick(MenuItem item) {
+                switch (item.getItemId()) {
+                    case R.id.action_delete_deck:
+                        // TODO(kash): Implement delete.
+                        // mDB.deleteDeck(deck.getId());
+                        return true;
+                }
+                return false;
+            }
+        });
+
+        holder.mToolbarTitle.setText(deck.getTitle());
+        holder.mThumb.setImageBitmap(deck.getThumb());
+        holder.mThumb.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Log.d(TAG, "Clicking through to PresentationActivity.");
+            }
+        });
+    }
+
+    @Override
+    public int getItemCount() {
+        return mDecks.getItemCount();
+    }
+
+    @Override
+    public void onError(Exception e) {
+        // TODO(kash): Not sure what to do here...  Call start()/stop() to reset
+        // the DynamicList?
+    }
+
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+        public final ImageView mThumb;
+        public final Toolbar mToolbar;
+        public final TextView mToolbarTitle;
+        public final TextView mToolbarLiveNow;
+        public final TextView mToolbarLastOpened;
+
+        public ViewHolder(final View itemView) {
+            super(itemView);
+            mThumb = (ImageView) itemView.findViewById(R.id.deck_thumb);
+            mToolbar = (Toolbar) itemView.findViewById(R.id.deck_card_toolbar);
+            mToolbarTitle = (TextView) itemView.findViewById(R.id.deck_card_toolbar_title);
+            mToolbarLiveNow = (TextView) itemView.findViewById(R.id.deck_card_toolbar_live_now);
+            mToolbarLastOpened =
+                    (TextView) itemView.findViewById(R.id.deck_card_toolbar_last_opened);
+            mToolbar.inflateMenu(R.menu.deck_card);
+        }
+    }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/db/DB.java b/android/app/src/main/java/io/v/syncslides/db/DB.java
index 8281671..bdb6b0a 100644
--- a/android/app/src/main/java/io/v/syncslides/db/DB.java
+++ b/android/app/src/main/java/io/v/syncslides/db/DB.java
@@ -36,4 +36,9 @@
      * Perform initialization steps.
      */
     void init(Context context) throws InitException;
+
+    /**
+     * Returns a dynamically updating list of decks that are visible to the user.
+     */
+    DynamicList<Deck> getDecks();
 }
diff --git a/android/app/src/main/java/io/v/syncslides/db/DeckWatcher.java b/android/app/src/main/java/io/v/syncslides/db/DeckWatcher.java
new file mode 100644
index 0000000..f3bc39f
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/DeckWatcher.java
@@ -0,0 +1,97 @@
+// 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.syncslides.db;
+
+import android.util.Log;
+
+import java.util.Comparator;
+import java.util.List;
+
+import io.v.impl.google.naming.NamingUtil;
+import io.v.syncslides.model.Deck;
+import io.v.syncslides.model.DeckImpl;
+import io.v.v23.VIterable;
+import io.v.v23.context.VContext;
+import io.v.v23.services.watch.ResumeMarker;
+import io.v.v23.syncbase.nosql.BatchDatabase;
+import io.v.v23.syncbase.nosql.ChangeType;
+import io.v.v23.syncbase.nosql.Database;
+import io.v.v23.syncbase.nosql.DatabaseCore;
+import io.v.v23.syncbase.nosql.WatchChange;
+import io.v.v23.vdl.VdlAny;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+import static io.v.v23.VFutures.sync;
+
+/**
+ * Watches all of the decks in syncbase for changes.  Decks are sorted by their ID (which
+ * is a random number).  TODO(kash): Sort by something more useful.
+ */
+class DeckWatcher implements Watcher<Deck> {
+
+    private static final String TAG = "DeckWatcher";
+    private final Database mDB;
+
+    DeckWatcher(Database db) {
+        mDB = db;
+    }
+
+    public void watch(VContext context, Listener<Deck> listener) {
+        try {
+            BatchDatabase batch = sync(mDB.beginBatch(context, null));
+            ResumeMarker resumeMarker = sync(batch.getResumeMarker(context));
+            DatabaseCore.QueryResults results = sync(batch.exec(context,
+                    "SELECT k, v FROM Decks WHERE Type(v) like \"%VDeck\""));
+            for (List<VdlAny> row : results) {
+                if (row.size() != 2) {
+                    throw new VException("Wrong number of columns: " + row.size());
+                }
+                String key = (String) row.get(0).getElem();
+                Log.i(TAG, "Fetched deck " + key);
+                VDeck vDeck = (VDeck) row.get(1).getElem();
+                listener.onPut(new DeckImpl(vDeck.getTitle(), vDeck.getThumbnail(), key));
+            }
+            if (results.error() != null) {
+                throw results.error();
+            }
+
+            VIterable<WatchChange> changes = sync(mDB.watch(
+                    context, SyncbaseDB.DECKS_TABLE, "", resumeMarker));
+            for (WatchChange change : changes) {
+                final String key = change.getRowName();
+                // Ignore slide changes.
+                if (NamingUtil.split(key).size() != 1) {
+                    continue;
+                }
+                Log.d(TAG, "Processing change to deck: " + key);
+                if (change.getChangeType().equals(ChangeType.PUT_CHANGE)) {
+                    // New deck or change to an existing deck.
+                    VDeck vDeck = null;
+                    try {
+                        vDeck = (VDeck) VomUtil.decode(change.getVomValue(), VDeck.class);
+                    } catch (VException e) {
+                        Log.e(TAG, "Couldn't decode deck: " + e.toString());
+                        continue;
+                    }
+                    final Deck deck = new DeckImpl(
+                            vDeck.getTitle(), vDeck.getThumbnail(), key);
+                    listener.onPut(deck);
+                } else {  // ChangeType.DELETE_CHANGE
+                    listener.onDelete(new DeckImpl(null, null, key));
+                }
+            }
+            if (changes.error() != null) {
+                throw changes.error();
+            }
+            Log.d(TAG, "Deck change thread exiting");
+        } catch (Exception e) {
+            listener.onError(e);
+        }
+    }
+
+    public int compare(Deck lhs, Deck rhs) {
+        return lhs.getId().compareTo(rhs.getId());
+    }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/db/SyncbaseDB.java b/android/app/src/main/java/io/v/syncslides/db/SyncbaseDB.java
index 8ad5fce..9d05596 100644
--- a/android/app/src/main/java/io/v/syncslides/db/SyncbaseDB.java
+++ b/android/app/src/main/java/io/v/syncslides/db/SyncbaseDB.java
@@ -22,6 +22,7 @@
 import io.v.syncslides.V23;
 import io.v.syncslides.model.Deck;
 import io.v.syncslides.model.DynamicList;
+import io.v.syncslides.model.NoopList;
 import io.v.v23.context.VContext;
 import io.v.v23.rpc.Server;
 import io.v.v23.security.BlessingPattern;
@@ -35,13 +36,14 @@
 import io.v.v23.syncbase.nosql.Database;
 import io.v.v23.syncbase.nosql.Table;
 import io.v.v23.verror.VException;
+import static io.v.v23.VFutures.sync;
 
 public class SyncbaseDB implements DB {
     private static final String TAG = "SyncbaseDB";
     private static final String SYNCBASE_APP = "syncslides";
     private static final String SYNCBASE_DB = "syncslides";
-    private static final String DECKS_TABLE = "Decks";
-    private static final String NOTES_TABLE = "Notes";
+    static final String DECKS_TABLE = "Decks";
+    static final String NOTES_TABLE = "Notes";
     static final String PRESENTATIONS_TABLE = "Presentations";
     static final String CURRENT_SLIDE = "CurrentSlide";
     static final String QUESTIONS = "questions";
@@ -80,7 +82,7 @@
         setupSyncbase();
     }
 
-    // TODO(kash): Run this in an AsyncTask so it doesn't block the UI.
+    // TODO(kash): Do this asynchronously so it doesn't block the UI.
     private void setupSyncbase() throws InitException {
         Blessings blessings = V23.Singleton.get().getBlessings();
         AccessList everyoneAcl = new AccessList(
@@ -118,24 +120,24 @@
             // Now that we've started Syncbase, set up our connections to it.
             SyncbaseService service = Syncbase.newService(serverName);
             SyncbaseApp app = service.getApp(SYNCBASE_APP);
-            if (!app.exists(mVContext)) {
-                app.create(mVContext, mPermissions);
+            if (!sync(app.exists(mVContext))) {
+                sync(app.create(mVContext, mPermissions));
             }
             mDB = app.getNoSqlDatabase(SYNCBASE_DB, null);
-            if (!mDB.exists(mVContext)) {
-                mDB.create(mVContext, mPermissions);
+            if (!sync(mDB.exists(mVContext))) {
+                sync(mDB.create(mVContext, mPermissions));
             }
             Table decks = mDB.getTable(DECKS_TABLE);
-            if (!decks.exists(mVContext)) {
-                decks.create(mVContext, mPermissions);
+            if (!sync(decks.exists(mVContext))) {
+                sync(decks.create(mVContext, mPermissions));
             }
             Table notes = mDB.getTable(NOTES_TABLE);
-            if (!notes.exists(mVContext)) {
-                notes.create(mVContext, mPermissions);
+            if (!sync(notes.exists(mVContext))) {
+                sync(notes.create(mVContext, mPermissions));
             }
             Table presentations = mDB.getTable(PRESENTATIONS_TABLE);
-            if (!presentations.exists(mVContext)) {
-                presentations.create(mVContext, mPermissions);
+            if (!sync(presentations.exists(mVContext))) {
+                sync(presentations.create(mVContext, mPermissions));
             }
             //importDecks();
         } catch (VException e) {
@@ -143,4 +145,12 @@
         }
         mInitialized = true;
     }
+
+    @Override
+    public DynamicList<Deck> getDecks() {
+        if (!mInitialized) {
+            return new NoopList<>();
+        }
+        return new WatchedList<Deck>(mVContext, new DeckWatcher(mDB));
+    }
 }
diff --git a/android/app/src/main/java/io/v/syncslides/db/WatchedList.java b/android/app/src/main/java/io/v/syncslides/db/WatchedList.java
new file mode 100644
index 0000000..cdb367b
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/WatchedList.java
@@ -0,0 +1,161 @@
+// 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.syncslides.db;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import io.v.syncslides.model.DynamicList;
+import io.v.syncslides.model.ListListener;
+import io.v.v23.context.CancelableVContext;
+import io.v.v23.context.VContext;
+
+/**
+ * WatchedList manages an in-memory copy of data that is in syncbase.  The Watcher
+ * passed to the WatchedList constructor contains the logic for fetching the
+ * actual data from syncbase and sorting it appropriately.  WatchedList keeps
+ * track of the data and listeners and notifies the listeners when the data
+ * changes.
+ */
+class WatchedList<E> implements DynamicList<E> {
+    private static final String TAG = "WatchedList";
+
+    private final VContext mBaseContext;
+    private final Set<ListListener> mListeners;
+    private final ExecutorService mExecutor;
+    private final Handler mHandler;
+    private final Watcher mWatcher;
+    private final List<E> mElems;
+    private CancelableVContext mCurrentContext;
+
+    WatchedList(VContext context, Watcher watcher) {
+        mListeners = Sets.newHashSet();
+        mBaseContext = context;
+        mExecutor = Executors.newSingleThreadExecutor();
+        mHandler = new Handler(Looper.getMainLooper());
+        mWatcher = watcher;
+        mElems = Lists.newArrayList();
+    }
+
+    @Override
+    public int getItemCount() {
+        return mElems.size();
+    }
+
+    @Override
+    public E get(int i) {
+        return mElems.get(i);
+    }
+
+    @Override
+    public void addListener(final ListListener listener) {
+        if (mListeners.isEmpty()) {
+            mListeners.add(listener);
+            mCurrentContext = mBaseContext.withCancel();
+            mExecutor.submit(new Runnable() {
+                @Override
+                public void run() {
+                    mWatcher.watch(mCurrentContext, new Watcher.Listener<E>() {
+                        // TODO(kash): Switch to retrolambda to save on the boilerplate.
+                        @Override
+                        public void onPut(final E elem) {
+                            mHandler.post(new Runnable() {
+                                @Override
+                                public void run() {
+                                    put(elem);
+                                }
+                            });
+                        }
+
+                        @Override
+                        public void onDelete(final E elem) {
+                            mHandler.post(new Runnable() {
+                                @Override
+                                public void run() {
+                                    delete(elem);
+                                }
+                            });
+                        }
+
+                        @Override
+                        public void onError(final Exception e) {
+                            mHandler.post(new Runnable() {
+                                @Override
+                                public void run() {
+                                    error(e);
+                                }
+                            });
+                        }
+                    });
+                }
+            });
+        }
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                listener.notifyDataSetChanged();
+            }
+        });
+    }
+
+    @Override
+    public void removeListener(ListListener listener) {
+        mListeners.remove(listener);
+        if (mListeners.isEmpty()) {
+            // Stop mWatcher via cancel.
+            mCurrentContext.cancel();
+            mCurrentContext = null;
+            mHandler.removeCallbacksAndMessages(null);
+        }
+    }
+
+    private void put(E elem) {
+        int idx = 0;
+        for (; idx < mElems.size(); idx++) {
+            int comp = mWatcher.compare(mElems.get(idx), elem);
+            if (comp == 0) {
+                // Existing entry with a change.
+                mElems.set(idx, elem);
+                for (ListListener listener : mListeners) {
+                    listener.notifyItemChanged(idx);
+                }
+                return;
+            } else if (comp > 0) {
+                break;
+            }
+        }
+        // New element.
+        mElems.add(idx, elem);
+        for (ListListener listener : mListeners) {
+            listener.notifyItemInserted(idx);
+        }
+
+    }
+
+    private void delete(E elem) {
+        for (int i = 0; i < mElems.size(); i++) {
+            if (mWatcher.compare(mElems.get(i), elem) == 0) {
+                mElems.remove(i);
+                for (ListListener listener : mListeners) {
+                    listener.notifyItemRemoved(i);
+                }
+            }
+        }
+    }
+
+    private void error(Exception e) {
+        for (ListListener listener : mListeners) {
+            listener.onError(e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/io/v/syncslides/db/Watcher.java b/android/app/src/main/java/io/v/syncslides/db/Watcher.java
new file mode 100644
index 0000000..0e3c8d7
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/Watcher.java
@@ -0,0 +1,46 @@
+// 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.syncslides.db;
+
+import java.util.Comparator;
+
+import io.v.v23.context.VContext;
+
+/**
+ * Implementations of Watcher work closely with @see io.v.syncslides.db.WatchedList to
+ * watch a set of data in syncbase for changes.  In addition to watching for changes,
+ * a Watcher also knows how to sort the data appropriately for display in a UI.
+ */
+public interface Watcher<E> extends Comparator<E> {
+    /**
+     * Fetches the initial data set from Syncbase and then watches it for subsequent changes.
+     * Both the initial data set and changes are passed to {@code listener}.
+     *
+     * @param context to be used for communications with Syncbase
+     * @param listener receives notifications for the initial data and for changes
+     */
+    void watch(VContext context, Listener<E> listener);
+
+    /**
+     * Receives notifications of type E for the Watcher's data set.  These
+     * notifications will run in an arbitrary thread.
+     */
+    interface Listener<E> {
+        /**
+         * Notifies that {@code elem} was inserted or changed.
+         */
+        void onPut(E elem);
+
+        /**
+         * Notifies that {@code elem} was deleted.
+         */
+        void onDelete(E elem);
+
+        /**
+         * Notifies that there was an error and the Watcher is no longer running.
+         */
+        void onError(Exception e);
+    }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/db/schema.vdl b/android/app/src/main/java/io/v/syncslides/db/schema.vdl
new file mode 100644
index 0000000..4f290dc
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/schema.vdl
@@ -0,0 +1,63 @@
+// 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 db
+
+// Deck is the metadata for a set of slides.
+type VDeck struct {
+    Title string
+    Thumbnail []byte
+}
+
+// Slide contains the image content for the slide.
+type VSlide struct {
+    // A low-res version of the image.
+    Thumbnail []byte
+    // A high-res version of the image.
+    // TODO(spetrovic): The type of this field should really be a BlobRef, but we can't reference
+    // non-VDLROOT vdl types from Java just yet.
+    ImageRef string
+}
+
+// Note contains private-to-the-user notes for a specific slide.
+type VNote struct {
+    Text string
+}
+
+// Presentation represents a live display of a Deck.
+type VPresentation struct {
+    // If the presenter has handed control of the presentation to an audience member,
+    // driver will be present.  When the presenter takes back control, this
+    // will switch back to the empty string.
+    Driver ?VPerson  // ? means optional.
+}
+
+// Person represents either an audience member or the presenter.
+type VPerson struct{
+    Blessing string
+    // The person's full name.
+    Name string
+}
+
+// CurrentSlide contains state for the live presentation.  It is separate from the
+// Presentation so that the presenter can temporarily delegate control of the
+// CurrentSlide without giving up control of the entire presentation.
+type VCurrentSlide struct {
+    // The number of the slide that the presenter is talking about.
+    Num int32
+    // In the future, we could add markup/doodles here.  That markup would be transient
+    // if stored here.  Maybe better to put it in a separate row...
+}
+
+// Question represents a member of the audience asking a question of the presenter.
+// TODO(kash): Add support for the user to type in their question.  Right now, they
+// need to ask their question verbally.
+type VQuestion struct {
+    // The person who asked the question.
+    Questioner VPerson
+    // Time when the question was asked in milliseconds since the epoch.
+    Time int64
+    // Track whether this question has been answered.
+    Answered bool
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/io/v/syncslides/model/DeckImpl.java b/android/app/src/main/java/io/v/syncslides/model/DeckImpl.java
new file mode 100644
index 0000000..8b877f3
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/model/DeckImpl.java
@@ -0,0 +1,47 @@
+// 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.syncslides.model;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+/**
+ * An implementation of {@link Deck} interface.
+ */
+public class DeckImpl implements Deck {
+    private final String mTitle;
+    private final byte[] mThumb;
+    private final String mDeckId;
+
+    public DeckImpl(String title, byte[] thumb, String deckId) {
+        mTitle = title;
+        mThumb = thumb;
+        mDeckId = deckId;
+    }
+
+    public String toString() {
+        return "[title=\"" + (mTitle == null ? "unknown" : mTitle) +
+                "\", id=" + (mDeckId == null ? "unknown" : mDeckId) +
+                ", thumb=" + (mThumb == null ? "no" : "yes") + "]";
+    }
+
+    @Override
+    public Bitmap getThumb() {
+        return BitmapFactory.decodeByteArray(mThumb, 0, mThumb.length);
+    }
+    @Override
+    public byte[] getThumbData() {
+        return mThumb;
+    }
+    @Override
+    public String getTitle() {
+        return mTitle;
+    }
+    @Override
+    public String getId() {
+        return mDeckId;
+    }
+
+}
diff --git a/android/app/src/main/java/io/v/syncslides/model/ListListener.java b/android/app/src/main/java/io/v/syncslides/model/ListListener.java
index 015d621..6a61c75 100644
--- a/android/app/src/main/java/io/v/syncslides/model/ListListener.java
+++ b/android/app/src/main/java/io/v/syncslides/model/ListListener.java
@@ -8,7 +8,9 @@
  * Callbacks for list changes.
  */
 public interface ListListener {
+    void notifyDataSetChanged();
     void notifyItemChanged(int position);
     void notifyItemInserted(int position);
     void notifyItemRemoved(int position);
+    void onError(Exception e);
 }
diff --git a/android/app/src/main/java/io/v/syncslides/model/NoopList.java b/android/app/src/main/java/io/v/syncslides/model/NoopList.java
new file mode 100644
index 0000000..0e91d38
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/model/NoopList.java
@@ -0,0 +1,33 @@
+// 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.syncslides.model;
+
+/**
+ * An implementation of DynamicList that doesn't do anything.  Useful for when the
+ * underlying dataset has not loaded.
+ */
+public class NoopList<E> implements DynamicList<E> {
+    @Override
+    public int getItemCount() {
+        return 0;
+    }
+
+    @Override
+    public E get(int i) {
+        return null;
+    }
+
+    @Override
+    public void addListener(ListListener listener) {
+        // Do nothing.
+    }
+
+    @Override
+    public void removeListener(ListListener listener) {
+        // Do nothing.
+    }
+
+}
+
diff --git a/android/app/src/main/res/layout/deck_card.xml b/android/app/src/main/res/layout/deck_card.xml
new file mode 100644
index 0000000..66302b8
--- /dev/null
+++ b/android/app/src/main/res/layout/deck_card.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.CardView
+    android:id="@+id/deck_card"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/deck_card_width"
+    android:layout_height="wrap_content"
+    android:layout_margin="@dimen/deck_card_margin"
+    >
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <!-- A thumbnail of the deck title slide. -->
+        <ImageView
+            android:id="@+id/deck_thumb"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/deck_thumb_height"
+            android:scaleType="centerCrop"/>
+
+        <!-- Display the title of the deck and a menu to configure it. -->
+        <Toolbar
+            android:id="@+id/deck_card_toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/card_toolbar_height"
+            android:contentInsetStart="@dimen/toolbar_inset"
+            android:theme="@style/ThemeToolbar">
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:orientation="vertical">
+
+                <TextView
+                    android:id="@+id/deck_card_toolbar_title"
+                    style="@style/DeckTitleFont"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"/>
+
+                <TextView
+                    android:id="@+id/deck_card_toolbar_live_now"
+                    style="@style/DeckLiveNowFont"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="@dimen/toolbar_text_top_margin"
+                    android:paddingEnd="@dimen/toolbar_text_live_now_padding"
+                    android:paddingStart="@dimen/toolbar_text_live_now_padding"/>
+
+                <TextView
+                    android:id="@+id/deck_card_toolbar_last_opened"
+                    style="@style/DeckLastOpenedFont"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="@dimen/toolbar_text_top_margin"/>
+
+            </LinearLayout>
+        </Toolbar>
+
+    </LinearLayout>
+
+</android.support.v7.widget.CardView>
diff --git a/android/app/src/main/res/layout/fragment_deck_chooser.xml b/android/app/src/main/res/layout/fragment_deck_chooser.xml
index 2716b62..f1e634e 100644
--- a/android/app/src/main/res/layout/fragment_deck_chooser.xml
+++ b/android/app/src/main/res/layout/fragment_deck_chooser.xml
@@ -6,11 +6,11 @@
     android:layout_height="match_parent"
     android:background="@color/snow_2">
 
-    <!--<android.support.v7.widget.RecyclerView-->
-        <!--android:id="@+id/deck_grid"-->
-        <!--android:layout_width="match_parent"-->
-        <!--android:layout_height="match_parent"-->
-        <!--android:scrollbars="vertical"/>-->
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/deck_grid"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scrollbars="vertical"/>
 
     <android.support.design.widget.FloatingActionButton
         android:id="@+id/new_deck_fab"
diff --git a/android/app/src/main/res/menu/deck_card.xml b/android/app/src/main/res/menu/deck_card.xml
new file mode 100644
index 0000000..ab5f030
--- /dev/null
+++ b/android/app/src/main/res/menu/deck_card.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+        <item android:id="@+id/action_delete_deck"
+              android:title="@string/action_delete_deck"
+              android:showAsAction="never" />
+</menu>
\ No newline at end of file