syncslides: Session and Presentation interfaces.

The Session holds the deckId and presentationId, paramters that
we stored in the Bundle in the old version of Syncslides.  The
plan is to add more functionality to the Session such as the
ability to watch the local slide number for changes.

The Presentation holds all of the state for the presentation that
the user is currently viewing.  The plan is for SyncbaseDB to cache
the most recently used Presentation so that when the user rotates
their phone, we don't need to refetch all of the slide data from
the database-- it'll be right there in the Presentation.

This change adds the first of many methods to Presentation: getSlides().
I plan to use this method in the slide list view of the app.
I didn't include all of that UI code in this change because it
was getting too big.

Change-Id: Iab053dea8798450b9107338bd32033b584726120
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 8b00aa8..884e0c3 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
@@ -11,7 +11,9 @@
 import io.v.syncslides.InitException;
 import io.v.syncslides.model.Deck;
 import io.v.syncslides.model.DynamicList;
+import io.v.syncslides.model.Session;
 import io.v.syncslides.model.Slide;
+import io.v.v23.verror.VException;
 
 /**
  * Provides high-level methods for getting and setting the state of SyncSlides.
@@ -41,6 +43,19 @@
     void init(Context context) throws InitException;
 
     /**
+     * Returns the Session for the given ID.  This method is synchronous because
+     * it fetches a small amount of data and therefore it can complete quickly.
+     * Additionally, very little UI can be rendered without the information
+     * contained in the Session.
+     */
+    Session getSession(String sessionId) throws VException;
+
+    /**
+     * Returns the Presentation for the given session.
+     */
+    Presentation getPresentation(Session session);
+
+    /**
      * 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/DBSlide.java b/android/app/src/main/java/io/v/syncslides/db/DBSlide.java
new file mode 100644
index 0000000..3b79b05
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/DBSlide.java
@@ -0,0 +1,62 @@
+// 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.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import io.v.syncslides.model.Slide;
+
+/**
+ * DBSlide is backed by our database-specific datastructures.  It lazily fetches the full-size
+ * image from syncbase.
+ */
+class DBSlide implements Slide {
+
+    private final String mId;
+    private final VSlide mVSlide;
+    private final String mNotes;
+
+    DBSlide(String id, VSlide vSlide, String notes) {
+        mId = id;
+        mVSlide = vSlide;
+        mNotes = notes;
+    }
+
+    @Override
+    public String getId() {
+        return mId;
+    }
+
+    @Override
+    public Bitmap getThumb() {
+        byte[] thumbnail = mVSlide.getThumbnail();
+        return BitmapFactory.decodeByteArray(thumbnail, 0 /* offset */, thumbnail.length);
+    }
+
+    @Override
+    public byte[] getThumbData() {
+        return mVSlide.getThumbnail();
+    }
+
+    @Override
+    public Bitmap getImage() {
+        // TODO(kash): I think I want to change this API to return a future so the UI is
+        // not blocked on loading the image.
+        throw new RuntimeException("Implement me");
+    }
+
+    @Override
+    public byte[] getImageData() {
+        // TODO(kash): I think I want to change this API to return a future so the UI is
+        // not blocked on loading the image.
+        throw new RuntimeException("Implement me");
+    }
+
+    @Override
+    public String getNotes() {
+        return mNotes;
+    }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/db/Presentation.java b/android/app/src/main/java/io/v/syncslides/db/Presentation.java
new file mode 100644
index 0000000..c0933fa
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/Presentation.java
@@ -0,0 +1,20 @@
+// 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 io.v.syncslides.model.DynamicList;
+import io.v.syncslides.model.Slide;
+
+/**
+ * A Presentation acts as a bridge between the UI and the database for all of the state related
+ * to a presentation.
+ */
+public interface Presentation {
+
+    /**
+     * Returns a dynamically updating list of slides in the deck.
+     */
+    DynamicList<Slide> getSlides();
+}
diff --git a/android/app/src/main/java/io/v/syncslides/db/SlideWatcher.java b/android/app/src/main/java/io/v/syncslides/db/SlideWatcher.java
new file mode 100644
index 0000000..391b42c
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/SlideWatcher.java
@@ -0,0 +1,129 @@
+// 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.List;
+
+import io.v.impl.google.naming.NamingUtil;
+import io.v.syncslides.model.Slide;
+import io.v.syncslides.model.SlideImpl;
+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.Table;
+import io.v.v23.syncbase.nosql.WatchChange;
+import io.v.v23.vdl.VdlAny;
+import io.v.v23.verror.NoExistException;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+
+import static io.v.v23.VFutures.sync;
+
+/**
+ * Watches the slides in a single deck for changes.  Slides are sorted by their key.
+ */
+class SlideWatcher implements Watcher<Slide> {
+
+    private static final String TAG = "SlideWatcher";
+    private final Database mDb;
+    private final String mDeckId;
+
+    SlideWatcher(Database db, String deckId) {
+        mDb = db;
+        mDeckId = deckId;
+    }
+
+    @Override
+    public void watch(VContext context, Listener<Slide> listener) {
+        try {
+            BatchDatabase batch = sync(mDb.beginBatch(context, null));
+            ResumeMarker watchMarker = sync(batch.getResumeMarker(context));
+            fetchInitialState(context, listener, batch);
+            watchChanges(context, listener, watchMarker);
+        } catch (VException e) {
+            listener.onError(e);
+        }
+    }
+
+    @Override
+    public int compare(Slide lhs, Slide rhs) {
+        return lhs.getId().compareTo(rhs.getId());
+    }
+
+    private void fetchInitialState(VContext context, Listener<Slide> listener,
+                                   BatchDatabase batch) throws VException {
+        Table notesTable = batch.getTable(SyncbaseDB.NOTES_TABLE);
+        String query = "SELECT k, v FROM Decks WHERE Type(v) LIKE \"%VSlide\" " +
+                "AND k LIKE \"" + NamingUtil.join(mDeckId, "slides") + "%\"";
+        DatabaseCore.QueryResults results = sync(batch.exec(context, query));
+        for (List<VdlAny> row : results) {
+            if (row.size() != 2) {
+                throw new VException("Wrong number of columns: " + row.size());
+            }
+            final String key = (String) row.get(0).getElem();
+            Log.i(TAG, "Fetched slide " + key);
+            VSlide slide = (VSlide) row.get(1).getElem();
+            String notes = notesForSlide(context, notesTable, key);
+            Slide newSlide = new DBSlide(key, slide, notes);
+            listener.onPut(newSlide);
+        }
+    }
+
+    private void watchChanges(VContext context, Listener<Slide> listener, ResumeMarker watchMarker)
+            throws VException {
+        Table notesTable = mDb.getTable(SyncbaseDB.NOTES_TABLE);
+        VIterable<WatchChange> changes =
+                sync(mDb.watch(context, SyncbaseDB.DECKS_TABLE, mDeckId, watchMarker));
+        for (WatchChange change : changes) {
+            String key = change.getRowName();
+            if (isDeckKey(key)) {
+                Log.d(TAG, "Ignoring deck change: " + key);
+                continue;
+            }
+            if (change.getChangeType().equals(ChangeType.PUT_CHANGE)) {
+                // New slide or change to an existing slide.
+                VSlide vSlide = null;
+                try {
+                    vSlide = (VSlide) VomUtil.decode(change.getVomValue(), VSlide.class);
+                } catch (VException e) {
+                    Log.e(TAG, "Couldn't decode slide: " + e.toString());
+                    continue; // Just skip it.
+                }
+                String notes = notesForSlide(context, notesTable, key);
+                Slide newSlide = new DBSlide(key, vSlide, notes);
+                listener.onPut(newSlide);
+            } else { // ChangeType.DELETE_CHANGE
+                listener.onDelete(new SlideImpl(key, null, null, null));
+            }
+        }
+        if (changes.error() != null) {
+            throw changes.error();
+        }
+    }
+
+    /**
+     * Returns true if {@code key} looks like a VDeck and not a VSlide.
+     */
+    private boolean isDeckKey(String key) {
+        return NamingUtil.split(key).size() <= 1;
+    }
+
+    private static String notesForSlide(VContext context, Table notesTable, String key)
+            throws VException {
+        try {
+            VNote note = (VNote) sync(notesTable.get(context, key, VNote.class));
+            return note.getText();
+        } catch (NoExistException e) {
+            // It is ok for the notes to not exist for a slide.
+            return "";
+        }
+    }
+}
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 01133fa..0d2a736 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
@@ -16,6 +16,8 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 
+import org.joda.time.Duration;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -31,7 +33,9 @@
 import io.v.syncslides.model.Deck;
 import io.v.syncslides.model.DynamicList;
 import io.v.syncslides.model.NoopList;
+import io.v.syncslides.model.Session;
 import io.v.syncslides.model.Slide;
+import io.v.v23.context.CancelableVContext;
 import io.v.v23.context.VContext;
 import io.v.v23.rpc.Server;
 import io.v.v23.security.BlessingPattern;
@@ -49,13 +53,14 @@
 
 import static io.v.v23.VFutures.sync;
 
-public class SyncbaseDB implements DB {
+class SyncbaseDB implements DB {
     private static final String TAG = "SyncbaseDB";
     private static final String SYNCBASE_APP = "syncslides";
     private static final String SYNCBASE_DB = "syncslides";
     static final String DECKS_TABLE = "Decks";
     static final String NOTES_TABLE = "Notes";
     static final String PRESENTATIONS_TABLE = "Presentations";
+    static final String UI_TABLE = "UI";
     static final String CURRENT_SLIDE = "CurrentSlide";
     static final String QUESTIONS = "questions";
     private static final String SYNCGROUP_PRESENTATION_DESCRIPTION = "Live Presentation";
@@ -152,6 +157,10 @@
             if (!sync(presentations.exists(mVContext))) {
                 sync(presentations.create(mVContext, mPermissions));
             }
+            Table ui = mDB.getTable(UI_TABLE);
+            if (!sync(ui.exists(mVContext))) {
+                sync(ui.create(mVContext, mPermissions));
+            }
             //importDecks();
         } catch (VException e) {
             throw new InitException("Couldn't setup syncbase service", e);
@@ -160,6 +169,21 @@
     }
 
     @Override
+    public Session getSession(String sessionId) throws VException {
+        Table ui = mDB.getTable(UI_TABLE);
+        CancelableVContext context = mVContext.withTimeout(Duration.millis(5000));
+        VSession vSession = (VSession) sync(ui.get(context, sessionId, VSession.class));
+        return new SyncbaseSession(vSession);
+    }
+
+    @Override
+    public Presentation getPresentation(Session session) {
+        // TODO(kash): Cache this presentation so that it survives the phone
+        // rotating.
+        return new SyncbasePresentation(mVContext, mDB, session);
+    }
+
+    @Override
     public DynamicList<Deck> getDecks() {
         if (!mInitialized) {
             return new NoopList<>();
diff --git a/android/app/src/main/java/io/v/syncslides/db/SyncbasePresentation.java b/android/app/src/main/java/io/v/syncslides/db/SyncbasePresentation.java
new file mode 100644
index 0000000..c1363fd
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/SyncbasePresentation.java
@@ -0,0 +1,31 @@
+// 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 io.v.syncslides.model.DynamicList;
+import io.v.syncslides.model.Session;
+import io.v.syncslides.model.Slide;
+import io.v.v23.context.VContext;
+import io.v.v23.syncbase.nosql.Database;
+
+class SyncbasePresentation implements Presentation {
+    private final VContext mVContext;
+    private final Database mDb;
+    private final Session mSession;
+
+    public SyncbasePresentation(VContext vContext, Database db, Session session) {
+        mVContext = vContext;
+        mDb = db;
+        mSession = session;
+    }
+
+    @Override
+    public DynamicList<Slide> getSlides() {
+        // TODO(kash): Cache this list so it survives a phone rotation.
+        // We'll need a corresponding method to clear the cache when this
+        // Presentation is no longer needed.
+        return new WatchedList<>(mVContext, new SlideWatcher(mDb, mSession.getDeckId()));
+    }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/db/SyncbaseSession.java b/android/app/src/main/java/io/v/syncslides/db/SyncbaseSession.java
new file mode 100644
index 0000000..2e9bd43
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/SyncbaseSession.java
@@ -0,0 +1,28 @@
+// 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 io.v.syncslides.model.Session;
+
+/**
+ * SyncbaseSession gets its session state from Syncbase.
+ */
+class SyncbaseSession implements Session {
+    private final VSession mVSession;
+
+    SyncbaseSession(VSession vSession) {
+        mVSession = vSession;
+    }
+
+    @Override
+    public String getDeckId() {
+        return mVSession.getDeckId();
+    }
+
+    @Override
+    public String getPresentationId() {
+        return mVSession.getPresentationId();
+    }
+}
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
index 0e3c8d7..7765cac 100644
--- a/android/app/src/main/java/io/v/syncslides/db/Watcher.java
+++ b/android/app/src/main/java/io/v/syncslides/db/Watcher.java
@@ -13,7 +13,7 @@
  * 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> {
+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}.
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
index 5590bd4..77bcfef 100644
--- a/android/app/src/main/java/io/v/syncslides/db/schema.vdl
+++ b/android/app/src/main/java/io/v/syncslides/db/schema.vdl
@@ -80,4 +80,4 @@
     // This field allows us to find the most recently used session and prompt
     // the user to resume it.
     LastTouched int64
-}
\ No newline at end of file
+}
diff --git a/android/app/src/main/java/io/v/syncslides/lib/DeckImporter.java b/android/app/src/main/java/io/v/syncslides/lib/DeckImporter.java
index 8ea9e59..3b9c0fa 100644
--- a/android/app/src/main/java/io/v/syncslides/lib/DeckImporter.java
+++ b/android/app/src/main/java/io/v/syncslides/lib/DeckImporter.java
@@ -142,7 +142,9 @@
                 imageData = readImage(dir, slide.getString(IMAGE));
             }
             String note = slide.getString(NOTE);
-            ret[i] = new SlideImpl(thumbData, imageData, note);
+            // Temporarily use the thumbnail image's filename as the slide's unique ID.  SyncbaseDB
+            // will generate a real ID on its own, so this is just a placeholder.
+            ret[i] = new SlideImpl(slide.getString(THUMB), thumbData, imageData, note);
         }
         return ret;
     }
