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