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