syncslides: Display the list of slides in a deck.

Change-Id: I31d643729c97e378b82681ea223ae9a8e74a298e
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 0f1223b..2e8ea3c 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -30,6 +30,7 @@
             android:name=".DeckChooserActivity"
             android:label="@string/app_name" >
         </activity>
+        <activity android:name=".PresentationActivity" />
     </application>
 
 </manifest>
diff --git a/android/app/src/main/java/io/v/syncslides/DeckListAdapter.java b/android/app/src/main/java/io/v/syncslides/DeckListAdapter.java
index 6558118..0db269d 100644
--- a/android/app/src/main/java/io/v/syncslides/DeckListAdapter.java
+++ b/android/app/src/main/java/io/v/syncslides/DeckListAdapter.java
@@ -4,6 +4,8 @@
 
 package io.v.syncslides;
 
+import android.content.Context;
+import android.content.Intent;
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -12,6 +14,7 @@
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.TextView;
+import android.widget.Toast;
 import android.widget.Toolbar;
 
 import java.util.Calendar;
@@ -21,6 +24,7 @@
 import io.v.syncslides.model.Deck;
 import io.v.syncslides.model.DynamicList;
 import io.v.syncslides.model.ListListener;
+import io.v.v23.verror.VException;
 
 /**
  * Provides a list of decks to be shown in the RecyclerView of the
@@ -29,8 +33,9 @@
 public class DeckListAdapter extends RecyclerView.Adapter<DeckListAdapter.ViewHolder>
         implements ListListener {
     private static final String TAG = "DeckListAdapter";
+
+    private final DB mDB;
     private DynamicList<Deck> mDecks;
-    private DB mDB;
 
     public DeckListAdapter(DB db) {
         mDB = db;
@@ -90,6 +95,14 @@
             @Override
             public void onClick(View v) {
                 Log.d(TAG, "Clicking through to PresentationActivity.");
+                String sessionId;
+                try {
+                    sessionId = mDB.createSession(deck.getId());
+                } catch (VException e) {
+                    handleError(v.getContext(), "Could not view deck.", e);
+                    return;
+                }
+                startPresentationActivity(v.getContext(), sessionId);
             }
         });
     }
@@ -123,4 +136,16 @@
             mToolbar.inflateMenu(R.menu.deck_card);
         }
     }
+
+    private void startPresentationActivity(Context ctx, String sessionId) {
+        Intent intent = new Intent(ctx, PresentationActivity.class);
+        intent.putExtra(PresentationActivity.SESSION_ID_KEY, sessionId);
+        ctx.startActivity(intent);
+    }
+
+    private void handleError(Context ctx, String msg, Throwable throwable) {
+        Log.e(TAG, msg + ": " + Log.getStackTraceString(throwable));
+        Toast.makeText(ctx, msg, Toast.LENGTH_SHORT).show();
+    }
+
 }
diff --git a/android/app/src/main/java/io/v/syncslides/PresentationActivity.java b/android/app/src/main/java/io/v/syncslides/PresentationActivity.java
new file mode 100644
index 0000000..b8bc34d
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/PresentationActivity.java
@@ -0,0 +1,122 @@
+// 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.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
+
+import io.v.syncslides.db.DB;
+import io.v.syncslides.model.Session;
+import io.v.v23.verror.VException;
+
+/**
+ * Handles multiple views of a presentation: list of slides, fullscreen slide,
+ * navigate through the deck with notes.
+ */
+public class PresentationActivity extends AppCompatActivity {
+    private static final String TAG = "PresentationActivity";
+
+    public static final String SESSION_ID_KEY = "session_id_key";
+
+    private String mSessionId;
+    private Session mSession;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        // Immediately initialize V23, possibly sending user to the
+        // AccountManager to get blessings.
+        try {
+            V23.Singleton.get().init(getApplicationContext(), this);
+        } catch (InitException e) {
+            // TODO(kash): Start a to-be-written SettingsActivity that makes it possible
+            // to wipe the state of syncbase and/or blessings.
+            handleError("Failed to init", e);
+        }
+        setContentView(R.layout.activity_presentation);
+
+        if (savedInstanceState == null) {
+            mSessionId = 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;
+        }
+        DB db = DB.Singleton.get();
+        try {
+            mSession = db.getSession(mSessionId);
+        } catch (VException e) {
+            handleError("Failed to load state", e);
+            finish();
+        }
+        showSlideList();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        try {
+            if (V23.Singleton.get().onActivityResult(
+                    getApplicationContext(), requestCode, resultCode, data)) {
+                return;
+            }
+        } catch (InitException e) {
+            // TODO(kash): Start a to-be-written SettingsActivity that makes it possible
+            // to wipe the state of syncbase and/or blessings.
+            handleError("Failed onActivityResult initialization", e);
+        }
+        // Any other activity results would be handled here.
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle b) {
+        super.onSaveInstanceState(b);
+        b.putString(SESSION_ID_KEY, mSessionId);
+    }
+
+    /**
+     * Set the system UI to be immersive or not.
+     */
+    public void setUiImmersive(boolean immersive) {
+        if (immersive) {
+            getSupportActionBar().hide();
+            getWindow().getDecorView().setSystemUiVisibility(
+                    View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+                            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+                            | View.SYSTEM_UI_FLAG_FULLSCREEN
+                            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+        } else {
+            getSupportActionBar().show();
+            // See the comment at the top of fragment_slide_list.xml for why we don't simply
+            // use View.SYSTEM_UI_FLAG_VISIBLE.
+            getWindow().getDecorView().setSystemUiVisibility(
+                    View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+                            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+        }
+    }
+
+    /**
+     * Shows the slide list, where users can see the slides in a presentation
+     * and click on one to browse the deck, or press the play FAB to start
+     * presenting.
+     */
+    public void showSlideList() {
+        SlideListFragment fragment = SlideListFragment.newInstance(mSessionId);
+        getSupportFragmentManager().beginTransaction().replace(R.id.fragment, fragment).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
new file mode 100644
index 0000000..a0e01ca
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/SlideListAdapter.java
@@ -0,0 +1,88 @@
+// 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.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+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;
+
+/**
+ * Provides a list of slides to be shown in the RecyclerView of the SlideListFragment.
+ */
+public class SlideListAdapter extends RecyclerView.Adapter<SlideListAdapter.ViewHolder>
+        implements ListListener {
+
+    private final RecyclerView mRecyclerView;
+    private DynamicList<Slide> mSlides;
+
+    public SlideListAdapter(RecyclerView recyclerView, DB db, Session session) {
+        mRecyclerView = recyclerView;
+        mSlides = db.getPresentation(session).getSlides();
+        mSlides.addListener(this);
+    }
+
+    @Override
+    public SlideListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        View v = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.slide_card, parent, false);
+        v.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                int position = mRecyclerView.getChildAdapterPosition(v);
+                // TODO(kash): Implement me.
+                //((PresentationActivity) v.getContext()).navigateToSlide(position);
+            }
+        });
+        return new ViewHolder(v);
+    }
+
+    @Override
+    public void onBindViewHolder(SlideListAdapter.ViewHolder holder, int position) {
+        Slide slide = mSlides.get(position);
+        holder.mNotes.setText(slide.getNotes());
+        holder.mImage.setImageBitmap(slide.getThumb());
+    }
+
+    @Override
+    public int getItemCount() {
+        return mSlides.getItemCount();
+    }
+
+    @Override
+    public void onError(Exception e) {
+        // TODO(kash): Not sure what to do here...  Call start()/stop() to reset
+        // the DynamicList?
+    }
+
+    /**
+     * Stops any background monitoring of the underlying data.
+     */
+    public void stop() {
+        mSlides.removeListener(this);
+        mSlides = null;
+    }
+
+
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+        public final ImageView mImage;
+        public final TextView mNotes;
+
+        public ViewHolder(View itemView) {
+            super(itemView);
+            mImage = (ImageView) itemView.findViewById(R.id.slide_card_image);
+            mNotes = (TextView) itemView.findViewById(R.id.slide_card_notes);
+        }
+    }
+
+}
diff --git a/android/app/src/main/java/io/v/syncslides/SlideListFragment.java b/android/app/src/main/java/io/v/syncslides/SlideListFragment.java
new file mode 100644
index 0000000..183df0d
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/SlideListFragment.java
@@ -0,0 +1,120 @@
+// 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.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.app.Fragment;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import io.v.syncslides.db.DB;
+import io.v.syncslides.model.Session;
+import io.v.v23.verror.VException;
+
+/**
+ * Displays the set of slides in a deck as a scrolling list.
+ */
+public class SlideListFragment extends Fragment {
+
+    private static final String SESSION_ID_KEY = "session_id_key";
+    private static final String TAG = "SlideListFragment";
+
+    private Session mSession;
+    private RecyclerView mRecyclerView;
+    private LinearLayoutManager mLayoutManager;
+    private SlideListAdapter mAdapter;
+
+    /**
+     * Returns a new instance of this fragment for the given deck.
+     */
+    public static SlideListFragment newInstance(String sessionId) {
+        SlideListFragment fragment = new SlideListFragment();
+        Bundle args = new Bundle();
+        args.putString(SESSION_ID_KEY, sessionId);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        // See comment at the top of fragment_slide_list.xml.
+        ((PresentationActivity) getActivity()).setUiImmersive(false);
+        // Inflate the layout for this fragment
+        View rootView = inflater.inflate(R.layout.fragment_slide_list, container, false);
+
+        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) {
+            handleError("Failed to fetch Session", e);
+            getActivity().finish();
+        }
+
+        // If there is not already a presentation for this session,
+        // make the FAB visible so that clicking it will start a new
+        // presentation.
+        if (mSession.getPresentationId() == null) {
+            final FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(
+                    R.id.play_presentation_fab);
+            fab.setVisibility(View.VISIBLE);
+            fab.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    // TODO(kash): Implement me.
+//                    mRole = Role.PRESENTER;
+//                    fab.setVisibility(View.INVISIBLE);
+//                    PresentationActivity activity = (PresentationActivity) v.getContext();
+//                    activity.startPresentation();
+                }
+            });
+        }
+        mRecyclerView = (RecyclerView) rootView.findViewById(R.id.slide_list);
+        mRecyclerView.setHasFixedSize(true);
+
+        mLayoutManager = new LinearLayoutManager(container.getContext(),
+                LinearLayoutManager.VERTICAL, false);
+        mRecyclerView.setLayoutManager(mLayoutManager);
+
+        return rootView;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        DB db = DB.Singleton.get();
+        mAdapter = new SlideListAdapter(mRecyclerView, db, mSession);
+        mRecyclerView.setAdapter(mAdapter);
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        mAdapter.stop();
+        mAdapter = null;
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putString(SESSION_ID_KEY, mSession.getId());
+    }
+
+    private void handleError(String msg, Throwable throwable) {
+        Log.e(TAG, msg + ": " + Log.getStackTraceString(throwable));
+        Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
+    }
+
+}
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 884e0c3..2378d7f 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
@@ -43,6 +43,15 @@
     void init(Context context) throws InitException;
 
     /**
+     * Creates a new session in the database for a local viewing of a deck.  The user
+     * can later turn this into a live presentation.
+     *
+     * @param deckId the ID of the deck that the user is viewing
+     * @return the unique ID for the session to later be passed to {@link #getSession(string)}.
+     */
+    String createSession(String deckId) throws VException;
+
+    /**
      * 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
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 0d2a736..e7a11d8 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 java.io.IOException;
 import java.io.OutputStream;
 import java.util.Arrays;
+import java.util.UUID;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Executors;
 
@@ -169,11 +170,21 @@
     }
 
     @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));
+        return uuid;
+    }
+
+    @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);
+        return new SyncbaseSession(sessionId, vSession);
     }
 
     @Override
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 2e9bd43..c70a6af 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
@@ -10,13 +10,20 @@
  * SyncbaseSession gets its session state from Syncbase.
  */
 class SyncbaseSession implements Session {
+    private String mId;
     private final VSession mVSession;
 
-    SyncbaseSession(VSession vSession) {
+    SyncbaseSession(String id, VSession vSession) {
+        mId = id;
         mVSession = vSession;
     }
 
     @Override
+    public String getId() {
+        return mId;
+    }
+
+    @Override
     public String getDeckId() {
         return mVSession.getDeckId();
     }
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 7381897..a674964 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
@@ -8,6 +8,7 @@
  * A Session represents the UI state for one presentation.
  */
 public interface Session {
+    String getId();
     String getDeckId();
     String getPresentationId();
 }
diff --git a/android/app/src/main/res/layout/activity_presentation.xml b/android/app/src/main/res/layout/activity_presentation.xml
new file mode 100644
index 0000000..184c363
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_presentation.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- As the main content view, the view below consumes the entire space available using match_parent
+ in both dimensions. This frame encompasses all views for the presentation activity, including the
+ slide list, presenter, and audience views. -->
+<FrameLayout
+    android:id="@+id/fragment"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
diff --git a/android/app/src/main/res/layout/fragment_slide_list.xml b/android/app/src/main/res/layout/fragment_slide_list.xml
new file mode 100644
index 0000000..32fe27e
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_slide_list.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- We want to enter immersive mode whenever the NavigationFragment starts,
+and exit immersive mode when it stops (e.g. user goes back to slide list).
+However, this doesn't just work.  Going back to the slide list causes
+the slide list's toolbar to be shifted down.  Between the toolbar and the
+status bar, there is a gray strip that is the same height as the status
+bar.  We work around this mis-render by having the content
+of the slide list go behind the toolbar.  We then need to pad the content
+of the slide list to be below the toolbar. -->
+<android.support.design.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/slide_list_coordinator"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/snow_2"
+    android:label="@string/slide_list_title"
+    android:paddingTop="@dimen/toolbar_and_statusbar_height"
+    tools:context=".PresentationActivity">
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/slide_list"
+        android:name="io.v.syncslides.SlideListFragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scrollbars="vertical"/>
+
+    <android.support.design.widget.FloatingActionButton
+        android:id="@+id/play_presentation_fab"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="end|bottom"
+        android:layout_margin="@dimen/fab_margin"
+        android:src="@drawable/ic_play_arrow_white_36dp"
+        android:visibility="invisible"
+        app:backgroundTint="@color/action_orange"
+        app:elevation="@dimen/fab_elevation"
+        app:fabSize="normal"/>
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/android/app/src/main/res/layout/slide_card.xml b/android/app/src/main/res/layout/slide_card.xml
new file mode 100644
index 0000000..45f484a
--- /dev/null
+++ b/android/app/src/main/res/layout/slide_card.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--TODO(afergan): set the aspect ratio of the image to be 16:9-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <Space
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"/>
+
+    <android.support.v7.widget.CardView
+        android:id="@+id/slide_card"
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="@dimen/slide_card_width"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_margin="@dimen/slide_card_margin">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+
+            <!-- The actual slide image to be shown-->
+            <ImageView
+                android:id="@+id/slide_card_image"
+                android:layout_width="0dp"
+                android:layout_height="@dimen/slide_card_height"
+                android:layout_weight="1"
+                android:scaleType="centerCrop"/>
+
+            <!-- Presenter or audience notes to be shown alongside the slide-->
+            <TextView
+                android:id="@+id/slide_card_notes"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_margin="@dimen/slide_card_text_margin"
+                android:layout_weight="1"/>
+        </LinearLayout>
+    </android.support.v7.widget.CardView>
+
+    <Space
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"/>
+</LinearLayout>
\ No newline at end of file