syncslides: Basic navigation functionality.

- Copy/paste the XML from the previous version.
- NavigateFragment is heavily based on the previous version,
  but it still should be reviewed fully.  There are many
  parts that are commented out because I wasn't ready to
  implement the corresponding syncbase interactions.
- Removed the Presentation interface.  I'll just put it all
  in Session.
    mSession.getPresentation().foo()
      vs
    mSession.foo()
- Fixed a bug in WatchedList where the second and subsequent
  listeners would not get added to the list.

Change-Id: I20bdda9dba465462941df435b0948cbfe47747ec
diff --git a/android/app/src/main/java/io/v/syncslides/NavigateFragment.java b/android/app/src/main/java/io/v/syncslides/NavigateFragment.java
new file mode 100644
index 0000000..7a2cb34
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/NavigateFragment.java
@@ -0,0 +1,424 @@
+// 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.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import io.v.syncslides.db.DB;
+import io.v.syncslides.model.DynamicList;
+import io.v.syncslides.model.ListListener;
+import io.v.syncslides.model.Session;
+import io.v.syncslides.model.Slide;
+import io.v.v23.verror.VException;
+
+/**
+ * Provides both the presenter and audience views for navigating through a presentation.
+ * Instantiated by the PresentationActivity along with other views/fragments of the presentation
+ * to make transitions between them seamless.
+ */
+public class NavigateFragment extends Fragment {
+    private static final String TAG = "NavigateFragment";
+    private static final String SESSION_ID_KEY = "session_id_key";
+
+    private Session mSession;
+    private int mSlideNum = 0;
+    private SlideNumberListener mSlideNumberListener = new SlideNumberListener();
+    private ListListener mSlideListListener = new SlideListListener();
+    private ImageView mPrevThumb;
+    private ImageView mNextThumb;
+    private ImageView mCurrentSlide;
+    private TextView mSlideNumText;
+    private boolean mEditing;
+    private DynamicList<Slide> mSlides;
+
+    public static NavigateFragment newInstance(String sessionId) {
+        NavigateFragment fragment = new NavigateFragment();
+        Bundle args = new Bundle();
+        args.putString(SESSION_ID_KEY, sessionId);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        // When editing notes, display a menu with "Save".
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        Bundle bundle = savedInstanceState;
+        if (bundle == null) {
+            bundle = getArguments();
+        }
+        String sessionId = bundle.getString(SESSION_ID_KEY);
+        try {
+            mSession = DB.Singleton.get().getSession(sessionId);
+        } catch (VException e) {
+            handleFatalError("Failed to fetch Session", e);
+        }
+
+        final View rootView = inflater.inflate(R.layout.fragment_navigate, container, false);
+
+//        mFabSync = rootView.findViewById(R.id.audience_sync_fab);
+//        if (((PresentationActivity) getActivity()).getSynced() || mRole != Role.AUDIENCE) {
+//            mFabSync.setVisibility(View.INVISIBLE);
+//        } else {
+//            mFabSync.setVisibility(View.VISIBLE);
+//        }
+//
+//        mFabSync.setOnClickListener(new NavigateClickListener() {
+//            @Override
+//            public void onClick(View v) {
+//                super.onClick(v);
+//                sync();
+//                mFabSync.setVisibility(View.INVISIBLE);
+//            }
+//        });
+        View.OnClickListener previousSlideListener = new NavigateClickListener() {
+            @Override
+            void onNavigate() {
+                previousSlide();
+            }
+        };
+        View arrowBack = rootView.findViewById(R.id.arrow_back);
+        arrowBack.setOnClickListener(previousSlideListener);
+        mPrevThumb = (ImageView) rootView.findViewById(R.id.prev_thumb);
+        mPrevThumb.setOnClickListener(previousSlideListener);
+
+        View.OnClickListener nextSlideListener = new NavigateClickListener() {
+            @Override
+            void onNavigate() {
+                nextSlide();
+            }
+        };
+        // Show either the arrowForward or the FAB but not both.
+        View arrowForward = rootView.findViewById(R.id.arrow_forward);
+        View fabForward = rootView.findViewById(R.id.primary_navigation_fab);
+//        if (mRole == Role.PRESENTER) {
+//            arrowForward.setVisibility(View.INVISIBLE);
+//            fabForward.setOnClickListener(nextSlideListener);
+//        } else {
+        fabForward.setVisibility(View.INVISIBLE);
+        arrowForward.setOnClickListener(nextSlideListener);
+//        }
+        mNextThumb = (ImageView) rootView.findViewById(R.id.next_thumb);
+        mNextThumb.setOnClickListener(nextSlideListener);
+//        mQuestions = (ImageView) rootView.findViewById(R.id.questions);
+//        // TODO(kash): Hide the mQuestions button if mRole == BROWSER.
+//        mQuestions.setOnClickListener(new NavigateClickListener() {
+//            @Override
+//            public void onClick(View v) {
+//                super.onClick(v);
+//                questionButton();
+//            }
+//        });
+        mCurrentSlide = (ImageView) rootView.findViewById(R.id.slide_current_medium);
+//        mCurrentSlide.setOnClickListener(new NavigateClickListener() {
+//            @Override
+//            public void onClick(View v) {
+//                super.onClick(v);
+//                if (mRole == Role.AUDIENCE || mRole == Role.BROWSER) {
+//                    ((PresentationActivity) getActivity()).showFullscreenSlide(mSlideNum);
+//                }
+//            }
+//        });
+//
+        mSlideNumText = (TextView) rootView.findViewById(R.id.slide_num_text);
+//        mNotes = (EditText) rootView.findViewById(R.id.notes);
+//        mNotes.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+//            @Override
+//            public void onFocusChange(View v, boolean hasFocus) {
+//                ((PresentationActivity) getActivity()).getSupportActionBar().show();
+//                mEditing = hasFocus;
+//                getActivity().invalidateOptionsMenu();
+//                unsync();
+//            }
+//        });
+//
+//        // The parent of mNotes needs to be focusable in order to clear focus
+//        // from mNotes when done editing.  We set the attributes in code rather
+//        // than in XML because it is too easy to add an extra level of layout
+//        // in XML and forget to add these attributes.
+//        ViewGroup parent = (ViewGroup) mNotes.getParent();
+//        parent.setFocusable(true);
+//        parent.setClickable(true);
+//        parent.setFocusableInTouchMode(true);
+//
+//        View slideListIcon = rootView.findViewById(R.id.slide_list);
+//        slideListIcon.setOnClickListener(new NavigateClickListener() {
+//            @Override
+//            public void onClick(View v) {
+//                super.onClick(v);
+//                if (mRole == Role.AUDIENCE) {
+//                    ((PresentationActivity) getActivity()).showSlideList();
+//                } else {
+//                    getActivity().getSupportFragmentManager().popBackStack();
+//                }
+//            }
+//        });
+//        mQuestionsNum = (TextView) rootView.findViewById(R.id.questions_num);
+//        // Start off invisible for everyone.  If there are questions, this
+//        // will be set to visible in the mDB.getQuestionerList() callback.
+//        mQuestionsNum.setVisibility(View.INVISIBLE);
+//
+//        mDB = DB.Singleton.get(getActivity().getApplicationContext());
+//        mDB.getSlides(mDeckId, new DB.Callback<List<Slide>>() {
+//            @Override
+//            public void done(List<Slide> slides) {
+//                mSlides = slides;
+//                // The CurrentSlideListener could have been notified while we were waiting for
+//                // the slides to load.
+//                if (mLoadingCurrentSlide != -1) {
+//                    currentSlideChanged(mLoadingCurrentSlide);
+//                }
+//                updateView();
+//            }
+//        });
+//        if (((PresentationActivity) getActivity()).getSynced()) {
+//            sync();
+//        } else {
+//            unsync();
+//        }
+
+        return rootView;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        ((PresentationActivity) getActivity()).setUiImmersive(true);
+        mSlides = mSession.getSlides();
+        mSlides.addListener(mSlideListListener);
+        mSession.addSlideNumberListener(mSlideNumberListener);
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        ((PresentationActivity) getActivity()).setUiImmersive(false);
+        mSession.removeSlideNumberListener(mSlideNumberListener);
+        mSlides.removeListener(mSlideListListener);
+        mSlides = null;
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putString(SESSION_ID_KEY, mSession.getId());
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        if (mEditing) {
+            inflater.inflate(R.menu.edit_notes, menu);
+        }
+        super.onCreateOptionsMenu(menu, inflater);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.action_save:
+                saveNotes();
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * Advances to the next slide, if there is one, and updates the UI.
+     */
+    private void nextSlide() {
+        if (mSlideNum == -1) {
+            // Wait until the state has loaded before letting the user move around.
+            return;
+        }
+        if (mSlideNum < mSession.getSlides().getItemCount() - 1) {
+            try {
+                mSession.setLocalSlideNum(mSlideNum + 1);
+            } catch (VException e) {
+                handleError("Failed to advance", e);
+            }
+        }
+    }
+
+    /**
+     * Goes back to the previous slide, if there is one, and updates the UI.
+     */
+    private void previousSlide() {
+        if (mSlideNum == -1) {
+            // Wait until the state has loaded before letting the user move around.
+            return;
+        }
+        if (mSlideNum > 0) {
+            try {
+                mSession.setLocalSlideNum(mSlideNum - 1);
+            } catch (VException e) {
+                handleError("Failed to go back", e);
+            }
+        }
+    }
+
+    private void updateView() {
+        if (mSlideNum < 0 || mSlideNum >= mSlides.getItemCount()) {
+            // Still loading.
+            return;
+        }
+        if (mSlideNum > 0) {
+            setThumbBitmap(mPrevThumb, mSlides.get(mSlideNum - 1).getThumb());
+        } else {
+            setThumbNull(mPrevThumb);
+        }
+        // TODO(kash): Switch to full size image.
+        mCurrentSlide.setImageBitmap(mSlides.get(mSlideNum).getThumb());
+        if (mSlideNum == mSlides.getItemCount() - 1) {
+            setThumbNull(mNextThumb);
+        } else {
+            setThumbBitmap(mNextThumb, mSlides.get(mSlideNum + 1).getThumb());
+        }
+        // TODO(kash): Implement me.
+//        if (!mSlides.get(mSlideNum).getNotes().equals("")) {
+//            mNotes.setText(mSlides.get(mSlideNum).getNotes());
+//        } else {
+//            mNotes.getText().clear();
+//        }
+        mSlideNumText.setText(
+                String.valueOf(mSlideNum + 1) + " of " + String.valueOf(mSlides.getItemCount()));
+    }
+
+    private void setThumbBitmap(ImageView thumb, Bitmap bitmap) {
+        thumb.setImageBitmap(bitmap);
+        // In landscape, the height is dependent on the image size.  However, if the
+        // image was null, the height is hardcoded to 9/16 of the width in setThumbNull.
+        // This resets it to the actual image size.
+        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
+            ViewGroup.LayoutParams thumbParams = thumb.getLayoutParams();
+            thumbParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+        }
+    }
+
+    private void setThumbNull(ImageView thumb) {
+        thumb.setImageDrawable(null);
+        // In landscape, the height is dependent on the image size.  Because we don't have an
+        // image, assume all of the images are 16:9.  The width is fixed, so we can calculate
+        // the expected height.
+        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
+            ViewGroup grandparent = (ViewGroup) thumb.getParent().getParent();
+            ViewGroup.LayoutParams thumbParams = thumb.getLayoutParams();
+            thumbParams.height = (int) ((9 / 16.0) * grandparent.getMeasuredWidth());
+        }
+    }
+
+    /**
+     * If the user is editing the text field and the text has changed, save the
+     * notes locally and in Syncbase.
+     */
+    public void saveNotes() {
+        // TODO(kash): Port this code.
+//        final String notes = mNotes.getText().toString();
+//        if (mEditing && (!notes.equals(mSlides.get(mUserSlideNum).getNotes()))) {
+//            toast("Saving notes");
+//            mSlides.get(mUserSlideNum).setNotes(notes);
+//            mDB.setSlideNotes(mDeckId, mUserSlideNum, notes);
+//        }
+//        mNotes.clearFocus();
+//        InputMethodManager inputManager =
+//                (InputMethodManager) getContext().
+//                        getSystemService(Context.INPUT_METHOD_SERVICE);
+//        if (getActivity().getCurrentFocus() != null) {
+//            inputManager.hideSoftInputFromWindow(
+//                    getActivity().getCurrentFocus().getWindowToken(),
+//                    InputMethodManager.HIDE_NOT_ALWAYS);
+//        }
+//        ((PresentationActivity) getActivity()).setUiImmersive(true);
+    }
+
+    private void handleError(String msg, Throwable throwable) {
+        Log.e(TAG, msg + ": " + Log.getStackTraceString(throwable));
+        Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
+    }
+
+    private void handleFatalError(String msg, Throwable throwable) {
+        handleError(msg, throwable);
+        getActivity().finish();
+    }
+
+    /**
+     * Updates the view whenever the list of slides changes.
+     */
+    private class SlideListListener implements ListListener {
+        @Override
+        public void notifyDataSetChanged() {
+            updateView();
+        }
+
+        @Override
+        public void notifyItemChanged(int position) {
+            updateView();
+        }
+
+        @Override
+        public void notifyItemInserted(int position) {
+            updateView();
+        }
+
+        @Override
+        public void notifyItemRemoved(int position) {
+            updateView();
+        }
+
+        @Override
+        public void onError(Exception e) {
+            handleFatalError("Error watching slide list", e);
+        }
+    }
+
+    private class SlideNumberListener implements Session.SlideNumberListener {
+        @Override
+        public void onChange(int slideNum) {
+            mSlideNum = slideNum;
+            updateView();
+        }
+
+        @Override
+        public void onError(Exception e) {
+            handleFatalError("Error listening to slide number changes", e);
+        }
+    }
+
+    /**
+     * If the user is editing notes and then clicks anywhere else on the screen,
+     * we want that action to save the notes first.  Using this class forces
+     * that behavior.
+     */
+    private abstract class NavigateClickListener implements View.OnClickListener {
+        @Override
+        public final void onClick(View v) {
+            saveNotes();
+            onNavigate();
+        }
+
+        /**
+         * Called when there is a click.
+         */
+        abstract void onNavigate();
+    }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/PresentationActivity.java b/android/app/src/main/java/io/v/syncslides/PresentationActivity.java
index b8bc34d..c56fe3e 100644
--- a/android/app/src/main/java/io/v/syncslides/PresentationActivity.java
+++ b/android/app/src/main/java/io/v/syncslides/PresentationActivity.java
@@ -6,6 +6,7 @@
 
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.v4.app.FragmentTransaction;
 import android.support.v7.app.AppCompatActivity;
 import android.util.Log;
 import android.view.View;
