SyncbaseTodoList persistence
Change-Id: I89e3780fda046d45781d5a0ad0f33d23db664d8c
diff --git a/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java b/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
index b2a5497..d3ebf20 100644
--- a/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
+++ b/app/src/androidTestMock/java/io/v/todos/MainActivityTest.java
@@ -29,7 +29,9 @@
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.not;
import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -125,7 +127,7 @@
verify(mocked).addTodoList(any(ListSpec.class));
verify(mocked, never()).deleteTodoList(anyString());
- verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+ verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
}
// Press the fab but don't actually add the item.
@@ -141,7 +143,7 @@
verify(mocked, never()).addTodoList(any(ListSpec.class));
verify(mocked, never()).deleteTodoList(anyString());
- verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+ verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
}
// Press the fab but don't actually add the item.
@@ -157,7 +159,7 @@
verify(mocked, never()).addTodoList(any(ListSpec.class));
verify(mocked, never()).deleteTodoList(anyString());
- verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+ verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
}
// Add some default items so that we can interact with them with swipes.
@@ -265,7 +267,7 @@
verify(mocked, never()).addTodoList(any(ListSpec.class));
verify(mocked, never()).deleteTodoList(anyString());
- verify(mocked).completeAllTasks(any(ListMetadata.class));
+ verify(mocked).setCompletion(any(ListMetadata.class), eq(true));
}
// Swipe a todo list item to the left to attempt to delete it.
@@ -282,7 +284,7 @@
verify(mocked, never()).addTodoList(any(ListSpec.class));
verify(mocked).deleteTodoList(anyString());
- verify(mocked, never()).completeAllTasks(any(ListMetadata.class));
+ verify(mocked, never()).setCompletion(any(ListMetadata.class), anyBoolean());
}
// Tap a todo list item to launch its corresponding TodoListActivity
diff --git a/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java b/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
index 2df421e..a8f7f1b 100644
--- a/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
+++ b/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
@@ -84,7 +84,7 @@
}
@Override
- public void completeAllTasks(final ListMetadata listMetadata) {
+ public void setCompletion(final ListMetadata listMetadata, final boolean done) {
// Update all child tasks for this key to have done = true.
Firebase tasksRef = getFirebase().child(FirebaseTodoList.TASKS).child(listMetadata.key);
tasksRef.runTransaction(new Transaction.Handler() {
@@ -94,7 +94,7 @@
// this in a batch or to split up the Task into components.
for (MutableData taskData : mutableData.getChildren()) {
TaskSpec spec = taskData.getValue(TaskSpec.class);
- spec.setDone(true);
+ spec.setDone(done);
taskData.setValue(spec);
}
return Transaction.success(mutableData);
diff --git a/app/src/main/java/io/v/todos/MainActivity.java b/app/src/main/java/io/v/todos/MainActivity.java
index 8c1e5e3..878f28f 100644
--- a/app/src/main/java/io/v/todos/MainActivity.java
+++ b/app/src/main/java/io/v/todos/MainActivity.java
@@ -33,19 +33,18 @@
* @author alexfandrianto
*/
public class MainActivity extends Activity {
- private static final String TAG = "MainActivity";
-
private MainPersistence mPersistence;
// Snackoos are the code name for the list of todos.
// These todos are backed up at the SNACKOOS child of the Firebase URL.
- // We use the snackoosList to track a custom sorted list of the stored values.
+ // We use mMainList to track a custom sorted list of the stored values.
static final String INTENT_SNACKOO_KEY = "snackoo key";
- private DataList<ListMetadata> snackoosList = new DataList<>();
+ private DataList<ListMetadata> mMainList = new DataList<>();
// This adapter handle mirrors the firebase list values and generates the corresponding todo
// item View children for a list view.
- private TodoListRecyclerAdapter adapter;
+ private TodoListRecyclerAdapter mAdapter;
+ private RecyclerView mRecyclerView;
@Override
protected void onDestroy() {
@@ -65,7 +64,7 @@
getActionBar().setTitle(R.string.app_name);
// Set up the todo list adapter
- adapter = new TodoListRecyclerAdapter(snackoosList, new View.OnClickListener() {
+ mAdapter = new TodoListRecyclerAdapter(mMainList, new View.OnClickListener() {
@Override
public void onClick(View view) {
String fbKey = (String)view.getTag();
@@ -76,39 +75,36 @@
}
});
- RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler);
- recyclerView.setAdapter(adapter);
+ mRecyclerView = (RecyclerView)findViewById(R.id.recycler);
+ mRecyclerView.setAdapter(mAdapter);
new ItemTouchHelper(new SwipeableTouchHelperCallback() {
@Override
public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int direction) {
String todoListKey = (String)viewHolder.itemView.getTag();
if (direction == ItemTouchHelper.RIGHT) {
- int position = snackoosList.findIndexByKey(todoListKey);
+ int position = mMainList.findIndexByKey(todoListKey);
if (position == -1) {
return;
}
- ListMetadata l = snackoosList.get(position);
+ ListMetadata l = mMainList.get(position);
if (l.canCompleteAll()) {
- mPersistence.completeAllTasks(l);
+ mPersistence.setCompletion(l, true);
+ } else if (l.numTasks > 0) {
+ mPersistence.setCompletion(l, false);
+ } else {
+ mAdapter.notifyItemChanged(position);
}
- // TODO(alexfandrianto): Can we remove this? The bug when we don't have it
- // here is that the swiped card doesn't return from being swiped to the right
- // whether or not is it marking tasks as done or not.
- // Doing this causes a little bit of flicker when marking all tasks as done and
- // appears natural in the no-op case.
- adapter.notifyItemChanged(position);
} else if (direction == ItemTouchHelper.LEFT) {
mPersistence.deleteTodoList(todoListKey);
}
}
- }).attachToRecyclerView(recyclerView);
+ }).attachToRecyclerView(mRecyclerView);
new PersistenceInitializer<MainPersistence>(this) {
@Override
protected MainPersistence initPersistence() throws Exception {
- return PersistenceFactory.getMainPersistence(mActivity,
- createMainListener());
+ return PersistenceFactory.getMainPersistence(mActivity, createMainListener());
}
@Override
@@ -123,31 +119,49 @@
@VisibleForTesting
ListEventListener<ListMetadata> createMainListener() {
return new ListEventListener<ListMetadata>() {
- @Override
- public void onItemAdd(ListMetadata item) {
- int position = snackoosList.insertInOrder(item);
+ @Override
+ public void onItemAdd(ListMetadata item) {
+ int position = mMainList.insertInOrder(item);
- adapter.notifyItemInserted(position);
- setEmptyVisiblity();
- }
+ mAdapter.notifyItemInserted(position);
+ setEmptyVisiblity();
+ }
- @Override
- public void onItemUpdate(ListMetadata item) {
- int start = snackoosList.findIndexByKey(item.key);
- int end = snackoosList.updateInOrder(item);
+ @Override
+ public void onItemUpdate(final ListMetadata item) {
+ int start = mMainList.findIndexByKey(item.key);
+ int end = mMainList.updateInOrder(item);
- adapter.notifyItemMoved(start, end);
- adapter.notifyItemChanged(end);
- }
+ if (start != end) {
+ mAdapter.notifyItemMoved(start, end);
+ }
- @Override
- public void onItemDelete(String key) {
- int position = snackoosList.removeByKey(key);
+ // The change animation involves a cross-fade that, if interrupted
+ // while another for the same item is already in progress, interacts
+ // badly with ItemTouchHelper's swipe animator. The effect would be
+ // a flicker of the intermediate ListMetadata view, then it fading
+ // out to the latest view but X-translated off the screen due to the
+ // swipe animator.
+ //
+ // We could queue up the next change after the current one, but it's
+ // probably better just to rebind.
+ View view = mRecyclerView.getChildAt(end);
+ if (view.getAlpha() < 1) {
+ mAdapter.bindViewHolder((TodoListViewHolder) mRecyclerView
+ .getChildViewHolder(view), end);
+ } else {
+ mAdapter.notifyItemChanged(end);
+ }
+ }
- adapter.notifyItemRemoved(position);
- setEmptyVisiblity();
- }
- };
+ @Override
+ public void onItemDelete(String key) {
+ int position = mMainList.removeByKey(key);
+
+ mAdapter.notifyItemRemoved(position);
+ setEmptyVisiblity();
+ }
+ };
}
// Allow the tests to mock out the main persistence.
@@ -159,7 +173,7 @@
// Set the visibility based on what the adapter thinks is the visible item count.
private void setEmptyVisiblity() {
View v = findViewById(R.id.empty);
- v.setVisibility(adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
+ v.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
public void initiateItemAdd(View view) {
diff --git a/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java b/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java
index 58856c3..717461e 100644
--- a/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java
+++ b/app/src/main/java/io/v/todos/SwipeableTouchHelperCallback.java
@@ -44,6 +44,7 @@
}
}
+ @Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
SwipeableCardViewHolder holder = (SwipeableCardViewHolder)viewHolder;
@@ -52,6 +53,7 @@
isCurrentlyActive);
}
+ @Override
public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder, float dX, float dY,
int actionState, boolean isCurrentlyActive) {
diff --git a/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java b/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java
index 73533dc..02ab30b 100644
--- a/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java
+++ b/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java
@@ -17,16 +17,16 @@
* @author alexfandrianto
*/
public class TaskRecyclerAdapter extends RecyclerView.Adapter<TaskViewHolder> {
- private ArrayList<Task> backup;
- private View.OnClickListener itemListener;
- private boolean showDone = true;
+ private ArrayList<Task> mBackup;
+ private View.OnClickListener mItemListener;
+ private boolean mShowDone = true;
private static final int RESOURCE_ID = R.layout.task_row;
public TaskRecyclerAdapter(ArrayList<Task> backup, View.OnClickListener itemListener) {
super();
- this.backup = backup;
- this.itemListener = itemListener;
+ mBackup = backup;
+ mItemListener = itemListener;
}
@Override
@@ -38,26 +38,39 @@
@Override
public void onBindViewHolder(TaskViewHolder holder, int position) {
- Task task = backup.get(position);
- holder.bindTask(task, itemListener);
+ Task task = mBackup.get(position);
+ holder.bindTask(task, mItemListener);
}
@Override
public int getItemCount() {
- return showDone ? backup.size() : nonDoneSize();
+ return mShowDone ? mBackup.size() : nonDoneSize();
}
private int nonDoneSize() {
- for (int i = 0; i < backup.size(); i++) {
- if (backup.get(i).done) {
+ for (int i = 0; i < mBackup.size(); i++) {
+ if (mBackup.get(i).done) {
return i;
}
}
- return backup.size();
+ return mBackup.size();
}
public void setShowDone(boolean showDone) {
- this.showDone = showDone;
- this.notifyDataSetChanged();
+ if (mShowDone != showDone) {
+ mShowDone = showDone;
+ int nonDoneSize = nonDoneSize(),
+ doneSize = mBackup.size() - nonDoneSize;
+
+ if (showDone) {
+ notifyItemRangeInserted(nonDoneSize, doneSize);
+ } else {
+ notifyItemRangeRemoved(nonDoneSize, doneSize);
+ }
+ }
+ }
+
+ public boolean getShowDone() {
+ return mShowDone;
}
}
\ No newline at end of file
diff --git a/app/src/main/java/io/v/todos/TaskViewHolder.java b/app/src/main/java/io/v/todos/TaskViewHolder.java
index 2aed003..89e8262 100644
--- a/app/src/main/java/io/v/todos/TaskViewHolder.java
+++ b/app/src/main/java/io/v/todos/TaskViewHolder.java
@@ -14,8 +14,6 @@
* @author alexfandrianto
*/
public class TaskViewHolder extends SwipeableCardViewHolder {
- private boolean showDone = true;
-
public TaskViewHolder(View itemView) {
super(itemView);
}
@@ -34,15 +32,9 @@
itemView.setTag(task.key);
itemView.setOnClickListener(listener);
-
- itemView.setVisibility(!showDone && task.done ? View.GONE : View.VISIBLE);
}
private String computeCreated(Task task) {
return UIUtil.computeTimeAgo("Created", task.addedAt);
}
-
- public void setShowDone(boolean showDone) {
- this.showDone = showDone;
- }
}
diff --git a/app/src/main/java/io/v/todos/TodoListActivity.java b/app/src/main/java/io/v/todos/TodoListActivity.java
index ebe2e68..a5c603b 100644
--- a/app/src/main/java/io/v/todos/TodoListActivity.java
+++ b/app/src/main/java/io/v/todos/TodoListActivity.java
@@ -46,7 +46,6 @@
// The menu item that toggles whether done items are shown or not.
private MenuItem mShowDoneMenuItem;
- private boolean mShowDone; // mirrors the checked status of mShowDoneMenuItem
@Override
protected void onDestroy() {
@@ -110,7 +109,6 @@
@Override
public void onUpdateShowDone(boolean showDone) {
- mShowDone = showDone;
if (mShowDoneMenuItem != null) {
// Only interact with mShowDoneMenu if it has been inflated.
mShowDoneMenuItem.setChecked(showDone);
@@ -212,8 +210,8 @@
// Also, obtain the reference to the show done menu item.
mShowDoneMenuItem = menu.findItem(R.id.show_done);
- // Since the menu item may be inflated too late, set checked to mShowDone.
- mShowDoneMenuItem.setChecked(mShowDone);
+ // Since the menu item may be inflated too late, set checked to the adapter's value.
+ mShowDoneMenuItem.setChecked(adapter.getShowDone());
return true;
}
diff --git a/app/src/main/java/io/v/todos/TodoListRecyclerAdapter.java b/app/src/main/java/io/v/todos/TodoListRecyclerAdapter.java
index 46f6cd3..079a0c9 100644
--- a/app/src/main/java/io/v/todos/TodoListRecyclerAdapter.java
+++ b/app/src/main/java/io/v/todos/TodoListRecyclerAdapter.java
@@ -17,15 +17,15 @@
* @author alexfandrianto
*/
public class TodoListRecyclerAdapter extends RecyclerView.Adapter<TodoListViewHolder> {
- private ArrayList<ListMetadata> backup;
- private View.OnClickListener itemListener;
+ private ArrayList<ListMetadata> mBackup;
+ private View.OnClickListener mItemListener;
private static final int RESOURCE_ID = R.layout.todo_list_row;
public TodoListRecyclerAdapter(ArrayList<ListMetadata> backup, View.OnClickListener itemListener) {
super();
- this.backup = backup;
- this.itemListener = itemListener;
+ this.mBackup = backup;
+ this.mItemListener = itemListener;
}
@Override
@@ -37,12 +37,12 @@
@Override
public void onBindViewHolder(TodoListViewHolder holder, int position) {
- ListMetadata listMetadata = backup.get(position);
- holder.bindTodoList(listMetadata, itemListener);
+ ListMetadata listMetadata = mBackup.get(position);
+ holder.bindTodoList(listMetadata, mItemListener);
}
@Override
public int getItemCount() {
- return backup.size();
+ return mBackup.size();
}
}
diff --git a/app/src/main/java/io/v/todos/TodoListViewHolder.java b/app/src/main/java/io/v/todos/TodoListViewHolder.java
index 5ddfd8c..0394188 100644
--- a/app/src/main/java/io/v/todos/TodoListViewHolder.java
+++ b/app/src/main/java/io/v/todos/TodoListViewHolder.java
@@ -13,19 +13,19 @@
* @author alexfandrianto
*/
public class TodoListViewHolder extends SwipeableCardViewHolder {
+ private final TextView mName, mCompletedStatus, mTimeAgo;
+
public TodoListViewHolder(View itemView) {
super(itemView);
+ mName = (TextView) itemView.findViewById(R.id.todo_list_name);
+ mCompletedStatus = (TextView) itemView.findViewById(R.id.todo_list_completed);
+ mTimeAgo = (TextView) itemView.findViewById(R.id.todo_list_time);
}
public void bindTodoList(ListMetadata listMetadata, View.OnClickListener listener) {
- final TextView name=(TextView) itemView.findViewById(R.id.todo_list_name);
- name.setText(listMetadata.name);
-
- final TextView completedStatus=(TextView) itemView.findViewById(R.id.todo_list_completed);
- completedStatus.setText(computeCompleted(listMetadata));
-
- final TextView timeAgo=(TextView) itemView.findViewById(R.id.todo_list_time);
- timeAgo.setText(computeTimeAgo(listMetadata));
+ mName.setText(listMetadata.name);
+ mCompletedStatus.setText(computeCompleted(listMetadata));
+ mTimeAgo.setText(computeTimeAgo(listMetadata));
getCardView().setCardBackgroundColor(listMetadata.isDone() ? 0xFFCCCCCC : 0xFFFFFFFF);
@@ -48,4 +48,9 @@
return listMetadata.numCompleted + " of " + listMetadata.numTasks;
}
}
+
+ @Override
+ public String toString() {
+ return mName.getText() + " (" + mCompletedStatus.getText() + "), " + mTimeAgo.getText();
+ }
}
diff --git a/app/src/main/java/io/v/todos/persistence/MainPersistence.java b/app/src/main/java/io/v/todos/persistence/MainPersistence.java
index 0d3a5a3..d5ac712 100644
--- a/app/src/main/java/io/v/todos/persistence/MainPersistence.java
+++ b/app/src/main/java/io/v/todos/persistence/MainPersistence.java
@@ -10,5 +10,5 @@
public interface MainPersistence extends Persistence {
void addTodoList(ListSpec listSpec);
void deleteTodoList(String key);
- void completeAllTasks(ListMetadata listMetadata);
+ void setCompletion(ListMetadata listMetadata, boolean done);
}
diff --git a/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java b/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
index d6fe975..5ceaba7 100644
--- a/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
+++ b/app/src/mock/java/io/v/todos/persistence/PersistenceFactory.java
@@ -5,7 +5,6 @@
package io.v.todos.persistence;
import android.app.Activity;
-import android.content.Context;
import io.v.todos.model.ListMetadata;
import io.v.todos.model.ListSpec;
@@ -56,7 +55,7 @@
public void deleteTodoList(String key) {}
@Override
- public void completeAllTasks(ListMetadata listMetadata) {}
+ public void setCompletion(ListMetadata listMetadata, boolean done) {}
@Override
public void close() {}
diff --git a/app/src/syncbase/java/io/v/todos/persistence/PersistenceFactory.java b/app/src/syncbase/java/io/v/todos/persistence/PersistenceFactory.java
index 7960ed7..2134fd4 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/PersistenceFactory.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/PersistenceFactory.java
@@ -9,6 +9,7 @@
import io.v.impl.google.services.syncbase.SyncbaseServer;
import io.v.todos.model.ListMetadata;
import io.v.todos.persistence.syncbase.SyncbaseMain;
+import io.v.todos.persistence.syncbase.SyncbaseTodoList;
import io.v.v23.verror.VException;
public final class PersistenceFactory {
@@ -37,14 +38,15 @@
* used.
*/
public static boolean mightGetTodoListPersistenceBlock() {
- return false;
+ return !SyncbaseTodoList.isInitialized();
}
/**
* Instantiates a persistence object that can be used to manipulate a todo list.
*/
public static TodoListPersistence getTodoListPersistence(
- Activity activity, String key, TodoListListener listener) throws VException {
- throw new RuntimeException("Unsupported product flavor.");
+ Activity activity, String key, TodoListListener listener)
+ throws VException, SyncbaseServer.StartException {
+ return new SyncbaseTodoList(activity, key, listener);
}
}
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/MainListTracker.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/MainListTracker.java
index 744ee81..be1f568 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/MainListTracker.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/MainListTracker.java
@@ -41,7 +41,7 @@
private final Map<String, Boolean> mIsTaskCompleted = new HashMap<>();
private int mNumCompletedTasks;
- private boolean mFireUpdates;
+ private boolean mListExistsLocally;
public final ListenableFuture<Void> watchFuture;
@@ -108,10 +108,10 @@
ListMetadata listMetadata = getListMetadata();
Log.d(TAG, listMetadata.toString());
- if (mFireUpdates) {
+ if (mListExistsLocally) {
mListener.onItemUpdate(listMetadata);
} else {
- mFireUpdates = true;
+ mListExistsLocally = true;
mListener.onItemAdd(listMetadata);
}
}
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
index 14c1ddf..8d6b190 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
@@ -7,14 +7,14 @@
import android.app.Activity;
import android.util.Log;
-import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.UUID;
+import java.util.concurrent.Callable;
import javax.annotation.Nullable;
@@ -37,23 +37,14 @@
import io.v.v23.syncbase.RowRange;
import io.v.v23.syncbase.WatchChange;
import io.v.v23.vdl.VdlAny;
-import io.v.v23.verror.ExistException;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;
public class SyncbaseMain extends SyncbasePersistence implements MainPersistence {
private static final String
TAG = SyncbaseMain.class.getSimpleName(),
- MAIN_COLLECTION_NAME = "userdata",
LISTS_PREFIX = "lists_";
- private static final Object sMainCollectionMutex = new Object();
- private static volatile Collection sMainCollection;
-
- public static boolean isInitialized() {
- return sMainCollection != null;
- }
-
private final Map<String, MainListTracker> mTaskTrackers = new HashMap<>();
/**
@@ -63,21 +54,8 @@
throws VException, SyncbaseServer.StartException {
super(activity);
- synchronized (sMainCollectionMutex) {
- if (sMainCollection == null) {
- Collection mainCollection = getDatabase()
- .getCollection(mVContext, MAIN_COLLECTION_NAME);
- try {
- VFutures.sync(mainCollection.create(mVContext, null));
- } catch (ExistException e) {
- // This is fine.
- }
- sMainCollection = mainCollection;
- }
- }
-
InputChannel<WatchChange> watch = getDatabase().watch(
- mVContext, sMainCollection.id(), LISTS_PREFIX);
+ mVContext, getUserCollection().id(), LISTS_PREFIX);
trap(InputChannels.withCallback(watch, new InputChannelCallback<WatchChange>() {
@Override
public ListenableFuture<Void> onNext(WatchChange change) {
@@ -91,7 +69,7 @@
if (mTaskTrackers.put(listId, listTracker) != null) {
// List entries in the main collection are just ( list ID => nil ), so we
// never expect updates other than an initial add...
- Log.w(TAG, "Unexpected update to " + MAIN_COLLECTION_NAME + " collection " +
+ Log.w(TAG, "Unexpected update to " + USER_COLLECTION_NAME + " collection " +
"for list " + listId);
}
@@ -104,14 +82,14 @@
@Override
public void addTodoList(final ListSpec listSpec) {
- final String listName = LISTS_PREFIX + UUID.randomUUID().toString().replace('-', '_');
+ final String listName = LISTS_PREFIX + randomName();
final Collection listCollection = getDatabase().getCollection(mVContext, listName);
Futures.addCallback(listCollection.create(mVContext, null),
new TrappingCallback<Void>(mActivity) {
@Override
public void onSuccess(@Nullable Void result) {
// These can happen in either order
- trap(sMainCollection.put(mVContext, listName, null, VdlAny.class));
+ trap(getUserCollection().put(mVContext, listName, null, VdlAny.class));
trap(listCollection.put(mVContext, SyncbaseTodoList.LIST_ROW_NAME, listSpec,
ListSpec.class));
}
@@ -120,42 +98,41 @@
@Override
public void deleteTodoList(String key) {
- trap(sMainCollection.delete(mVContext, key));
+ trap(getUserCollection().delete(mVContext, key));
}
@Override
- public void completeAllTasks(ListMetadata listMetadata) {
+ public void setCompletion(ListMetadata listMetadata, final boolean done) {
final String listId = listMetadata.key;
trap(Batch.runInBatch(mVContext, getDatabase(), new BatchOptions(),
new Batch.BatchOperation() {
@Override
- public ListenableFuture<Void> run(BatchDatabase db) {
- final Collection list = db.getCollection(mVContext, listId);
+ public ListenableFuture<Void> run(final BatchDatabase db) {
+ return sExecutor.submit(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ final Collection list = db.getCollection(mVContext, listId);
- InputChannel<KeyValue> scan = list.scan(mVContext,
- RowRange.prefix(SyncbaseTodoList.TASKS_PREFIX));
- InputChannel<ListenableFuture<Void>> puts = InputChannels.transform(
- mVContext, scan, new InputChannels.TransformFunction<KeyValue,
- ListenableFuture<Void>>() {
- @Override
- public ListenableFuture<Void> apply(KeyValue kv)
- throws VException {
- TaskSpec taskSpec =
- (TaskSpec) VomUtil.decode(kv.getValue());
- taskSpec.setDone(true);
- return list.put(mVContext, kv.getKey(), taskSpec,
- TaskSpec.class);
- }
- });
+ InputChannel<KeyValue> scan = list.scan(mVContext,
+ RowRange.prefix(SyncbaseTodoList.TASKS_PREFIX));
- return Futures.transform(Futures.allAsList(InputChannels.asIterable(puts)),
- new Function<List<Void>, Void>() {
- @Nullable
- @Override
- public Void apply(@Nullable List<Void> input) {
- return null;
+ List<ListenableFuture<Void>> puts = new ArrayList<>();
+ for (KeyValue kv : InputChannels.asIterable(scan)) {
+ TaskSpec taskSpec = (TaskSpec) VomUtil.decode(kv.getValue());
+ if (taskSpec.getDone() != done) {
+ taskSpec.setDone(done);
+ puts.add(list.put(mVContext, kv.getKey(), taskSpec,
+ TaskSpec.class));
}
- });
+ }
+
+ if (!puts.isEmpty()) {
+ puts.add(SyncbaseTodoList.updateListTimestamp(mVContext, list));
+ }
+ VFutures.sync(Futures.allAsList(puts));
+ return null;
+ }
+ });
}
}));
}
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
index 75b9342..b2b0db9 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
@@ -24,6 +24,7 @@
import com.google.common.util.concurrent.SettableFuture;
import java.io.File;
+import java.util.UUID;
import java.util.concurrent.Executors;
import javax.annotation.Nullable;
@@ -41,6 +42,7 @@
import io.v.v23.security.access.AccessList;
import io.v.v23.security.access.Constants;
import io.v.v23.security.access.Permissions;
+import io.v.v23.syncbase.Collection;
import io.v.v23.syncbase.Database;
import io.v.v23.syncbase.Syncbase;
import io.v.v23.syncbase.SyncbaseService;
@@ -60,6 +62,8 @@
PROXY = "proxy",
DATABASE = "db",
BLESSINGS_KEY = "blessings";
+ public static final String
+ USER_COLLECTION_NAME = "userdata";
// BlessingPattern initialization has to be deferred until after V23 init due to native binding.
private static final Supplier<AccessList> OPEN_ACL = Suppliers.memoize(
new Supplier<AccessList>() {
@@ -107,7 +111,7 @@
* @throws io.v.impl.google.services.syncbase.SyncbaseServer.StartException if there was an
* error starting the syncbase service
*/
- private static SyncbaseService ensureSyncbaseStarted(Context androidContext)
+ private static void ensureSyncbaseStarted(Context androidContext)
throws SyncbaseServer.StartException {
synchronized (sSyncbaseMutex) {
if (sSyncbase == null) {
@@ -140,13 +144,12 @@
sVContext = singletonContext;
}
}
- return sSyncbase;
}
private static final Object sDatabaseMutex = new Object();
private static Database sDatabase;
- private static Database ensureDatabaseExists(Context androidContext) throws VException {
+ private static void ensureDatabaseExists() throws VException {
synchronized (sDatabaseMutex) {
if (sDatabase == null) {
final Database db = sSyncbase.getDatabase(sVContext, DATABASE, null);
@@ -159,7 +162,32 @@
sDatabase = db;
}
}
- return sDatabase;
+ }
+
+ private static final Object sUserCollectionMutex = new Object();
+ private static volatile Collection sUserCollection;
+
+ private static void ensureUserCollectionExists() throws VException {
+ synchronized (sUserCollectionMutex) {
+ if (sUserCollection == null) {
+ Collection userCollection = sDatabase.getCollection(sVContext,
+ USER_COLLECTION_NAME);
+ try {
+ VFutures.sync(userCollection.create(sVContext, null));
+ } catch (ExistException e) {
+ // This is fine.
+ }
+ sUserCollection = userCollection;
+ }
+ }
+ }
+
+ public static boolean isInitialized() {
+ return sUserCollection != null;
+ }
+
+ protected static String randomName() {
+ return UUID.randomUUID().toString().replace('-', '_');
}
/**
@@ -224,6 +252,10 @@
return sDatabase;
}
+ protected Collection getUserCollection() {
+ return sUserCollection;
+ }
+
/**
* @see TrappingCallback
*/
@@ -256,7 +288,8 @@
}
VFutures.sync(Futures.dereference(blessings));
ensureSyncbaseStarted(activity);
- ensureDatabaseExists(activity);
+ ensureDatabaseExists();
+ ensureUserCollectionExists();
}
@Override
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
index 56f56e9..49a4a14 100644
--- a/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
@@ -4,8 +4,158 @@
package io.v.todos.persistence.syncbase;
-public class SyncbaseTodoList {
+import android.app.Activity;
+import android.support.annotation.NonNull;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import io.v.impl.google.services.syncbase.SyncbaseServer;
+import io.v.todos.model.ListSpec;
+import io.v.todos.model.Task;
+import io.v.todos.model.TaskSpec;
+import io.v.todos.persistence.TodoListListener;
+import io.v.todos.persistence.TodoListPersistence;
+import io.v.v23.InputChannel;
+import io.v.v23.InputChannelCallback;
+import io.v.v23.InputChannels;
+import io.v.v23.context.VContext;
+import io.v.v23.syncbase.ChangeType;
+import io.v.v23.syncbase.Collection;
+import io.v.v23.syncbase.WatchChange;
+import io.v.v23.verror.NoExistException;
+import io.v.v23.verror.VException;
+
+public class SyncbaseTodoList extends SyncbasePersistence implements TodoListPersistence {
public static final String
LIST_ROW_NAME = "list",
TASKS_PREFIX = "tasks_";
+
+ private static final String
+ SHOW_DONE_ROW_NAME = "ShowDone";
+
+ private final Collection mList;
+ private final TodoListListener mListener;
+ private final Set<String> mTaskIds = new HashSet<>();
+
+ /**
+ * This assumes that the collection for this list already exists.
+ */
+ public SyncbaseTodoList(Activity activity, String listId, TodoListListener listener)
+ throws VException, SyncbaseServer.StartException {
+ super(activity);
+ mListener = listener;
+
+ mList = getDatabase().getCollection(mVContext, listId);
+ InputChannel<WatchChange> listWatch = getDatabase().watch(mVContext, mList.id(), "");
+ ListenableFuture<Void> listWatchFuture = InputChannels.withCallback(listWatch,
+ new InputChannelCallback<WatchChange>() {
+ @Override
+ public ListenableFuture<Void> onNext(WatchChange change) {
+ processWatchChange(change);
+ return null;
+ }
+ });
+ Futures.addCallback(listWatchFuture, new TrappingCallback<Void>(activity) {
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ if (t instanceof NoExistException) {
+ // The collection has been deleted.
+ mListener.onDelete();
+ } else {
+ super.onFailure(t);
+ }
+ }
+ });
+
+ // Watch the "showDone" boolean in the userdata collection and forward changes to the
+ // listener.
+ InputChannel<WatchChange> showDoneWatch = getDatabase()
+ .watch(mVContext, getUserCollection().id(), SHOW_DONE_ROW_NAME);
+ trap(InputChannels.withCallback(showDoneWatch, new InputChannelCallback<WatchChange>() {
+ @Override
+ public ListenableFuture<Void> onNext(WatchChange result) {
+ mListener.onUpdateShowDone((boolean)result.getValue());
+ return null;
+ }
+ }));
+ }
+
+ private void processWatchChange(WatchChange change) {
+ String rowName = change.getRowName();
+
+ if (rowName.equals(SyncbaseTodoList.LIST_ROW_NAME)) {
+ ListSpec listSpec = SyncbasePersistence.castWatchValue(change.getValue(),
+ ListSpec.class);
+ mListener.onUpdate(listSpec);
+ } else if (change.getChangeType() == ChangeType.DELETE_CHANGE) {
+ mTaskIds.remove(rowName);
+ mListener.onItemDelete(rowName);
+ } else {
+ TaskSpec taskSpec = SyncbasePersistence.castWatchValue(change.getValue(),
+ TaskSpec.class);
+ Task task = new Task(rowName, taskSpec);
+
+ if (mTaskIds.add(rowName)) {
+ mListener.onItemAdd(task);
+ } else {
+ mListener.onItemUpdate(task);
+ }
+ }
+ }
+
+ @Override
+ public void updateTodoList(ListSpec listSpec) {
+ trap(mList.put(mVContext, LIST_ROW_NAME, listSpec, ListSpec.class));
+ }
+
+ @Override
+ public void deleteTodoList() {
+ trap(getUserCollection().delete(mVContext, mList.id().getName()));
+ trap(mList.destroy(mVContext));
+ }
+
+ public static ListenableFuture<Void> updateListTimestamp(final VContext vContext,
+ final Collection list) {
+ ListenableFuture<Object> get = list.get(vContext, LIST_ROW_NAME, ListSpec.class);
+ return Futures.transform(get, new AsyncFunction<Object, Void>() {
+ @Override
+ public ListenableFuture<Void> apply(Object oldValue) throws Exception {
+ ListSpec listSpec = (ListSpec) oldValue;
+ listSpec.setUpdatedAt(System.currentTimeMillis());
+ return list.put(vContext, LIST_ROW_NAME, listSpec, ListSpec.class);
+ }
+ });
+ }
+
+ private void updateListTimestamp() {
+ trap(updateListTimestamp(mVContext, mList));
+ }
+
+ @Override
+ public void addTask(TaskSpec task) {
+ trap(mList.put(mVContext, TASKS_PREFIX + randomName(), task, TaskSpec.class));
+ updateListTimestamp();
+ }
+
+ @Override
+ public void updateTask(Task task) {
+ trap(mList.put(mVContext, task.key, task.toSpec(), TaskSpec.class));
+ updateListTimestamp();
+ }
+
+ @Override
+ public void deleteTask(String key) {
+ trap(mList.delete(mVContext, key));
+ updateListTimestamp();
+ }
+
+ @Override
+ public void setShowDone(boolean showDone) {
+ trap(getUserCollection().put(mVContext, SHOW_DONE_ROW_NAME, showDone, Boolean.TYPE));
+ }
}