TODOs: 2.1.5 High-level API and Restore Sharing
Major changes:
- Uses the high-level API in a synchronous manner.
- Restores the Sharing assets.
- Sharing was tested, watch was tested.
- Login helper is being used.
- Ensure lollipop is ok by adding permissions to Manifest.
Notes:
- Still crashes randomly. Callbacks have a problem.
- Discovery isn't that great. Sometimes some peers don't see others.
- There are a lot of TODOs needed still! The HLAPI has some
catching up to do before I can finish them.
I would prefer to submit this bulk change sooner rather than later
though, or else it'll just get larger and larger.
Change-Id: Id8a8216fc5196cfcd37604829c07469bb5b16f94
diff --git a/app/build.gradle b/app/build.gradle
index 31ff599..85e4d95 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -83,5 +83,5 @@
)
firebaseCompile 'com.firebase:firebase-client-android:2.5.2'
syncbaseCompile 'io.v:vanadium-android:2.2.+'
- syncbase2Compile 'io.v:syncbase:0.1.4'
+ syncbase2Compile 'io.v:syncbase:0.1.5'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 274a641..8c96879 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,12 @@
package="io.v.todos">
<uses-permission android:name="android.permission.INTERNET" />
+ <!-- TODO(alexfandrianto): We should include these permissions in syncbase instead -->
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
+ android:maxSdkVersion="22" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
+ android:maxSdkVersion="22" />
+
<application
android:allowBackup="true"
diff --git a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
index aeeab28..5359f7e 100644
--- a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
+++ b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseMain.java
@@ -6,12 +6,15 @@
import android.app.Activity;
import android.os.Bundle;
+import android.util.Log;
import java.util.UUID;
import io.v.syncbase.Collection;
import io.v.syncbase.DatabaseHandle;
import io.v.syncbase.Id;
+import io.v.syncbase.Syncbase;
+import io.v.syncbase.core.VError;
import io.v.todos.model.ListMetadata;
import io.v.todos.model.ListSpec;
import io.v.todos.persistence.ListEventListener;
@@ -35,27 +38,37 @@
@Override
public String addTodoList(ListSpec listSpec) {
- // TODO(alexfandrianto): We do want to create this with a syncgroup, but even if we set
- // the flag to off, it takes too long to create (and put) on the UI thread. To work around
- // this, we might mock the encoded Id synchronously and then do creation/put asynchronously.
DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions();
- opts.withoutSyncgroup = true;
- Collection c = mDb.collection(UUID.randomUUID().toString(), opts);
- c.put(TODO_LIST_KEY, listSpec);
- return c.getId().encode();
+ try {
+ // TODO(alexfandrianto): We're not allowed to have dashes in our collection names still!
+ // You also must start with a letter, not a number.
+ Collection c = sDb.collection("list_" + UUID.randomUUID().toString().replaceAll("-", ""), opts);
+ c.put(TODO_LIST_KEY, listSpec);
+ return c.getId().encode();
+ } catch (VError vError) {
+ Log.e(TAG, "Failed to create todo list collection", vError);
+ throw new RuntimeException(vError);
+ }
}
@Override
public void deleteTodoList(String key) {
Id listId = Id.decode(key);
- Collection c = mDb.getCollection(listId);
- c.delete(TODO_LIST_KEY);
- // TODO(alexfandrianto): Instead of deleting the key, should we destroy the collection?
+ Collection c = sDb.getCollection(listId);
+ try {
+ c.delete(TODO_LIST_KEY);
+ } catch (VError vError) {
+ Log.e(TAG, "Failed to delete todo list key", vError);
+ }
+ // TODO(alexfandrianto): Instead of deleting the key, we should destroy the collection.
+ // Unfortunately, I can't yet: https://v.io/i/1374
}
@Override
public void close() {
removeMainListener();
- super.close();
+ if (isInitialized()) {
+ Syncbase.shutdown();
+ }
}
}
diff --git a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
index 475054e..72affe4 100644
--- a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
+++ b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbasePersistence.java
@@ -5,26 +5,27 @@
package io.v.todos.persistence.syncbase;
import android.app.Activity;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
import android.os.Bundle;
-import android.os.Handler;
+import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
import android.util.Log;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.SettableFuture;
-
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
-import io.v.android.VAndroidContexts;
-import io.v.android.security.BlessingsManager;
import io.v.syncbase.Collection;
import io.v.syncbase.Database;
import io.v.syncbase.Syncbase;
import io.v.syncbase.Id;
+import io.v.syncbase.Syncgroup;
+import io.v.syncbase.SyncgroupInvite;
import io.v.syncbase.WatchChange;
+import io.v.syncbase.core.VError;
import io.v.todos.model.ListMetadata;
import io.v.todos.model.ListSpec;
import io.v.todos.model.Task;
@@ -32,19 +33,15 @@
import io.v.todos.persistence.ListEventListener;
import io.v.todos.persistence.Persistence;
import io.v.todos.persistence.TodoListListener;
-import io.v.v23.VFutures;
-import io.v.v23.context.VContext;
-import io.v.v23.security.Blessings;
-import io.v.v23.verror.VException;
+import io.v.todos.sharing.NeighborhoodFragment;
+import io.v.todos.sharing.ShareListDialogFragment;
-public class SyncbasePersistence implements Persistence {
+public abstract class SyncbasePersistence implements Persistence {
protected static final String SETTINGS_COLLECTION = "settings";
protected static final String SHOW_DONE_KEY = "showDoneKey";
protected static final String TODO_LIST_KEY = "todoListKey";
protected static final String TAG = "High-Level Syncbase";
- private static final String BLESSINGS_KEY = "blessings";
-
protected static boolean sInitialized = false;
protected static final Map<Id, ListSpec> sListSpecMap = new HashMap<>();
@@ -52,14 +49,16 @@
protected static final Map<Id, Map<String, TaskSpec>> sTasksByListMap = new HashMap<>();
protected static boolean sShowDone = true;
- protected Database mDb;
- protected Collection mSettings;
+ protected static Database sDb;
+ protected static Collection sSettings;
private static final Object sSyncbaseMutex = new Object();
- private TodoListListener mTodoListListener;
- private ListEventListener<ListMetadata> mMainListener;
+ private static TodoListListener sTodoListListener;
+ private static Id sTodoListExpectedId;
+ private static ListEventListener<ListMetadata> sMainListener;
- public SyncbasePersistence(final Activity activity, Bundle savedInstanceState) {
+ SyncbasePersistence(final Activity activity, Bundle savedInstanceState) {
+ Log.d(TAG, "Trying to start Syncbase Persistence...");
/**
* Initializes Syncbase Server
* Starts up a watch stream to watch all the data with methods to access/modify the data.
@@ -70,67 +69,54 @@
if (!sInitialized) {
Log.d(TAG, "Initializing Syncbase Persistence...");
- Syncbase.DatabaseOptions dbOpts = new Syncbase.DatabaseOptions();
- dbOpts.rootDir = activity.getFilesDir().getAbsolutePath();
- dbOpts.disableUserdataSyncgroup = true;
- dbOpts.vContext = VAndroidContexts.withDefaults(activity,
- savedInstanceState).getVContext();
+ Syncbase.Options opts = new Syncbase.Options();
+ opts.rootDir = activity.getFilesDir().getAbsolutePath();
+ opts.disableSyncgroupPublishing = true;
+ // TODO(alexfandrianto): https://v.io/i/1375
+ opts.disableUserdataSyncgroup = true;
+ try {
+ Syncbase.init(opts);
+ } catch (VError vError) {
+ Log.e(TAG, "Failed to initialize", vError);
+ return;
+ }
- final VContext vContext = dbOpts.vContext;
+ final Object initializeMutex = new Object();
- Log.d(TAG, "Done getting vanadium context!");
-
- final SettableFuture<ListenableFuture<Blessings>> blessings =
- SettableFuture.create();
- if (activity.getMainLooper().getThread() == Thread.currentThread()) {
- blessings.set(BlessingsManager.getBlessings(vContext, activity,
- BLESSINGS_KEY, true));
- } else {
- new Handler(activity.getMainLooper()).post(new Runnable() {
+ Log.d(TAG, "Logging the user in!");
+ if (!Syncbase.isLoggedIn()) {
+ Syncbase.loginAndroid(activity, new Syncbase.LoginCallback() {
@Override
- public void run() {
- blessings.set(BlessingsManager.getBlessings(vContext,
- activity, BLESSINGS_KEY, true));
+ public void onSuccess() {
+ Log.d(TAG, "Successfully logged in!");
+ try {
+ sDb = Syncbase.database();
+ } catch (VError vError) {
+ Log.e(TAG, "Failed to create database", vError);
+ callNotify();
+ return;
+ }
+ continueSetup();
+ sInitialized = true;
+ Log.d(TAG, "Successfully initialized!");
+ callNotify();
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ Log.e(TAG, "Failed to login. :(", e);
+ callNotify();
+ }
+
+ private void callNotify() {
+ synchronized (initializeMutex) {
+ initializeMutex.notify();
+ }
}
});
}
- try {
- VFutures.sync(Futures.dereference(blessings));
- } catch (VException e) {
- Log.e(TAG, "Failed to get blessings", e);
- }
-
- Log.d(TAG, "Done getting blessings!");
-
- final Object initializeMutex = new Object();
-
- Syncbase.database(new Syncbase.DatabaseCallback() {
- @Override
- public void onSuccess(Database db) {
- super.onSuccess(db);
- Log.d(TAG, "Got a db handle!");
- mDb = db;
- continueSetup();
- sInitialized = true;
- Log.d(TAG, "Successfully initialized!");
- synchronized (initializeMutex) {
- initializeMutex.notify();
- }
- }
-
- @Override
- public void onError(Throwable e) {
- super.onError(e);
-
- Log.e(TAG, "Failed to get database handle", e);
- synchronized (initializeMutex) {
- initializeMutex.notify();
- }
- }
- }, dbOpts);
-
- Log.d(TAG, "Let's wait until the database is ready...");
+ Log.d(TAG, "Let's wait until we are logged in...");
synchronized (initializeMutex) {
try {
initializeMutex.wait();
@@ -139,28 +125,66 @@
}
}
- Log.d(TAG, "Syncbase Persistence initialization complete!");
+ if (sInitialized) {
+ Log.d(TAG, "Syncbase Persistence initialization complete!");
+ } else {
+ Log.d(TAG, "Syncbase Persistence initialization FAILED!");
+ return;
+ }
}
}
+
+ // Prepare the share presence menu fragment.
+ FragmentManager mgr = activity.getFragmentManager();
+ if (savedInstanceState == null) {
+ FragmentTransaction t = mgr.beginTransaction();
+ addFeatureFragments(mgr, activity, t);
+ t.commit();
+ } else {
+ addFeatureFragments(mgr, activity, null);
+ }
+ }
+
+ /**
+ * Hook to insert or rebind fragments.
+ *
+ * @param manager
+ * @param transaction the fragment transaction to use to add fragments, or null if fragments are
+ * being restored by the system.
+ */
+ @CallSuper
+ protected void addFeatureFragments(FragmentManager manager, Context context,
+ @Nullable FragmentTransaction transaction) {
+ if (transaction != null) {
+ NeighborhoodFragment fragment = new NeighborhoodFragment();
+ fragment.initSharePresence(context);
+ transaction.add(fragment, NeighborhoodFragment.FRAGMENT_TAG);
+ }
}
private void continueSetup() {
Log.d(TAG, "Creating settings collection");
// Create a settings collection.
- mSettings = mDb.collection(SETTINGS_COLLECTION);
+ try {
+ sSettings = sDb.collection(SETTINGS_COLLECTION);
+ } catch (VError vError) {
+ Log.e(TAG, "couldn't create settings collection", vError);
+ }
Log.d(TAG, "Watching everything");
// Watch everything.
- mDb.addWatchChangeHandler(new Database.WatchChangeHandler() {
+ // TODO(alexfandrianto): This can be simplified if we watch specific collections and the
+ // entrance/exit of collections. https://v.io/i/1376
+ sDb.addWatchChangeHandler(new Database.WatchChangeHandler() {
@Override
- public void onInitialState(Iterator<WatchChange> values) {
+ public void onInitialState(final Iterator<WatchChange> values) {
while (values.hasNext()) {
handlePutChange(values.next());
}
}
@Override
- public void onChangeBatch(Iterator<WatchChange> changes) {
+ public void onChangeBatch(final Iterator<WatchChange> changes) {
while (changes.hasNext()) {
WatchChange change = changes.next();
if (change.getChangeType() == WatchChange.ChangeType.DELETE) {
@@ -175,56 +199,84 @@
// being within a batch. Over-firing the listeners isn't ideal, but the app should be
// okay.
private void handlePutChange(WatchChange value) {
- Id collectionId = value.getCollectionId();
+ Log.d(TAG, "Handling put change " + value.getRowKey());
+ Log.d(TAG, "From collection: " + value.getCollectionId());
+ Log.d(TAG, "With entity type: " + value.getEntityType());
+ if (value.getEntityType() != WatchChange.EntityType.ROW ||
+ value.getCollectionId().getName().equals("userdata__")) {
+ // TODO(alexfandrianto): I can't deal with these yet. Please skip to avoid crashing.
+ // TODO(alexfandrianto): export/hide userdata__ https://v.io/i/1372
+ return;
+ }
+ Log.d(TAG, "With row...: " + value.getRowKey());
+ final Id collectionId = value.getCollectionId();
if (collectionId.getName().equals(SETTINGS_COLLECTION)) {
if (value.getRowKey().equals(SHOW_DONE_KEY)) {
- sShowDone = (Boolean)value.getValue();
+ try {
+ sShowDone = value.getValue(Boolean.class);
+ Log.d(TAG, "Got a show done" + sShowDone);
- // Inform the relevant listener.
- if (mTodoListListener != null) {
- mTodoListListener.onUpdateShowDone(sShowDone);
+ // Inform the relevant listener.
+ if (sTodoListListener != null) {
+ sTodoListListener.onUpdateShowDone(sShowDone);
+ }
+ } catch (VError vError) {
+ Log.e(TAG, "Failed to decode watch change as Boolean", vError);
}
}
+ return; // Show done updated. Nothing left to do.
}
+ // If we are here, we must be modifying a todo list collection.
// Initialize the task spec map, if necessary.
if (sTasksByListMap.get(collectionId) == null) {
sTasksByListMap.put(collectionId, new HashMap<String, TaskSpec>());
}
if (value.getRowKey().equals(TODO_LIST_KEY)) {
- ListSpec listSpec = (ListSpec) value.getValue();
- sListSpecMap.put(collectionId, listSpec);
+ try {
+ final ListSpec listSpec = value.getValue(ListSpec.class);
+ Log.d(TAG, "Got a list" + listSpec.toString());
+ sListSpecMap.put(collectionId, listSpec);
- ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
- tracker.setSpec(listSpec);
+ final ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
+ tracker.setSpec(listSpec);
- // Inform the relevant listeners.
- if (mMainListener != null) {
- tracker.fireListener(mMainListener);
- }
- if (mTodoListListener != null) {
- mTodoListListener.onUpdate(listSpec);
+ // Inform the relevant listeners.
+ if (sMainListener != null) {
+ tracker.fireListener(sMainListener);
+ }
+ if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
+ sTodoListListener.onUpdate(listSpec);
+ }
+ } catch (VError vError) {
+ Log.e(TAG, "Failed to decode watch change value as ListSpec", vError);
}
} else {
Map<String, TaskSpec> taskData = sTasksByListMap.get(collectionId);
- TaskSpec newSpec = (TaskSpec)value.getValue();
- TaskSpec oldSpec = taskData.put(value.getRowKey(), newSpec);
+ final String rowKey = value.getRowKey();
+ try {
+ final TaskSpec newSpec = value.getValue(TaskSpec.class);
+ Log.d(TAG, "Got a task" + newSpec.toString());
+ final TaskSpec oldSpec = taskData.put(rowKey, newSpec);
- ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
- tracker.adjustTask(value.getRowKey(), newSpec.getDone());
+ final ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
+ tracker.adjustTask(rowKey, newSpec.getDone());
- // Inform the relevant listeners.
- if (mMainListener != null) {
- tracker.fireListener(mMainListener);
- }
- if (mTodoListListener != null) {
- if (oldSpec == null) {
- mTodoListListener.onItemAdd(new Task(value.getRowKey(), newSpec));
- } else {
- mTodoListListener.onItemUpdate(new Task(value.getRowKey(), newSpec));
+ // Inform the relevant listeners.
+ if (sMainListener != null) {
+ tracker.fireListener(sMainListener);
}
+ if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
+ if (oldSpec == null) {
+ sTodoListListener.onItemAdd(new Task(rowKey, newSpec));
+ } else {
+ sTodoListListener.onItemUpdate(new Task(rowKey, newSpec));
+ }
+ }
+ } catch (VError vError) {
+ Log.e(TAG, "Failed to decode watch change value as TaskSpec", vError);
}
}
}
@@ -233,35 +285,44 @@
// being within a batch. Over-firing the listeners isn't ideal, but the app should be
// okay.
private void handleDeleteChange(WatchChange value) {
- Id collectionId = value.getCollectionId();
- String oldKey = value.getRowKey();
+ Log.d(TAG, "Handling delete change " + value.getRowKey());
+ Log.d(TAG, "From collection: " + value.getCollectionId());
+ Log.d(TAG, "With entity type: " + value.getEntityType());
+ if (value.getEntityType() != WatchChange.EntityType.ROW || value.getCollectionId().getName().equals("userdata__")) {
+ // TODO(alexfandrianto): I can't deal with these yet. Please skip to avoid crashing.
+ // TODO(alexfandrianto): export/hide userdata__ https://v.io/i/1372
+ return;
+ }
+ Log.d(TAG, "With row...: " + value.getRowKey());
+
+ final Id collectionId = value.getCollectionId();
+ final String oldKey = value.getRowKey();
if (oldKey.equals(TODO_LIST_KEY)) {
sListSpecMap.remove(collectionId);
sListMetadataTrackerMap.remove(collectionId);
// TODO(alexfandrianto): Potentially destroy the collection too?
-
// Inform the relevant listeners.
- if (mMainListener != null) {
- mMainListener.onItemDelete(oldKey);
+ if (sMainListener != null) {
+ sMainListener.onItemDelete(collectionId.encode());
}
- if (mTodoListListener != null) {
- mTodoListListener.onDelete();
+ if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
+ sTodoListListener.onDelete();
}
} else {
Map<String, TaskSpec> tasks = sTasksByListMap.get(collectionId);
if (tasks != null) {
tasks.remove(oldKey);
- ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
+ final ListMetadataTracker tracker = getListMetadataTrackerSafe(collectionId);
tracker.removeTask(oldKey);
// Inform the relevant listeners.
- if (mMainListener != null) {
- tracker.fireListener(mMainListener);
+ if (sMainListener != null) {
+ tracker.fireListener(sMainListener);
}
- if (mTodoListListener != null) {
- mTodoListListener.onItemDelete(value.getRowKey());
+ if (sTodoListListener != null && sTodoListExpectedId.equals(collectionId)) {
+ sTodoListListener.onItemDelete(oldKey);
}
}
}
@@ -276,20 +337,17 @@
Log.d(TAG, "Accepting all invitations");
// Automatically accept invitations.
- // TODO(alexfandrianto): Uncomment. This part of the high-level API isn't implemented yet.
- /*mDb.addSyncgroupInviteHandler(new Database.SyncgroupInviteHandler() {
+ sDb.addSyncgroupInviteHandler(new Database.SyncgroupInviteHandler() {
@Override
public void onInvite(SyncgroupInvite invite) {
- mDb.acceptSyncgroupInvite(invite, new Database.AcceptSyncgroupInviteCallback() {
+ sDb.acceptSyncgroupInvite(invite, new Database.AcceptSyncgroupInviteCallback() {
@Override
public void onSuccess(Syncgroup sg) {
- super.onSuccess(sg);
Log.d(TAG, "Successfully joined syncgroup: " + sg.getId().toString());
}
@Override
public void onFailure(Throwable e) {
- super.onFailure(e);
Log.w(TAG, "Failed to accept invitation", e);
}
});
@@ -299,7 +357,10 @@
public void onError(Throwable e) {
Log.w(TAG, "error while handling invitations", e);
}
- }, new Database.AddSyncgroupInviteHandlerOptions());*/
+ }, new Database.AddSyncgroupInviteHandlerOptions());
+
+ // And do a background scan for peers near me.
+ ShareListDialogFragment.initScan();
}
public static boolean isInitialized() {
@@ -307,9 +368,7 @@
}
@Override
- public void close() {
-
- }
+ public abstract void close();
@Override
public String debugDetails() {
@@ -317,17 +376,18 @@
}
protected void setMainListener(ListEventListener<ListMetadata> listener) {
- mMainListener = listener;
+ sMainListener = listener;
}
protected void removeMainListener() {
- mMainListener = null;
+ sMainListener = null;
}
- protected void setTodoListListener(TodoListListener listener) {
- mTodoListListener = listener;
+ protected void setTodoListListener(TodoListListener listener, Id expectedId) {
+ sTodoListListener = listener;
+ sTodoListExpectedId = expectedId;
}
protected void removeTodoListListener() {
- mTodoListListener = null;
+ sTodoListListener = null;
}
private ListMetadataTracker getListMetadataTrackerSafe(Id listId) {
@@ -351,6 +411,9 @@
}
ListMetadata computeListMetadata() {
+ if (spec == null) {
+ return null;
+ }
return new ListMetadata(collectionId.encode(), spec, numCompleted,
taskCompletion.size());
}
@@ -376,10 +439,14 @@
}
void fireListener(ListEventListener<ListMetadata> listener) {
+ ListMetadata metadata = computeListMetadata();
+ if (metadata == null) {
+ return; // cannot fire yet
+ }
if (!hasFired) {
+ hasFired = true;
listener.onItemAdd(computeListMetadata());
} else {
- hasFired = true;
listener.onItemUpdate(computeListMetadata());
}
}
diff --git a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
index ba6aa17..556ecda 100644
--- a/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
+++ b/app/src/syncbase2/java/io/v/todos/persistence/syncbase/SyncbaseTodoList.java
@@ -5,30 +5,57 @@
package io.v.todos.persistence.syncbase;
import android.app.Activity;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
import android.os.Bundle;
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
+import io.v.syncbase.AccessList;
import io.v.syncbase.BatchDatabase;
import io.v.syncbase.Collection;
import io.v.syncbase.Database;
import io.v.syncbase.Id;
+import io.v.syncbase.User;
+import io.v.syncbase.core.VError;
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.todos.sharing.ShareListMenuFragment;
public class SyncbaseTodoList extends SyncbasePersistence implements TodoListPersistence {
private Collection mCollection;
+ private ShareListMenuFragment mShareListMenuFragment;
+
+ @Override
+ protected void addFeatureFragments(FragmentManager manager, Context context,
+ FragmentTransaction transaction) {
+ super.addFeatureFragments(manager, context, transaction);
+ if (transaction == null) {
+ mShareListMenuFragment = ShareListMenuFragment.find(manager);
+ } else {
+ mShareListMenuFragment = new ShareListMenuFragment();
+ transaction.add(mShareListMenuFragment, ShareListMenuFragment.FRAGMENT_TAG);
+ }
+ mShareListMenuFragment.persistence = this;
+ // TODO(alexfandrianto): I shouldn't show the sharing menu item when this person cannot
+ // share the todo list with other people. (Cannot re-share in this app.)
+ }
public SyncbaseTodoList(Activity activity, Bundle savedInstanceState, String key,
TodoListListener listener) {
super(activity, savedInstanceState);
Id listId = Id.decode(key);
- mCollection = mDb.getCollection(listId);
+ mCollection = sDb.getCollection(listId);
// Fire the listener for existing data (list, tasks, show done status).
ListSpec currentList = sListSpecMap.get(listId);
@@ -44,59 +71,114 @@
listener.onUpdateShowDone(sShowDone);
// Register the listener for future updates.
- setTodoListListener(listener);
+ setTodoListListener(listener, listId);
+
+ // TODO(alexfandrianto): Do we want this behavior? We need getLoggedInUser() if we do.
+ // if (!listId.getBlessing().equals(getPersonalBlessingsString())) {
+ // mShareListMenuFragment.hideShareMenuItem();
+ // }
+
+ // TODO(alexfandrianto): We also have to watch who the collection has been shared to!
+ // mShareListMenuFragment.setSharedTo needs to happen!!!
}
@Override
public void updateTodoList(ListSpec listSpec) {
- mCollection.put(TODO_LIST_KEY, listSpec);
+ try {
+ mCollection.put(TODO_LIST_KEY, listSpec);
+ } catch (VError vError) {
+ Log.w(TAG, vError);
+ }
}
@Override
public void deleteTodoList() {
- mCollection.delete(TODO_LIST_KEY);
+ try {
+ mCollection.delete(TODO_LIST_KEY);
+ } catch (VError vError) {
+ Log.w(TAG, vError);
+ }
}
@Override
public void completeTodoList() {
- mDb.runInBatch(new Database.BatchOperation() {
- @Override
- public void run(BatchDatabase bDb) {
- Collection bCollection = bDb.getCollection(mCollection.getId());
- Map<String, TaskSpec> curTasks = sTasksByListMap.get(mCollection.getId());
- for (Map.Entry<String, TaskSpec> entry : curTasks.entrySet()) {
- String rowKey = entry.getKey();
- TaskSpec curSpec= entry.getValue();
- TaskSpec newSpec = new TaskSpec(curSpec.getText(), curSpec.getAddedAt(), true);
- bCollection.put(rowKey, newSpec);
+ // TODO(alexfandrianto): All this try catch is getting excessive.
+ try {
+ sDb.runInBatch(new Database.BatchOperation() {
+ @Override
+ public void run(BatchDatabase bDb) {
+ Collection bCollection = bDb.getCollection(mCollection.getId());
+ Map<String, TaskSpec> curTasks = sTasksByListMap.get(mCollection.getId());
+ for (Map.Entry<String, TaskSpec> entry : curTasks.entrySet()) {
+ String rowKey = entry.getKey();
+ TaskSpec curSpec = entry.getValue();
+ TaskSpec newSpec = new TaskSpec(curSpec.getText(), curSpec.getAddedAt(),
+ true);
+
+ // TODO(alexfandrianto): If we're in a batch, it's okay to error, isn't it?
+ try {
+ bCollection.put(rowKey, newSpec);
+ } catch (VError vError) {
+ Log.w(TAG, vError);
+ }
+ }
}
- }
- }, new Database.BatchOptions());
+ }, new Database.BatchOptions());
+ } catch (VError vError) {
+ Log.w(TAG, vError);
+ }
}
@Override
public void addTask(TaskSpec task) {
- mCollection.put(UUID.randomUUID().toString(), task);
+ try {
+ mCollection.put(UUID.randomUUID().toString(), task);
+ } catch (VError vError) {
+ Log.w(TAG, vError);
+ }
}
@Override
public void updateTask(Task task) {
- mCollection.put(task.key, task.toSpec());
+ try {
+ mCollection.put(task.key, task.toSpec());
+ } catch (VError vError) {
+ Log.w(TAG, vError);
+ }
}
@Override
public void deleteTask(String key) {
- mCollection.delete(key);
+ try {
+ mCollection.delete(key);
+ } catch (VError vError) {
+ Log.w(TAG, vError);
+ }
}
@Override
public void setShowDone(boolean showDone) {
- mSettings.put(SHOW_DONE_KEY, showDone);
+ try {
+ sSettings.put(SHOW_DONE_KEY, showDone);
+ } catch (VError vError) {
+ Log.w(TAG, vError);
+ }
}
@Override
public void close() {
removeTodoListListener();
- super.close();
+ }
+
+ public void shareTodoList(Set<String> aliases) {
+ List<User> users = new ArrayList<User>();
+ for (String alias : aliases) {
+ users.add(new User(alias));
+ }
+ try {
+ mCollection.getSyncgroup().inviteUsers(users, AccessList.AccessLevel.READ_WRITE);
+ } catch (VError vError) {
+ Log.w(TAG, "Could not share to: " + users.toString(), vError);
+ }
}
}
diff --git a/app/src/syncbase2/java/io/v/todos/sharing/ContactAdapter.java b/app/src/syncbase2/java/io/v/todos/sharing/ContactAdapter.java
new file mode 100644
index 0000000..2bab5c9
--- /dev/null
+++ b/app/src/syncbase2/java/io/v/todos/sharing/ContactAdapter.java
@@ -0,0 +1,311 @@
+// 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.sharing;
+
+import android.support.annotation.StringRes;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import io.v.todos.R;
+
+public class ContactAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+ public static final int
+ VIEW_TYPE_SUBHEADER = 0,
+ VIEW_TYPE_CONTACT = 1;
+
+ public interface ContactTouchListener {
+ void onContactTouch(RecyclerView.ViewHolder viewHolder);
+ }
+
+ private static class SubheaderViewHolder extends RecyclerView.ViewHolder {
+ public final TextView category;
+
+ public SubheaderViewHolder(ViewGroup parent) {
+ super(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.sharing_subheader, parent, false));
+ category = (TextView) itemView.findViewById(R.id.category);
+ }
+ }
+
+ private static class ContactViewHolder extends RecyclerView.ViewHolder {
+ public final TextView name;
+
+ public ContactViewHolder(ViewGroup parent) {
+ super(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.sharing_entry, parent, false));
+ name = (TextView) itemView.findViewById(R.id.name);
+ }
+ }
+
+ // TODO(rosswang): Save expanded state, and/or manage more intelligently.
+ // TODO(rosswang): Potentially show condensed avatars in collapsed state.
+ private static class Sublist {
+ @StringRes
+ public final int header;
+ public final List<String> items = new ArrayList<>();
+
+ public Sublist(@StringRes int header) {
+ this.header = header;
+ }
+
+ public int getEffectiveSize() {
+ return 1 + items.size();
+ }
+ }
+
+ private final Sublist
+ mSharingAlready = new Sublist(R.string.sharing_already),
+ mSharingPossible = new Sublist(R.string.sharing_possible);
+
+ private final Sublist[] mSections = new Sublist[]{
+ mSharingAlready,
+ mSharingPossible
+ };
+
+ private Collection<String> mSharedAlready;
+ private final Set<String> mSharesAdded, mSharesRemoved, mSharesRecent;
+ private final Multiset<String> mDiscoCounter = HashMultiset.create();
+ private ContactTouchListener mContactTouchListener;
+
+ public ContactAdapter(Collection<String> sharedAlready, Set<String> sharesAdded,
+ Set<String> sharesRemoved, Set<String> sharesRecent) {
+ mSharesAdded = sharesAdded;
+ mSharesRemoved = sharesRemoved;
+ mSharesRecent = sharesRecent;
+
+ Set<String> recentUnaccountedFor = new HashSet<>(sharesRecent);
+ recentUnaccountedFor.removeAll(sharedAlready);
+ recentUnaccountedFor.removeAll(sharesAdded);
+ recentUnaccountedFor.removeAll(sharesRemoved);
+ mSharingPossible.items.addAll(recentUnaccountedFor);
+ mSharingPossible.items.addAll(sharesRemoved);
+ Collections.sort(mSharingPossible.items);
+
+ setSharedAlreadyData(sharedAlready);
+ }
+
+ private void setSharedAlreadyData(Collection<String> sharedAlready) {
+ mSharedAlready = sharedAlready;
+ mSharingAlready.items.clear();
+ mSharingAlready.items.addAll(sharedAlready);
+ mSharingAlready.items.removeAll(mSharesRemoved);
+ mSharingAlready.items.addAll(mSharesAdded);
+ Collections.sort(mSharingAlready.items);
+ }
+
+ public void filterDeltas() {
+ mSharesAdded.removeAll(mSharedAlready);
+ mSharesRemoved.retainAll(mSharedAlready);
+ }
+
+ private static class SublistEntry {
+ public final Sublist sublist;
+ public final int itemPosition;
+ public final String item;
+
+ public SublistEntry(Sublist sublist, int itemPosition, String item) {
+ this.sublist = sublist;
+ this.itemPosition = itemPosition;
+ this.item = item;
+ }
+ }
+
+ private SublistEntry getSublistItemPosition(int position) {
+ for (Sublist section : mSections) {
+ if (position == 0) {
+ return new SublistEntry(section, -1, null);
+ }
+ position--;
+ if (position < section.items.size()) {
+ return new SublistEntry(section, position, section.items.get(position));
+ }
+ position -= section.items.size();
+ }
+ throw new IndexOutOfBoundsException("No sublist at position " + position);
+ }
+
+ /**
+ * Inverse of {@link #getSublistItemPosition(int)}.
+ */
+ private int getViewPosition(Sublist section, int itemPosition) {
+ int offset = 1;
+ for (Sublist cursor: mSections) {
+ if (cursor == section) {
+ return offset + itemPosition;
+ } else {
+ offset += cursor.getEffectiveSize();
+ }
+ }
+ throw new NoSuchElementException("Section is not in list");
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ if (viewType == VIEW_TYPE_SUBHEADER) {
+ return new SubheaderViewHolder(parent);
+ } else {
+ final ContactViewHolder cvh = new ContactViewHolder(parent);
+ cvh.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mContactTouchListener.onContactTouch(cvh);
+ }
+ });
+ return cvh;
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
+ final SublistEntry entry = getSublistItemPosition(position);
+ if (entry.item == null) {
+ final SubheaderViewHolder svh = (SubheaderViewHolder) holder;
+ svh.category.setText(entry.sublist.header);
+ } else {
+ final ContactViewHolder cvh = (ContactViewHolder) holder;
+ cvh.name.setText(entry.item);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ int count = 0;
+ for (Sublist section : mSections) {
+ count += section.getEffectiveSize();
+ }
+ return count;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return getSublistItemPosition(position).item == null ?
+ VIEW_TYPE_SUBHEADER : VIEW_TYPE_CONTACT;
+ }
+
+ public void setContactTouchListener(ContactTouchListener listener) {
+ mContactTouchListener = listener;
+ }
+
+ public void onNearbyDeviceLost(String email) {
+ // short-circuit note; remove must happen
+ if (mDiscoCounter.remove(email, 1) > 1 ||
+ Collections.binarySearch(mSharingAlready.items, email) >= 0) {
+ return;
+ }
+ int i = Collections.binarySearch(mSharingPossible.items, email);
+ if (i >= 0) {
+ mSharingPossible.items.remove(i);
+ notifyItemRemoved(getViewPosition(mSharingPossible, i));
+ }
+ }
+
+ public void onNearbyDeviceDiscovered(String email) {
+ // short-circuit note; add must happen
+ if (mDiscoCounter.add(email, 1) > 0 ||
+ Collections.binarySearch(mSharingAlready.items, email) >= 0) {
+ return;
+ }
+ int i = Collections.binarySearch(mSharingPossible.items, email);
+ if (i < 0) {
+ i = ~i;
+ mSharingPossible.items.add(i, email);
+ notifyItemInserted(getViewPosition(mSharingPossible, i));
+ }
+ }
+
+ private int shareWithPossible(int i) {
+ String email = mSharingPossible.items.remove(i);
+ // Animate movement by moving the item.
+ int j = ~Collections.binarySearch(mSharingAlready.items, email);
+ int oldPosition = getViewPosition(mSharingPossible, i);
+ int newPosition = getViewPosition(mSharingAlready, j);
+ mSharingAlready.items.add(j, email);
+ notifyItemMoved(oldPosition, newPosition);
+ registerShare(email);
+ return newPosition;
+ }
+
+ private void unshare(int i) {
+ String email = mSharingAlready.items.remove(i);
+ int j = ~Collections.binarySearch(mSharingPossible.items, email);
+ mSharingPossible.items.add(j, email);
+ registerUnshare(email);
+
+ // animate movement
+ int oldPosition = getViewPosition(mSharingAlready, i);
+ int newPosition = getViewPosition(mSharingPossible, j);
+ notifyItemMoved(oldPosition, newPosition);
+ }
+
+ private int insertShare(String email) {
+ int i = Collections.binarySearch(mSharingAlready.items, email);
+ if (i >= 0) {
+ return getViewPosition(mSharingAlready, i);
+ } else {
+ i = ~i;
+ mSharingAlready.items.add(i, email);
+ i = getViewPosition(mSharingAlready, i);
+ notifyItemInserted(i);
+ registerShare(email);
+ return i;
+ }
+ }
+
+ private void registerShare(String email) {
+ mSharesRecent.add(email);
+ if (!mSharesRemoved.remove(email)) {
+ mSharesAdded.add(email);
+ }
+ }
+
+ private void registerUnshare(String email) {
+ if (!mSharesAdded.remove(email)) {
+ mSharesRemoved.add(email);
+ }
+ }
+
+ /**
+ * @return the position of the e-mail in the adapter
+ */
+ public int onCustomShare(String email) {
+ int i = Collections.binarySearch(mSharingPossible.items, email);
+ if (i >= 0) {
+ return shareWithPossible(i);
+ } else {
+ return insertShare(email);
+ }
+ }
+
+ public void setSharedTo(Collection<String> sharedAlready) {
+ setSharedAlreadyData(sharedAlready);
+ // TODO(rosswang): list differ
+
+ notifyDataSetChanged();
+ }
+
+ // TODO(rosswang): this is a hacky abstraction
+ public void toggleContact(int position) {
+ SublistEntry entry = getSublistItemPosition(position);
+ if (entry.sublist == mSharingAlready) {
+ unshare(entry.itemPosition);
+ } else {
+ shareWithPossible(entry.itemPosition);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/syncbase2/java/io/v/todos/sharing/NeighborhoodFragment.java b/app/src/syncbase2/java/io/v/todos/sharing/NeighborhoodFragment.java
new file mode 100644
index 0000000..bb2ff3b
--- /dev/null
+++ b/app/src/syncbase2/java/io/v/todos/sharing/NeighborhoodFragment.java
@@ -0,0 +1,135 @@
+package io.v.todos.sharing;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.SwitchCompat;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.CompoundButton;
+import android.widget.Toast;
+
+import io.v.syncbase.Syncbase;
+import io.v.syncbase.core.VError;
+import io.v.todos.R;
+import io.v.todos.persistence.syncbase.SyncbasePersistence;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Advertisement;
+
+/**
+ * A fragment encapsulating menu options and functionality related to list sharing.
+ */
+public class NeighborhoodFragment extends Fragment
+ implements SharedPreferences.OnSharedPreferenceChangeListener {
+ public static final String
+ FRAGMENT_TAG = NeighborhoodFragment.class.getSimpleName();
+
+ private static final String PREF_ADVERTISE_NEIGHBORHOOD = "advertise neighborhood";
+
+ private static SharedPreferences sPrefs;
+
+ // This has to be a field because registerOnSharedPreferenceChangeListener does not keep a hard
+ // reference to the listener, making it otherwise susceptible to garbage collection.
+ private static final SharedPreferences.OnSharedPreferenceChangeListener sSharedPrefListener =
+ new SharedPreferences.OnSharedPreferenceChangeListener() {
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ if (PREF_ADVERTISE_NEIGHBORHOOD.equals(key)) {
+ updateSharePresence();
+ }
+ }
+ };
+
+ /**
+ * Initializes advertisement of one's presence to the neighborhood and watches shared
+ * preferences to toggle sharing on and off as the preference is changed.
+ */
+ public void initSharePresence(Context androidContext) {
+ sPrefs = sPrefs != null ? sPrefs : PreferenceManager.getDefaultSharedPreferences(
+ androidContext);
+
+ sPrefs.registerOnSharedPreferenceChangeListener(sSharedPrefListener);
+ updateSharePresence();
+ }
+
+ private static void updateSharePresence() {
+ if (shouldAdvertise()) {
+ try {
+ Syncbase.advertiseLoggedInUserInNeighborhood();
+ } catch (VError vError) {
+ Log.w(FRAGMENT_TAG, "Failed to advertise logged in user", vError);
+ }
+ } else {
+ Syncbase.stopAdvertisingLoggedInUserInNeighborhood();
+ }
+ }
+
+ private static void setAdvertiseNeighborhood(boolean value) {
+ sPrefs.edit().putBoolean(PREF_ADVERTISE_NEIGHBORHOOD, value).apply();
+ }
+
+ private static boolean shouldAdvertise() {
+ return sPrefs.getBoolean(PREF_ADVERTISE_NEIGHBORHOOD, true);
+ }
+
+ private SwitchCompat mSwitch;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.neighborhood, menu);
+ MenuItem menuItem = menu.findItem(R.id.advertise_neighborhood);
+ mSwitch = (SwitchCompat) menuItem.getActionView().
+ findViewById(R.id.neighborhood_switch);
+ mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ setAdvertiseNeighborhood(isChecked);
+ }
+ });
+ sPrefs = sPrefs != null ? sPrefs : PreferenceManager.getDefaultSharedPreferences
+ (getActivity());
+ sPrefs.registerOnSharedPreferenceChangeListener(this);
+ updateAdvertiseNeighborhoodChecked();
+ }
+
+ @Override
+ public void onDestroyOptionsMenu() {
+ sPrefs.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (PREF_ADVERTISE_NEIGHBORHOOD.equals(key)) {
+ updateAdvertiseNeighborhoodChecked();
+ }
+ }
+
+ private static Boolean sLastToastCheck; // Used to track the last toasted value.
+
+ private void updateAdvertiseNeighborhoodChecked() {
+ boolean value = shouldAdvertise();
+ mSwitch.setChecked(value);
+ mSwitch.setThumbResource(value ?
+ R.drawable.sharing_activated_0_5x :
+ R.drawable.sharing_deactivated_0_5x);
+
+ // If this was a change, then toast information to the user.
+ if (sLastToastCheck == null || sLastToastCheck != value) {
+ sLastToastCheck = value;
+ Toast.makeText(getActivity(), value ?
+ R.string.presence_on : R.string.presence_off, Toast.LENGTH_SHORT).show();
+ }
+ }
+}
diff --git a/app/src/syncbase2/java/io/v/todos/sharing/ShareListDialogFragment.java b/app/src/syncbase2/java/io/v/todos/sharing/ShareListDialogFragment.java
new file mode 100644
index 0000000..e4550e1
--- /dev/null
+++ b/app/src/syncbase2/java/io/v/todos/sharing/ShareListDialogFragment.java
@@ -0,0 +1,182 @@
+// 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.sharing;
+
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+import io.v.syncbase.Syncbase;
+import io.v.syncbase.User;
+import io.v.todos.R;
+
+/**
+ * The share dialog contains two recycler views. The top one shows the existing items, and the
+ * bottom shows ones that are nearby but not shared to yet. There's also a freeform text box to
+ * allow entry of any value. Confirming this dialog sends the set of added and removed emails. Tap
+ * to add/remove.
+ */
+public class ShareListDialogFragment extends DialogFragment
+ implements ContactAdapter.ContactTouchListener {
+ public static final String FRAGMENT_TAG = ShareListDialogFragment.class.getSimpleName();
+
+ public static ShareListDialogFragment find(FragmentManager fragmentManager) {
+ return (ShareListDialogFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
+ }
+
+ private RecyclerView mContacts;
+ private Set<String> mRemoved, mAdded, mRecent;
+
+ private static final String
+ REMOVED_KEY = "removedShares",
+ ADDED_KEY = "addedShares",
+ RECENT_KEY = "recentShares";
+
+ private ContactAdapter mAdapter;
+
+ private ShareListMenuFragment getParent() {
+ return ((ShareListMenuFragment) getParentFragment());
+ }
+
+ @Override
+ public void onDestroyView() {
+ rmAdapter(mAdapter);
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putStringArrayList(REMOVED_KEY, new ArrayList<>(mRemoved));
+ outState.putStringArrayList(ADDED_KEY, new ArrayList<>(mAdded));
+ outState.putStringArrayList(RECENT_KEY, new ArrayList<>(mRecent));
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ View view = getActivity().getLayoutInflater().inflate(R.layout.sharing, null);
+
+ if (savedInstanceState == null) {
+ mRemoved = new HashSet<>();
+ mAdded = new HashSet<>();
+ mRecent = new HashSet<>();
+ } else {
+ mRemoved = new HashSet<>(savedInstanceState.getStringArrayList(REMOVED_KEY));
+ mAdded = new HashSet<>(savedInstanceState.getStringArrayList(ADDED_KEY));
+ mRecent = new HashSet<>(savedInstanceState.getStringArrayList(RECENT_KEY));
+ }
+
+ mAdapter = new ContactAdapter(getParent().getSharedTo(), mAdded, mRemoved, mRecent);
+ mAdapter.setContactTouchListener(this);
+ mContacts = (RecyclerView) view.findViewById(R.id.recycler);
+ mContacts.setAdapter(mAdapter);
+ addAdapter(mAdapter);
+
+ final EditText editText = (EditText) view.findViewById(R.id.custom_email);
+ editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ boolean handled = false;
+ if (actionId == EditorInfo.IME_ACTION_SEND) {
+ String email = editText.getText().toString();
+ mAdapter.onCustomShare(email);
+ editText.setText("");
+ handled = true;
+ }
+ return handled;
+ }
+ });
+
+ return new AlertDialog.Builder(getActivity())
+ .setView(view)
+ .setPositiveButton("Save", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ mAdapter.filterDeltas();
+ getParent().persistence.shareTodoList(mAdded);
+ // TODO(alexfandrianto/rosswang): removal
+ }
+ })
+ .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ }
+ })
+ .create();
+ }
+
+ public void onSharedToChanged() {
+ mContacts.post(new Runnable() {
+ @Override
+ public void run() {
+ mAdapter.setSharedTo(getParent().getSharedTo());
+ }
+ });
+ }
+
+ @Override
+ public void onContactTouch(RecyclerView.ViewHolder viewHolder) {
+ mAdapter.toggleContact(viewHolder.getAdapterPosition());
+ }
+
+ // Keep track of the Users we have found who are nearby.
+ private static Set<User> mUsers = new HashSet<>();
+ private static Set<ContactAdapter> mAdapters = new HashSet<>();
+
+ public static synchronized void addUser(User user) {
+ mUsers.add(user);
+ for(ContactAdapter adapter : mAdapters) {
+ adapter.onNearbyDeviceDiscovered(user.getAlias());
+ }
+ }
+ public static synchronized void rmUser(User user) {
+ mUsers.remove(user);
+ for(ContactAdapter adapter : mAdapters) {
+ adapter.onNearbyDeviceLost(user.getAlias());
+ }
+ }
+ public static synchronized void addAdapter(ContactAdapter adapter) {
+ mAdapters.add(adapter);
+ for(User user : mUsers) {
+ adapter.onNearbyDeviceDiscovered(user.getAlias());
+ }
+ }
+ public static synchronized void rmAdapter(ContactAdapter adapter) {
+ mAdapters.remove(adapter);
+ }
+
+ public static void initScan() {
+ Syncbase.ScanNeighborhoodForUsersCallback mScanCb = new Syncbase.ScanNeighborhoodForUsersCallback() {
+ @Override
+ public void onFound(User user) {
+ addUser(user);
+ }
+
+ @Override
+ public void onLost(User user) {
+ rmUser(user);
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ Log.w(FRAGMENT_TAG, e);
+ }
+ };
+ Syncbase.addScanForUsersInNeighborhood(mScanCb);
+ }
+}
diff --git a/app/src/syncbase2/java/io/v/todos/sharing/ShareListMenuFragment.java b/app/src/syncbase2/java/io/v/todos/sharing/ShareListMenuFragment.java
new file mode 100644
index 0000000..aa3889f
--- /dev/null
+++ b/app/src/syncbase2/java/io/v/todos/sharing/ShareListMenuFragment.java
@@ -0,0 +1,85 @@
+// 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.sharing;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.todos.R;
+import io.v.todos.persistence.syncbase.SyncbaseTodoList;
+
+/**
+ * In addition to providing the menu item, this class brokers interaction between
+ * {@link SyncbaseTodoList} and {@link ShareListDialogFragment}.
+ */
+public class ShareListMenuFragment extends Fragment {
+ public static final String FRAGMENT_TAG = ShareListMenuFragment.class.getSimpleName();
+
+ public static ShareListMenuFragment find(FragmentManager mgr) {
+ return (ShareListMenuFragment) mgr.findFragmentByTag(FRAGMENT_TAG);
+ }
+
+ public SyncbaseTodoList persistence;
+
+ private List<String> mSharedTo = new ArrayList<>();
+ private Menu mMenu;
+ private boolean mShouldHide;
+
+ public void setSharedTo(List<String> sharedTo) {
+ mSharedTo = sharedTo;
+
+ ShareListDialogFragment dialog = ShareListDialogFragment.find(getChildFragmentManager());
+ if (dialog != null) {
+ dialog.onSharedToChanged();
+ }
+ }
+
+ public List<String> getSharedTo() {
+ return mSharedTo;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ public void hideShareMenuItem() {
+ mShouldHide = true;
+ if (mMenu != null) {
+ mMenu.findItem(R.id.action_share).setVisible(false);
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ mMenu = menu;
+ inflater.inflate(R.menu.menu_share, menu);
+ if (mShouldHide) {
+ // In case the menu was added after the hide flag was set, hide the menu item now.
+ hideShareMenuItem();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_share:
+ new ShareListDialogFragment()
+ .show(getChildFragmentManager(), ShareListDialogFragment.FRAGMENT_TAG);
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/app/src/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_off_white_24dp.png b/app/src/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_off_white_24dp.png
deleted file mode 100644
index 6e61f95..0000000
--- a/app/src/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_off_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_on_white_24dp.png b/app/src/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_on_white_24dp.png
deleted file mode 100644
index 6c0314a..0000000
--- a/app/src/syncbase2/res/drawable-hdpi/ic_advertise_neighborhood_on_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_off_white_24dp.png b/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_off_white_24dp.png
deleted file mode 100644
index ab03ab5..0000000
--- a/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_off_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_on_white_24dp.png b/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_on_white_24dp.png
deleted file mode 100644
index 8dec6e0..0000000
--- a/app/src/syncbase2/res/drawable-mdpi/ic_advertise_neighborhood_on_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_off_white_24dp.png b/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_off_white_24dp.png
deleted file mode 100644
index e21ea70..0000000
--- a/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_off_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_on_white_24dp.png b/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_on_white_24dp.png
deleted file mode 100644
index cb07640..0000000
--- a/app/src/syncbase2/res/drawable-xhdpi/ic_advertise_neighborhood_on_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_off_white_24dp.png b/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_off_white_24dp.png
deleted file mode 100644
index 8735faa..0000000
--- a/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_off_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_on_white_24dp.png b/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_on_white_24dp.png
deleted file mode 100644
index 9e9560c..0000000
--- a/app/src/syncbase2/res/drawable-xxhdpi/ic_advertise_neighborhood_on_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_off_white_24dp.png b/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_off_white_24dp.png
deleted file mode 100644
index e28418e..0000000
--- a/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_off_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_on_white_24dp.png b/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_on_white_24dp.png
deleted file mode 100644
index f992b16..0000000
--- a/app/src/syncbase2/res/drawable-xxxhdpi/ic_advertise_neighborhood_on_white_24dp.png
+++ /dev/null
Binary files differ
diff --git a/app/src/syncbase2/res/drawable/sharing_activated_0_5x.png b/app/src/syncbase2/res/drawable/sharing_activated_0_5x.png
new file mode 100644
index 0000000..9d52b8d
--- /dev/null
+++ b/app/src/syncbase2/res/drawable/sharing_activated_0_5x.png
Binary files differ
diff --git a/app/src/syncbase2/res/drawable/sharing_deactivated_0_5x.png b/app/src/syncbase2/res/drawable/sharing_deactivated_0_5x.png
new file mode 100644
index 0000000..7fad11c
--- /dev/null
+++ b/app/src/syncbase2/res/drawable/sharing_deactivated_0_5x.png
Binary files differ
diff --git a/app/src/syncbase2/res/layout/neighborhood.xml b/app/src/syncbase2/res/layout/neighborhood.xml
new file mode 100644
index 0000000..b63875f
--- /dev/null
+++ b/app/src/syncbase2/res/layout/neighborhood.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="match_parent">
+
+ <android.support.v7.widget.SwitchCompat
+ android:id="@+id/neighborhood_switch"
+ android:layout_gravity="center_vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:theme="@style/SwitchCompat" />
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/app/src/syncbase2/res/menu/neighborhood.xml b/app/src/syncbase2/res/menu/neighborhood.xml
index b7d77d8..3ae819a 100644
--- a/app/src/syncbase2/res/menu/neighborhood.xml
+++ b/app/src/syncbase2/res/menu/neighborhood.xml
@@ -3,8 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/advertise_neighborhood"
android:orderInCategory="103"
- android:checkable="true"
- android:icon="@drawable/ic_advertise_neighborhood_off_white_24dp"
+ android:actionLayout="@layout/neighborhood"
android:title="@string/share_location"
android:showAsAction="always"/>
</menu>
\ No newline at end of file
diff --git a/app/src/syncbase2/res/values/strings.xml b/app/src/syncbase2/res/values/strings.xml
index b0e644b..f9c200f 100644
--- a/app/src/syncbase2/res/values/strings.xml
+++ b/app/src/syncbase2/res/values/strings.xml
@@ -1,6 +1,9 @@
<resources>
<string name="app_name">Syncbase Todos</string>
<string name="share_location">Share Presence</string>
+ <!-- For Sharing Presence Toasts -->
+ <string name="presence_on">Sharing Presence: ON</string>
+ <string name="presence_off">Sharing Presence: OFF</string>
<!-- For Sharing Menu -->
<string name="sharing_already">Shared With</string>
<string name="sharing_possible">Available</string>