@@ -24,7 +25,6 @@
 
     public static final String SESSION_ID_KEY = "session_id_key";
 
-    private String mSessionId;
     private Session mSession;
 
     @Override
@@ -41,22 +41,24 @@
         }
         setContentView(R.layout.activity_presentation);
 
+        String sessionId;
         if (savedInstanceState == null) {
-            mSessionId = getIntent().getStringExtra(SESSION_ID_KEY);
+            sessionId = getIntent().getStringExtra(SESSION_ID_KEY);
         } else {
-            mSessionId = savedInstanceState.getString(SESSION_ID_KEY);
-        }
-        if (savedInstanceState != null) {
-            // Let the framework take care of inflating the right fragment.
-            return;
+            sessionId = savedInstanceState.getString(SESSION_ID_KEY);
         }
         DB db = DB.Singleton.get();
         try {
-            mSession = db.getSession(mSessionId);
+            mSession = db.getSession(sessionId);
         } catch (VException e) {
             handleError("Failed to load state", e);
             finish();
         }
+
+        if (savedInstanceState != null) {
+            // Let the framework take care of inflating the right fragment.
+            return;
+        }
         showSlideList();
     }
 
@@ -79,7 +81,7 @@
     @Override
     protected void onSaveInstanceState(Bundle b) {
         super.onSaveInstanceState(b);
-        b.putString(SESSION_ID_KEY, mSessionId);
+        b.putString(SESSION_ID_KEY, mSession.getId());
     }
 
     /**
@@ -111,12 +113,34 @@
      * presenting.
      */
     public void showSlideList() {
-        SlideListFragment fragment = SlideListFragment.newInstance(mSessionId);
+        SlideListFragment fragment = SlideListFragment.newInstance(mSession.getId());
         getSupportFragmentManager().beginTransaction().replace(R.id.fragment, fragment).commit();
     }
 