diff --git a/android/app/src/main/java/io/v/syncslides/model/Session.java b/android/app/src/main/java/io/v/syncslides/model/Session.java
new file mode 100644
index 0000000..7381897
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/model/Session.java
@@ -0,0 +1,13 @@
+// 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;
+
+/**
+ * A Session represents the UI state for one presentation.
+ */
+public interface Session {
+    String getDeckId();
+    String getPresentationId();
+}
diff --git a/android/app/src/main/java/io/v/syncslides/model/Slide.java b/android/app/src/main/java/io/v/syncslides/model/Slide.java
index 7f6da9d..571f450 100644
--- a/android/app/src/main/java/io/v/syncslides/model/Slide.java
+++ b/android/app/src/main/java/io/v/syncslides/model/Slide.java
@@ -11,6 +11,11 @@
  */
 public interface Slide {
     /**
+     * Returns the unique id for this slide.
+     */
+    String getId();
+
+    /**
      * Returns a Bitmap of the slide thumbnail.
      */
     Bitmap getThumb();
diff --git a/android/app/src/main/java/io/v/syncslides/model/SlideImpl.java b/android/app/src/main/java/io/v/syncslides/model/SlideImpl.java
index af8d338..42d6eb4 100644
--- a/android/app/src/main/java/io/v/syncslides/model/SlideImpl.java
+++ b/android/app/src/main/java/io/v/syncslides/model/SlideImpl.java
@@ -12,17 +12,23 @@
  * a getter is called, to conserve memory.
  */
 public class SlideImpl implements Slide {
+    private String mId;
     private final byte[] mThumbnail;
     private final byte[] mImage;
     private String mNotes;
 
-    public SlideImpl(byte[] thumbnail, byte[] image, String notes) {
+    public SlideImpl(String id, byte[] thumbnail, byte[] image, String notes) {
+        mId = id;
         mThumbnail = thumbnail;
         mImage = image;
         mNotes = notes;
     }
 
     @Override
+    public String getId() {
+        return mId;
+    }
+    @Override
     public Bitmap getThumb() {
         return BitmapFactory.decodeByteArray(mThumbnail, 0 /* offset */, mThumbnail.length);
     }