syncslides: Editing notes.
Port the functionality for editing notes. The old code handled this
differently: it would update the in-memory state in addition to
saving the notes to the DB. This change switches to a unidirectional
flow of data, so it updates the DB and then watches for changes to
the notes.
Change-Id: Ie9757fd53b20f89e687378d5aa41b912ff0725e0
diff --git a/android/app/src/main/java/io/v/syncslides/NavigateFragment.java b/android/app/src/main/java/io/v/syncslides/NavigateFragment.java
index 7a2cb34..105df9f 100644
--- a/android/app/src/main/java/io/v/syncslides/NavigateFragment.java
+++ b/android/app/src/main/java/io/v/syncslides/NavigateFragment.java
@@ -4,6 +4,7 @@
package io.v.syncslides;
+import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.os.Bundle;
@@ -15,6 +16,8 @@
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@@ -43,6 +46,7 @@
private ImageView mNextThumb;
private ImageView mCurrentSlide;
private TextView mSlideNumText;
+ private EditText mNotes;
private boolean mEditing;
private DynamicList<Slide> mSlides;
@@ -142,26 +146,34 @@
// });
//
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);
-//
+ mNotes = (EditText) rootView.findViewById(R.id.notes);
+ mNotes.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ ((PresentationActivity) getActivity()).getSupportActionBar().show();
+ mEditing = true;
+ getActivity().invalidateOptionsMenu();
+ // We don't want the presentation to advance while the user
+ // is editing the notes. Force the app to stay on this slide.
+ try {
+ mSession.setLocalSlideNum(mSlideNum);
+ } catch (VException e) {
+ handleFatalError("Could not set local slide num", e);
+ }
+ }
+ }
+ });
+
+ // 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
@@ -294,12 +306,11 @@
} 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();
-// }
+ 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()));
}
@@ -329,26 +340,28 @@
/**
* If the user is editing the text field and the text has changed, save the
- * notes locally and in Syncbase.
+ * notes in Syncbase. That will trigger a notification that the slide has
+ * changed and the UI will refresh.
*/
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);
+ final String notes = mNotes.getText().toString();
+ if (mEditing && (!notes.equals(mSlides.get(mSlideNum).getNotes()))) {
+ try {
+ mSession.setNotes(mSlideNum, notes);
+ } catch (VException e) {
+ handleError("Could not save notes", e);
+ }
+ }
+ mNotes.clearFocus();
+ mEditing = false;
+ 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) {
diff --git a/android/app/src/main/java/io/v/syncslides/db/SlideWatcher.java b/android/app/src/main/java/io/v/syncslides/db/SlideWatcher.java
index 391b42c..a69b9cf 100644
--- a/android/app/src/main/java/io/v/syncslides/db/SlideWatcher.java
+++ b/android/app/src/main/java/io/v/syncslides/db/SlideWatcher.java
@@ -42,12 +42,24 @@
}
@Override
- public void watch(VContext context, Listener<Slide> listener) {
+ public void watch(final VContext context, final Listener<Slide> listener) {
try {
BatchDatabase batch = sync(mDb.beginBatch(context, null));
- ResumeMarker watchMarker = sync(batch.getResumeMarker(context));
+ final ResumeMarker watchMarker = sync(batch.getResumeMarker(context));
fetchInitialState(context, listener, batch);
- watchChanges(context, listener, watchMarker);
+ // Need to watch two tables, but the API allows watching only one
+ // table at a time. Start another thread for watching the notes.
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ watchNoteChanges(context, listener, watchMarker);
+ } catch (VException e) {
+ listener.onError(e);
+ }
+ }
+ }).start();
+ watchSlideChanges(context, listener, watchMarker);
} catch (VException e) {
listener.onError(e);
}
@@ -77,8 +89,8 @@
}
}
- private void watchChanges(VContext context, Listener<Slide> listener, ResumeMarker watchMarker)
- throws VException {
+ private void watchSlideChanges(VContext context, Listener<Slide> listener,
+ ResumeMarker watchMarker) throws VException {
Table notesTable = mDb.getTable(SyncbaseDB.NOTES_TABLE);
VIterable<WatchChange> changes =
sync(mDb.watch(context, SyncbaseDB.DECKS_TABLE, mDeckId, watchMarker));
@@ -109,6 +121,42 @@
}
}
+ private void watchNoteChanges(VContext context, Listener<Slide> listener,
+ ResumeMarker watchMarker) throws VException {
+ Table decksTable = mDb.getTable(SyncbaseDB.DECKS_TABLE);
+ VIterable<WatchChange> changes =
+ sync(mDb.watch(context, SyncbaseDB.NOTES_TABLE, mDeckId, watchMarker));
+ for (WatchChange change : changes) {
+ String key = change.getRowName();
+ if (!SyncbaseDB.isSlideKey(key)) {
+ continue;
+ }
+ VSlide vSlide = fetchVSlide(context, decksTable, key);
+ if (vSlide == null) {
+ // If the VSlide was deleted, the other watcher will handle the notification.
+ continue;
+ }
+ String notes = null;
+ if (change.getChangeType().equals(ChangeType.PUT_CHANGE)) {
+ VNote vNote = null;
+ try {
+ vNote = (VNote) VomUtil.decode(change.getVomValue(), VNote.class);
+ } catch (VException e) {
+ Log.e(TAG, "Couldn't decode notes: " + e.toString());
+ continue; // Just skip it.
+ }
+ notes = vNote.getText();
+ } else { // ChangeType.DELETE_CHANGE
+ notes = "";
+ }
+ Slide newSlide = new DBSlide(key, vSlide, notes);
+ listener.onPut(newSlide);
+ }
+ if (changes.error() != null) {
+ throw changes.error();
+ }
+ }
+
/**
* Returns true if {@code key} looks like a VDeck and not a VSlide.
*/
@@ -126,4 +174,13 @@
return "";
}
}
+
+ private static VSlide fetchVSlide(VContext context, Table decksTable, String key)
+ throws VException {
+ try {
+ return (VSlide) sync(decksTable.get(context, key, VSlide.class));
+ } catch (NoExistException e) {
+ return null;
+ }
+ }
}
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 99306fb..1580df3 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.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
@@ -65,6 +66,7 @@
static final String CURRENT_SLIDE = "CurrentSlide";
static final String QUESTIONS = "questions";
private static final String SYNCGROUP_PRESENTATION_DESCRIPTION = "Live Presentation";
+ public static final String SLIDE_DIR = "slides";
private boolean mInitialized = false;
private Handler mHandler;
@@ -246,7 +248,16 @@
Long.class);
}
- private String slideRowKey(String deckId, int slideNum) {
- return NamingUtil.join(deckId, "slides", String.format("%04d", slideNum));
+ static String slideRowKey(String deckId, int slideNum) {
+ return NamingUtil.join(deckId, SLIDE_DIR, String.format("%04d", slideNum));
}
+
+ static boolean isSlideKey(String key) {
+ List<String> parts = NamingUtil.split(key);
+ if (parts.size() != 3 || !parts.get(1).equals(SLIDE_DIR)) {
+ return false;
+ }
+ return true;
+ }
+
}
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 2ac8865..fbecc23 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
@@ -86,6 +86,14 @@
return mSlides;
}
+ @Override
+ public void setNotes(int slideNum, String notes) throws VException {
+ Table table = mDb.getTable(SyncbaseDB.NOTES_TABLE);
+ CancelableVContext context = mVContext.withTimeout(Duration.millis(5000));
+ String rowKey = SyncbaseDB.slideRowKey(mVSession.getDeckId(), slideNum);
+ sync(table.put(context, rowKey, new VNote(notes), VNote.class));
+ }
+
/**
* Persists the VSession to the UI_TABLE.
*
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 0c6a877..a570f1f 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
@@ -58,4 +58,8 @@
*/
DynamicList<Slide> getSlides();
+ /**
+ * Sets the notes for the given slide.
+ */
+ void setNotes(int slideNum, String notes) throws VException;
}