+    /**
+     * Shows the navigate fragment where the user can see the given slide and
+     * navigate to other components of the slide presentation.
+     */
+    public void navigateToSlide(int slideNum) {
+        try {
+            mSession.setLocalSlideNum(slideNum);
+        } catch (VException e) {
+            handleError("Could not update session", e);
+            return;
+        }
+        NavigateFragment fragment = NavigateFragment.newInstance(mSession.getId());
+        FragmentTransaction transaction = getSupportFragmentManager()
+                .beginTransaction()
+                .replace(R.id.fragment, fragment);
+        // TODO(kash): Figure out if we need to do this.
+//        if (mRole != Role.AUDIENCE) {
+//            transaction.addToBackStack("");
+//        }
+        transaction.commit();
+    }
+
     private void handleError(String msg, Throwable throwable) {
         Log.e(TAG, msg + ": " + Log.getStackTraceString(throwable));
         Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
     }
-}
\ No newline at end of file
+}
diff --git a/android/app/src/main/java/io/v/syncslides/SlideListAdapter.java b/android/app/src/main/java/io/v/syncslides/SlideListAdapter.java
index a0e01ca..2019fa6 100644
--- a/android/app/src/main/java/io/v/syncslides/SlideListAdapter.java
+++ b/android/app/src/main/java/io/v/syncslides/SlideListAdapter.java
@@ -26,9 +26,9 @@
     private final RecyclerView mRecyclerView;
     private DynamicList<Slide> mSlides;
 
