blob: eb7f0a61c93fc2237835f9b3dfef1ed1a6fc52a4 [file] [log] [blame]
// Copyright 2016 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.todos.persistence.firebase;
import android.content.Context;
import com.firebase.client.ChildEventListener;
import com.firebase.client.DataSnapshot;
import com.firebase.client.Firebase;
import com.firebase.client.FirebaseError;
import com.firebase.client.MutableData;
import com.firebase.client.Transaction;
import java.util.HashMap;
import java.util.Map;
import io.v.todos.Task;
import io.v.todos.TodoList;
import io.v.todos.persistence.ListEventListener;
import io.v.todos.persistence.MainPersistence;
public class FirebaseMain extends FirebasePersistence implements MainPersistence {
public static final String TODO_LISTS = "snackoos (TodoList)";
private final Firebase mTodoLists;
private final ChildEventListener mTodoListsListener;
private final ListEventListener<TodoList> mListener;
private final Map<String, ChildEventListener> mTodoListTaskListeners;
private final Map<String, TodoListTasksListener> mTodoListTrackers;
public FirebaseMain(Context context, final ListEventListener<TodoList> listener) {
super(context);
mTodoLists = getFirebase().child(TODO_LISTS);
// This handler will forward events to the passed in listener after ensuring that all the
// data in the TodoList is set and can automatically update.
mTodoListsListener = mTodoLists.addChildEventListener(
new ChildEventListenerAdapter<>(TodoList.class, new ListEventListener<TodoList>() {
@Override
public void onItemAdd(TodoList item) {
// Hook up listeners for the # completed and # tasks. Then forward the item.
startWatchTodoListTasks(item);
mListener.onItemAdd(item);
}
@Override
public void onItemUpdate(TodoList item) {
// Retrieve # completed and # tasks. Then forward the item.
setTaskCompletion(item);
mListener.onItemUpdate(item);
}
@Override
public void onItemDelete(String key) {
// Remove listeners for the # completed and # tasks. Then forward the item.
stopWatchTodoListTasks(key);
mListener.onItemDelete(key);
}
}));
mListener = listener;
mTodoListTaskListeners = new HashMap<>();
mTodoListTrackers = new HashMap<>();
}
@Override
public void addTodoList(TodoList todoList) {
mTodoLists.push().setValue(todoList);
}
@Override
public void deleteTodoList(String key) {
mTodoLists.child(key).removeValue();
// After deleting the list itself, delete all the orphaned tasks!
Firebase tasksRef = getFirebase().child(FirebaseTodoList.TASKS).child(key);
tasksRef.removeValue();
}
@Override
public void completeAllTasks(final TodoList todoList) {
// Update all child tasks for this key to have done = true.
Firebase tasksRef = getFirebase().child(FirebaseTodoList.TASKS).child(todoList.getKey());
tasksRef.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData mutableData) {
// Note: This is very easy to make conflicts with. It may be better to avoid doing
// this in a batch or to split up the Task into components.
for (Task t : mTodoListTrackers.get(todoList.getKey()).mTasks.values()) {
Task tCopy = t.copy();
tCopy.setDone(true);
mutableData.child(t.getKey()).setValue(tCopy);
}
return Transaction.success(mutableData);
}
@Override
public void onComplete(FirebaseError firebaseError, boolean b, DataSnapshot dataSnapshot) {
}
});
// Further, update this todo list to set its last updated time.
mTodoLists.child(todoList.getKey()).setValue(new TodoList(todoList.getName()));
}
private void setTaskCompletion(TodoList todoList) {
TodoListTasksListener tracker = mTodoListTrackers.get(todoList.getKey());
tracker.swapTodoList(todoList);
}
private void startWatchTodoListTasks(final TodoList todoList) {
final String todoListKey = todoList.getKey();
Firebase taskRef = getFirebase().child(FirebaseTodoList.TASKS).child(todoListKey);
TodoListTasksListener tasksListener = new TodoListTasksListener(todoList);
ChildEventListener l = taskRef.addChildEventListener(
new ChildEventListenerAdapter<>(Task.class, tasksListener)
);
mTodoListTrackers.put(todoListKey, tasksListener);
mTodoListTaskListeners.put(todoListKey, l);
}
private void stopWatchTodoListTasks(String key) {
mTodoListTrackers.remove(key).disable(); // Disable; we don't want this listener anymore.
ChildEventListener l = mTodoListTaskListeners.remove(key);
getFirebase().removeEventListener(l);
}
@Override
public void close() {
getFirebase().removeEventListener(mTodoListsListener);
for (ChildEventListener listener : mTodoListTaskListeners.values()) {
getFirebase().removeEventListener(listener);
}
}
private class TodoListTasksListener implements ListEventListener<Task> {
TodoList mTodoList; // The list whose numCompleted and numTasks fields will be updated.
final Map<String, Task> mTasks;
boolean disabled = false;
TodoListTasksListener(TodoList todoList) {
mTodoList = todoList;
mTasks = new HashMap<>();
}
// Prevent this listener from propagating any more updates.
// Note: It looks like Firebase will continue firing listeners if they have more data, so
// call this if you absolutely don't need any more events to fire.
public void disable() {
disabled = true;
}
public void swapTodoList(TodoList otherList) {
if (disabled) {
return;
}
assert mTodoList.getKey() == otherList.getKey();
otherList.numCompleted = mTodoList.numCompleted;
otherList.numTasks = mTodoList.numTasks;
mTodoList = otherList;
}
@Override
public void onItemAdd(Task item) {
if (disabled) {
return;
}
mTodoList.numTasks++;
if (item.getDone()) {
mTodoList.numCompleted++;
}
mTasks.put(item.getKey(), item);
mListener.onItemUpdate(mTodoList);
}
@Override
public void onItemUpdate(Task item) {
if (disabled) {
return;
}
Task oldItem = mTasks.get(item.getKey());
mTasks.put(item.getKey(), item);
if (oldItem.getDone() != item.getDone()) {
if (item.getDone()) {
mTodoList.numCompleted++;
} else {
mTodoList.numCompleted--;
}
mListener.onItemUpdate(mTodoList);
}
}
@Override
public void onItemDelete(String key) {
if (disabled) {
return;
}
mTodoList.numTasks--;
Task t = mTasks.remove(key);
if (t.getDone()) {
mTodoList.numCompleted--;
}
mListener.onItemUpdate(mTodoList);
}
}
}