-    public SlideListAdapter(RecyclerView recyclerView, DB db, Session session) {
+    public SlideListAdapter(RecyclerView recyclerView, DB db, DynamicList<Slide> slides) {
         mRecyclerView = recyclerView;
-        mSlides = db.getPresentation(session).getSlides();
+        mSlides = slides;
         mSlides.addListener(this);
     }
 
@@ -40,8 +40,7 @@
             @Override
             public void onClick(View v) {
                 int position = mRecyclerView.getChildAdapterPosition(v);
-                // TODO(kash): Implement me.
-                //((PresentationActivity) v.getContext()).navigateToSlide(position);
+                ((PresentationActivity) v.getContext()).navigateToSlide(position);
             }
         });
         return new ViewHolder(v);
diff --git a/android/app/src/main/java/io/v/syncslides/SlideListFragment.java b/android/app/src/main/java/io/v/syncslides/SlideListFragment.java
index 183df0d..577ec8b 100644
--- a/android/app/src/main/java/io/v/syncslides/SlideListFragment.java
+++ b/android/app/src/main/java/io/v/syncslides/SlideListFragment.java
@@ -95,7 +95,7 @@
     public void onStart() {
         super.onStart();
         DB db = DB.Singleton.get();
-        mAdapter = new SlideListAdapter(mRecyclerView, db, mSession);
+        mAdapter = new SlideListAdapter(mRecyclerView, db, mSession.getSlides());
         mRecyclerView.setAdapter(mAdapter);
     }
 
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 2378d7f..f437455 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
@@ -60,11 +60,6 @@
     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/Presentation.java b/android/app/src/main/java/io/v/syncslides/db/Presentation.java
deleted file mode 100644
index c0933fa..0000000
--- a/android/app/src/main/java/io/v/syncslides/db/Presentation.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package io.v.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/SlideNumberWatcher.java b/android/app/src/main/java/io/v/syncslides/db/SlideNumberWatcher.java
new file mode 100644
index 0000000..4440179
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/SlideNumberWatcher.java
@@ -0,0 +1,223 @@
+// 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 android.util.Log;
+
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import io.v.impl.google.naming.NamingUtil;
+import io.v.syncslides.model.Session;
+import io.v.v23.VIterable;
+import io.v.v23.context.CancelableVContext;
+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.Table;
+import io.v.v23.syncbase.nosql.WatchChange;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+
+import static io.v.v23.VFutures.sync;
+
+/**
+ * Watches both the local slide number as well as the live presentation's slide number.
+ * If the local number is INVALID_LOCAL_SLIDE_NUM, notifies listeners whenever the live
+ * presentation's slide number changes.  Otherwise, it notifies listeners whenever the
+ * local number changes.
+ */
+class SlideNumberWatcher {
+    private static final String TAG = "SlideNumberWatcher";
+
+    private final VContext mBaseContext;
+    private final Database mDb;
+    private final Set<Session.SlideNumberListener> mListeners;
+    private final ExecutorService mExecutor;
+    private final Handler mHandler;
+    private final String mSessionId;
+    private final String mDeckId;
+    private final String mPresentationId;
+    private int mLocalSlideNum;
+    private VCurrentSlide mCurrentSlide;
+    private CancelableVContext mCurrentContext;
+
+    /**
+     * If presentationId is non-null, SlideNumberWatcher will watch for changes in addition
+     * to watching the session's local slide number.
+     */
+    SlideNumberWatcher(VContext context, Database db, String sessionId, String deckId,
+                       String presentationId) {
+        mBaseContext = context;
+        mDb = db;
+        mSessionId = sessionId;
+        mDeckId = deckId;
+        mPresentationId = presentationId;
+        mListeners = Sets.newHashSet();
+        mLocalSlideNum = SyncbaseSession.INVALID_LOCAL_SLIDE_NUM;
+        mHandler = new Handler(Looper.getMainLooper());
+        mExecutor = Executors.newFixedThreadPool(2);
+    }
+
+    void addListener(Session.SlideNumberListener listener) {
+        mListeners.add(listener);
+        if (mListeners.size() == 1) {
+            // First listener.  Start the threads.
+            mCurrentContext = mBaseContext.withCancel();
+            mExecutor.submit(new Runnable() {
+                @Override
+                public void run() {
+                    watchLocalSlideNum();
+                }
+            });
+            mExecutor.submit(new Runnable() {
+                @Override
+                public void run() {
+                    watchCurrentSlide();
+                }
+            });
+        }
+        listener.onChange(getSlideNum());
+    }
+
+    void removeListener(Session.SlideNumberListener listener) {
+        mListeners.remove(listener);
+        if (mListeners.isEmpty()) {
+            // Stop watchers via cancel.
+            mCurrentContext.cancel();
+            mCurrentContext = null;
+            mHandler.removeCallbacksAndMessages(null);
+        }
+    }
+
+    private void currentSlideChanged(VCurrentSlide slide) {
+        mCurrentSlide = slide;
+        notifyListeners();
+    }
+
+    private void localSlideChanged(int localSlide) {
+        mLocalSlideNum = localSlide;
+        notifyListeners();
+    }
+
+    private void notifyListeners() {
+        int slideNum = getSlideNum();
+        for (Session.SlideNumberListener listener : mListeners) {
+            listener.onChange(slideNum);
+        }
+    }
+
+    private int getSlideNum() {
+        int slideNum = mLocalSlideNum;
+        if (slideNum == SyncbaseSession.INVALID_LOCAL_SLIDE_NUM && mCurrentSlide != null) {
+            slideNum = mCurrentSlide.getSlideNum();
+        }
+        return slideNum;
+    }
+
+    private void notifyError(Exception e) {
+        for (Session.SlideNumberListener listener : mListeners) {
+            listener.onError(e);
+        }
+    }
+
+    // Runs in a background thread.
+    private void watchCurrentSlide() {
+        try {
+            Log.i(TAG, "watchCurrentSlide");
+            String rowKey = NamingUtil.join(mDeckId, mPresentationId, SyncbaseDB.CURRENT_SLIDE);
+            BatchDatabase batch = sync(mDb.beginBatch(mCurrentContext, null));
+            Table presentations = batch.getTable(SyncbaseDB.PRESENTATIONS_TABLE);
+            if (sync(presentations.getRow(rowKey).exists(mCurrentContext))) {
+                final VCurrentSlide slide = (VCurrentSlide) presentations.get(
+                        mCurrentContext, rowKey, VCurrentSlide.class);
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        currentSlideChanged(slide);
+                    }
+                });
+            }
+            ResumeMarker marker = sync(batch.getResumeMarker(mCurrentContext));
+            VIterable<WatchChange> changes =
+                    sync(mDb.watch(mCurrentContext, SyncbaseDB.PRESENTATIONS_TABLE,
+                            rowKey, marker));
+            for (WatchChange change : changes) {
+                String key = change.getRowName();
+                Log.i(TAG, "Found CurrentSlide change " + key);
+                if (!key.equals(rowKey)) {
+                    continue;
+                }
+                if (change.getChangeType().equals(ChangeType.PUT_CHANGE)) {
+                    final VCurrentSlide slide = (VCurrentSlide) VomUtil.decode(
+                            change.getVomValue(), VCurrentSlide.class);
+                    mHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            currentSlideChanged(slide);
+                        }
+                    });
+                }
+            }
+        } catch (final VException e) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    notifyError(e);
+                }
+            });
+        }
+    }
+
+    // Runs in a background thread.
+    private void watchLocalSlideNum() {
+        try {
+            BatchDatabase batch = sync(mDb.beginBatch(mCurrentContext, null));
+            Table ui = batch.getTable(SyncbaseDB.UI_TABLE);
+            final VSession vSession = (VSession) sync(ui.get(
+                    mCurrentContext, mSessionId, VSession.class));
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    localSlideChanged(vSession.getLocalSlide());
+                }
+            });
+            ResumeMarker marker = sync(batch.getResumeMarker(mCurrentContext));
+            VIterable<WatchChange> changes =
+                    sync(mDb.watch(mCurrentContext, SyncbaseDB.UI_TABLE, mSessionId, marker));
+            for (WatchChange change : changes) {
+                String key = change.getRowName();
+                Log.i(TAG, "Found local slide change " + key);
+                if (!key.equals(mSessionId)) {
+                    continue;
+                }
+                if (change.getChangeType().equals(ChangeType.PUT_CHANGE)) {
+                    final VSession vSession1 = (VSession) VomUtil.decode(
+                            change.getVomValue(), VSession.class);
+                    mHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            localSlideChanged(vSession1.getLocalSlide());
+                        }
+                    });
+                }
+            }
+        } catch (final VException e) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    notifyError(e);
+                }
+            });
+        }
+    }
+}
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 e7a11d8..99306fb 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
@@ -171,11 +171,9 @@
 
     @Override
     public String createSession(String deckId) throws VException {
-        Table ui = mDB.getTable(UI_TABLE);
-        CancelableVContext context = mVContext.withTimeout(Duration.millis(5000));
-        VSession vSession = new VSession(deckId, null, -1, System.currentTimeMillis());
         String uuid = UUID.randomUUID().toString();
-        sync(ui.put(context, uuid, vSession, VSession.class));
+        SyncbaseSession session = new SyncbaseSession(mVContext, mDB, uuid, deckId);
+        session.save();
         return uuid;
     }
 
@@ -184,14 +182,7 @@
         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(sessionId, 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);
+        return new SyncbaseSession(mVContext, mDB, sessionId, vSession);
     }
 
     @Override
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
deleted file mode 100644
index c1363fd..0000000
--- a/android/app/src/main/java/io/v/syncslides/db/SyncbasePresentation.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package io.v.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
index c70a6af..2ac8865 100644
--- a/android/app/src/main/java/io/v/syncslides/db/SyncbaseSession.java
+++ b/android/app/src/main/java/io/v/syncslides/db/SyncbaseSession.java
@@ -4,18 +4,49 @@
 
 package io.v.syncslides.db;
 
+import org.joda.time.Duration;
+
+import io.v.syncslides.model.DynamicList;
 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.syncbase.nosql.Database;
+import io.v.v23.syncbase.nosql.Table;
+import io.v.v23.verror.VException;
+
+import static io.v.v23.VFutures.sync;
 
 /**
  * SyncbaseSession gets its session state from Syncbase.
  */
 class SyncbaseSession implements Session {
-    private String mId;
-    private final VSession mVSession;
+    /**
+     * The user wants to follow a live presentation or hasn't yet chosen a slide to view.
+     */
+    static final int INVALID_LOCAL_SLIDE_NUM = -1;
+    private static final long UNINITIALIZED_TIME = 0;
 
-    SyncbaseSession(String id, VSession vSession) {
+    private final VContext mVContext;
+    private final Database mDb;
+    private final String mId;
+    private final VSession mVSession;
+    private final SlideNumberWatcher mSlideNumberWatcher;
+    private final DynamicList<Slide> mSlides;
+
+    SyncbaseSession(VContext vContext, Database db, String id, String deckId) {
+        this(vContext, db, id,
+                new VSession(deckId, null, INVALID_LOCAL_SLIDE_NUM, UNINITIALIZED_TIME));
+    }
+
+    SyncbaseSession(VContext vContext, Database db, String id, VSession vSession) {
+        mVContext = vContext;
+        mDb = db;
         mId = id;
         mVSession = vSession;
+        mSlideNumberWatcher = new SlideNumberWatcher(mVContext, mDb, id, mVSession.getDeckId(),
+                mVSession.getPresentationId());
+        mSlides = new WatchedList<>(mVContext, new SlideWatcher(mDb, mVSession.getDeckId()));
     }
 
     @Override
@@ -32,4 +63,38 @@
     public String getPresentationId() {
         return mVSession.getPresentationId();
     }
+
+    @Override
+    public void setLocalSlideNum(int slideNum) throws VException {
+        // TODO(kash): if the user is driving, this should update the presentation state instead.
+        mVSession.setLocalSlide(slideNum);
+        save();
+    }
+
+    @Override
+    public void addSlideNumberListener(SlideNumberListener listener) {
+        mSlideNumberWatcher.addListener(listener);
+    }
+
+    @Override
+    public void removeSlideNumberListener(SlideNumberListener listener) {
+        mSlideNumberWatcher.removeListener(listener);
+    }
+
+    @Override
+    public DynamicList<Slide> getSlides() {
+        return mSlides;
+    }
+
+    /**
+     * Persists the VSession to the UI_TABLE.
+     *
+     * @throws VException when the Syncbase put() fails
+     */
+    void save() throws VException {
+        mVSession.setLastTouched(System.currentTimeMillis());
+        Table ui = mDb.getTable(SyncbaseDB.UI_TABLE);
+        CancelableVContext context = mVContext.withTimeout(Duration.millis(5000));
+        sync(ui.put(context, mId, mVSession, VSession.class));
+    }
 }
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
index cdb367b..4ab9cd1 100644
--- a/android/app/src/main/java/io/v/syncslides/db/WatchedList.java
+++ b/android/app/src/main/java/io/v/syncslides/db/WatchedList.java
@@ -59,8 +59,9 @@
 
     @Override
     public void addListener(final ListListener listener) {
-        if (mListeners.isEmpty()) {
-            mListeners.add(listener);
+        mListeners.add(listener);
+        if (mListeners.size() == 1) {
+            // First listener.  Start the thread.
             mCurrentContext = mBaseContext.withCancel();
             mExecutor.submit(new Runnable() {
                 @Override
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
index a674964..0c6a877 100644
--- a/android/app/src/main/java/io/v/syncslides/model/Session.java
+++ b/android/app/src/main/java/io/v/syncslides/model/Session.java
@@ -4,11 +4,58 @@
 
 package io.v.syncslides.model;
 
+import io.v.v23.verror.VException;
+
 /**
  * A Session represents the UI state for one presentation.
  */
 public interface Session {
     String getId();
+
     String getDeckId();
+
     String getPresentationId();
+
+    /**
+     * Store the user's desire to view the given slide.  This will trigger a notification to
+     * any SlideNumberListeners.
+     *
+     * @param slideNum the number of the slide
+     */
+    void setLocalSlideNum(int slideNum) throws VException;
+
+    interface SlideNumberListener {
+        /**
+         * Called whenever the UI should display a different slide.  If the user is following
+         * a live presentation, this will be called when the driver of the presentation changes
+         * the current slide.  If the user is browsing a presentation on his own, this will
+         * be triggered by calls to setLocalSlideNum().
+         */
+        void onChange(int slideNum);
+        /**
+         * Called whenever there is an error.  The listener should unregister and re-register
+         * itself if it wants to continue.
+         */
+        void onError(Exception e);
+    }
+
+    /**
+     * Adds a listener for changes to the to-be-displayed slide.
+     *
+     * @param listener notified of changes
+     */
+    void addSlideNumberListener(SlideNumberListener listener);
+
+    /**
+     * Removes a listener that was previously passed to addSlideNumberListener().
+     *
+     * @param listener previously passed to addSlideNumberListener().
+     */
+    void removeSlideNumberListener(SlideNumberListener listener);
+
+    /**
+     * Returns a dynamically updating list of slides in the deck.
+     */
+    DynamicList<Slide> getSlides();
+
 }
diff --git a/android/app/src/main/res/layout-land/fragment_navigate.xml b/android/app/src/main/res/layout-land/fragment_navigate.xml
new file mode 100644
index 0000000..41119a1
--- /dev/null
+++ b/android/app/src/main/res/layout-land/fragment_navigate.xml
@@ -0,0 +1,216 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/navigate_fragment"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal">
+
+    <LinearLayout
+        android:id="@+id/notes_and_thumbs"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <!-- Editable notes for the current slide. -->
+        <EditText
+            android:id="@+id/notes"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:background="@color/blue_grey_50"
+            android:gravity="left"
+            android:hint="@string/notes_hint"
+            android:textSize="18sp" />
+
+        <RelativeLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content">
+
+            <ImageView
+                android:id="@+id/next_thumb"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_centerInParent="true"
+                android:adjustViewBounds="true"
+                android:background="@color/blue_grey_50"
+                android:scaleType="fitCenter" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentTop="true"
+                android:layout_centerHorizontal="true"
+                android:layout_margin="@dimen/nav_hint_margin"
+                android:background="@drawable/nav_hint"
+                android:paddingLeft="@dimen/nav_hint_padding"
+                android:paddingRight="@dimen/nav_hint_padding"
+                android:text="@string/next_hint"
+                android:textColor="@color/nav_hint_text" />
+
+        </RelativeLayout>
+
+        <RelativeLayout
+            android:id="@+id/prev_thumb_layout"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content">
+
+            <ImageView
+                android:id="@+id/prev_thumb"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_centerInParent="true"
+                android:adjustViewBounds="true"
+                android:background="@color/blue_grey_50"
+                android:scaleType="fitCenter" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentTop="true"
+                android:layout_centerHorizontal="true"
+                android:layout_margin="@dimen/nav_hint_margin"
+                android:background="@drawable/nav_hint"
+                android:paddingLeft="@dimen/nav_hint_padding"
+                android:paddingRight="@dimen/nav_hint_padding"
+                android:text="@string/prev_hint"
+                android:textColor="@color/nav_hint_text" />
+
+        </RelativeLayout>
+
+    </LinearLayout>
+
+    <android.support.design.widget.CoordinatorLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        android:id="@+id/main_slide"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="3"
+        android:background="@color/matte_black">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1">
+
+                <!-- A medium version of the slide image. -->
+                <ImageView
+                    android:id="@+id/slide_current_medium"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:layout_gravity="center"
+                    android:layout_margin="12dp"
+                    android:adjustViewBounds="true"
+                    android:scaleType="fitCenter" />
+
+                <TextView
+                    android:id="@+id/slide_num_text"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignBottom="@id/slide_current_medium"
+                    android:layout_centerHorizontal="true"
+                    android:layout_margin="@dimen/nav_hint_margin"
+                    android:background="@drawable/nav_hint"
+                    android:paddingLeft="@dimen/nav_hint_padding"
+                    android:paddingRight="@dimen/nav_hint_padding"
+                    android:textColor="@color/nav_hint_text" />
+
+            </RelativeLayout>
+
+            <!-- A bar containing icons to navigate the presentation. -->
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/nav_bar_height"
+                android:background="@color/toolbar_gray"
+                android:gravity="center_vertical"
+                android:orientation="horizontal">
+
+                <!-- The icons below are 24dp scaled to 36dp.  For whatever reason, the
+                  36 dp icons look too fat. -->
+                <ImageView
+                    android:id="@+id/arrow_back"
+                    android:layout_width="@dimen/nav_button_size"
+                    android:layout_height="@dimen/nav_button_size"
+                    android:layout_marginLeft="@dimen/nav_button_margin"
+                    android:layout_marginRight="@dimen/nav_button_margin"
+                    android:src="@drawable/ic_arrow_back_white_24dp" />
+
+                <ImageView
+                    android:id="@+id/slide_list"
+                    android:layout_width="@dimen/nav_button_size"
+                    android:layout_height="@dimen/nav_button_size"
+                    android:layout_marginRight="@dimen/nav_button_margin"
+                    android:src="@drawable/ic_layers_white_24dp" />
+
+                <RelativeLayout
+                    android:id="@+id/questions_layout"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content">
+
+                    <ImageView
+                        android:id="@+id/questions"
+                        android:layout_width="@dimen/nav_button_size"
+                        android:layout_height="@dimen/nav_button_size"
+                        android:layout_marginRight="@dimen/nav_button_margin"
+                        android:src="@drawable/ic_live_help_white_24dp" />
+
+                    <TextView
+                        android:id="@+id/questions_num"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_alignRight="@id/questions"
+                        android:layout_alignTop="@id/questions"
+                        android:background="@drawable/orange_circle"
+                        android:gravity="center"
+                        android:textColor="@color/nav_question_num_text"
+                        android:textSize="@dimen/nav_question_num_size" />
+                </RelativeLayout>
+
+                <!-- This button shows up only some of the time.  NavigateFragment makes it visible
+                     dynamically. -->
+                <ImageView
+                    android:id="@+id/arrow_forward"
+                    android:layout_width="@dimen/nav_button_size"
+                    android:layout_height="@dimen/nav_button_size"
+                    android:layout_marginRight="@dimen/nav_button_margin"
+                    android:src="@drawable/ic_arrow_forward_white_24dp"
+                    android:visibility="visible" />
+            </LinearLayout>
+        </LinearLayout>
+
+        <!-- This button shows up only some of the time.  NavigateFragment makes it visible
+             dynamically. -->
+        <android.support.design.widget.FloatingActionButton
+            android:id="@+id/primary_navigation_fab"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/fab_margin"
+            android:clickable="true"
+            android:src="@drawable/ic_arrow_forward_white_36dp"
+            android:visibility="visible"
+            app:backgroundTint="@color/action_orange"
+            app:layout_anchor="@id/slide_current_medium"
+            app:layout_anchorGravity="bottom|right|end" />
+
+        <!-- This button shows up only some of the time.  NavigateFragment makes it visible
+             dynamically. -->
+        <android.support.design.widget.FloatingActionButton
+            android:id="@+id/audience_sync_fab"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/fab_margin"
+            android:clickable="true"
+            android:src="@drawable/ic_sync_white_36dp"
+            android:visibility="invisible"
+            app:backgroundTint="@color/action_orange"
+            app:layout_anchor="@id/slide_current_medium"
+            app:layout_anchorGravity="bottom|right|end" />
+    </android.support.design.widget.CoordinatorLayout>
+
+</LinearLayout>
diff --git a/android/app/src/main/res/layout/fragment_navigate.xml b/android/app/src/main/res/layout/fragment_navigate.xml
new file mode 100644
index 0000000..a4d2556
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_navigate.xml
@@ -0,0 +1,216 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/navigate_fragment"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <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="wrap_content"
+        android:background="@color/snow_2">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <!-- A medium version of the slide image. -->
+                <ImageView
+                    android:id="@+id/slide_current_medium"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:adjustViewBounds="true"
+                    android:scaleType="fitCenter" />
+
+                <TextView
+                    android:id="@+id/slide_num_text"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignBottom="@id/slide_current_medium"
+                    android:layout_centerHorizontal="true"
+                    android:layout_margin="@dimen/nav_hint_margin"
+                    android:background="@drawable/nav_hint"
+                    android:paddingLeft="@dimen/nav_hint_padding"
+                    android:paddingRight="@dimen/nav_hint_padding"
+                    android:textColor="@color/nav_hint_text" />
+
+            </RelativeLayout>
+
+            <!-- A bar containing icons to navigate the presentation. -->
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/nav_bar_height"
+                android:background="@color/toolbar_gray"
+                android:gravity="center_vertical"
+                android:orientation="horizontal">
+
+                <!-- The icons below are 24dp scaled to 36dp.  For whatever reason, the
+                  36 dp icons look too fat. -->
+                <ImageView
+                    android:id="@+id/arrow_back"
+                    android:layout_width="@dimen/nav_button_size"
+                    android:layout_height="@dimen/nav_button_size"
+                    android:layout_marginLeft="@dimen/nav_button_margin"
+                    android:layout_marginRight="@dimen/nav_button_margin"
+                    android:src="@drawable/ic_arrow_back_white_24dp" />
+
+                <ImageView
+                    android:id="@+id/slide_list"
+                    android:layout_width="@dimen/nav_button_size"
+                    android:layout_height="@dimen/nav_button_size"
+                    android:layout_marginRight="@dimen/nav_button_margin"
+                    android:src="@drawable/ic_layers_white_24dp" />
+
+                <RelativeLayout
+                    android:id="@+id/questions_layout"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content">
+
+                    <ImageView
+                        android:id="@+id/questions"
+                        android:layout_width="@dimen/nav_button_size"
+                        android:layout_height="@dimen/nav_button_size"
+                        android:layout_marginRight="@dimen/nav_button_margin"
+                        android:src="@drawable/ic_live_help_white_24dp" />
+
+                    <TextView
+                        android:id="@+id/questions_num"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_alignRight="@id/questions"
+                        android:layout_alignTop="@id/questions"
+                        android:background="@drawable/orange_circle"
+                        android:gravity="center"
+                        android:textColor="@color/nav_question_num_text"
+                        android:textSize="@dimen/nav_question_num_size" />
+                </RelativeLayout>
+
+
+                <!-- This button shows up only some of the time.  NavigateFragment makes it visible
+                     dynamically. -->
+                <ImageView
+                    android:id="@+id/arrow_forward"
+                    android:layout_width="@dimen/nav_button_size"
+                    android:layout_height="@dimen/nav_button_size"
+                    android:layout_marginRight="@dimen/nav_button_margin"
+                    android:src="@drawable/ic_arrow_forward_white_24dp"
+                    android:visibility="visible" />
+            </LinearLayout>
+        </LinearLayout>
+
+        <!-- This button shows up only some of the time.  NavigateFragment makes it visible
+             dynamically. -->
+        <android.support.design.widget.FloatingActionButton
+            android:id="@+id/primary_navigation_fab"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/fab_margin"
+            android:clickable="true"
+            android:src="@drawable/ic_arrow_forward_white_36dp"
+            android:visibility="visible"
+            app:backgroundTint="@color/action_orange"
+            app:layout_anchor="@id/slide_current_medium"
+            app:layout_anchorGravity="bottom|right|end" />
+
+        <!-- This button shows up only some of the time.  NavigateFragment makes it visible
+             dynamically. -->
+        <android.support.design.widget.FloatingActionButton
+            android:id="@+id/audience_sync_fab"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/fab_margin"
+            android:clickable="true"
+            android:src="@drawable/ic_sync_white_36dp"
+            android:visibility="invisible"
+            app:backgroundTint="@color/action_orange"
+            app:layout_anchor="@id/slide_current_medium"
+            app:layout_anchorGravity="bottom|right|end" />
+    </android.support.design.widget.CoordinatorLayout>
+
+    <!-- Editable notes for the current slide. -->
+    <EditText
+        android:id="@+id/notes"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:background="@color/blue_grey_50"
+        android:gravity="left"
+        android:hint="@string/notes_hint"
+        android:inputType="textMultiLine|textAutoComplete|textCapSentences"
+        android:scrollbars="vertical"
+        android:textSize="18sp" />
+
+    <!-- Display the next and previous slide thumbnails. -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <!-- We want the two RelativeLayouts below to equally share the width of the screen.
+          Setting the width to 0dp and the weight to 1 does the right thing. -->
+        <RelativeLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1">
+
+            <ImageView
+                android:id="@+id/prev_thumb"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_centerInParent="true"
+                android:adjustViewBounds="true"
+                android:background="@color/blue_grey_50"
+                android:scaleType="fitCenter" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentTop="true"
+                android:layout_margin="@dimen/nav_hint_margin"
+                android:background="@drawable/nav_hint"
+                android:paddingLeft="@dimen/nav_hint_padding"
+                android:paddingRight="@dimen/nav_hint_padding"
+                android:text="@string/prev_hint"
+                android:textColor="@color/nav_hint_text" />
+
+        </RelativeLayout>
+
+        <RelativeLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1">
+
+            <ImageView
+                android:id="@+id/next_thumb"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_centerInParent="true"
+                android:adjustViewBounds="true"
+                android:background="@color/blue_grey_50"
+                android:scaleType="fitCenter" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentRight="true"
+                android:layout_alignParentTop="true"
+                android:layout_margin="@dimen/nav_hint_margin"
+                android:background="@drawable/nav_hint"
+                android:paddingLeft="@dimen/nav_hint_padding"
+                android:paddingRight="@dimen/nav_hint_padding"
+                android:text="@string/next_hint"
+                android:textColor="@color/nav_hint_text" />
+
+        </RelativeLayout>
+
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/android/app/src/main/res/menu/edit_notes.xml b/android/app/src/main/res/menu/edit_notes.xml
new file mode 100644
index 0000000..0c07f98
--- /dev/null
+++ b/android/app/src/main/res/menu/edit_notes.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/action_save"
+        android:title="@string/action_save"
+        app:showAsAction="always"/>
+
+</menu>
\ No newline at end of file