TODOs: Wiring in neighborhood advertisement
Seems to work alright for me in the neighborhood.
- Not sure how to advertise globally though.
- We also have poorly formatted ads since we have no endpoints to
expose.
Is related to https://vanadium-review.googlesource.com/#/c/22301/
except with some rebasing and comments addressed.
Change-Id: Ie2f66b6ebe81fb8a13015c2ae45e87561c578e9e
diff --git a/app/src/main/java/io/v/todos/TodoListActivity.java b/app/src/main/java/io/v/todos/TodoListActivity.java
index 01392fa..43b99ce 100644
--- a/app/src/main/java/io/v/todos/TodoListActivity.java
+++ b/app/src/main/java/io/v/todos/TodoListActivity.java
@@ -9,15 +9,10 @@
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
-import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
import io.v.todos.model.DataList;
import io.v.todos.model.ListSpec;
import io.v.todos.model.Task;
@@ -40,7 +35,6 @@
*/
public class TodoListActivity extends TodosAppActivity<TodoListPersistence, TaskRecyclerAdapter> {
private ListSpec snackoo;
- private List<String> mSharedTo = new ArrayList<>();
private DataList<Task> snackoosList = new DataList<>();
// The menu item that toggles whether done items are shown or not.
@@ -128,11 +122,6 @@
}
@Override
- public void onShareChanged(List<String> sharedTo) {
- mSharedTo = sharedTo;
- }
-
- @Override
public void onItemAdd(Task item) {
int position = snackoosList.insertInOrder(item);
mAdapter.notifyItemInserted(position);
@@ -216,9 +205,8 @@
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
- int id = item.getItemId();
- switch (id) {
+ switch (item.getItemId()) {
case R.id.show_done:
mPersistence.setShowDone(!item.isChecked());
return true;
@@ -228,29 +216,6 @@
case R.id.action_debug:
sharePersistenceDebugDetails();
return true;
- case R.id.action_share:
- // TODO(alexfandrianto): We should figure out who is near us.
- List<String> fakeNearby = new ArrayList<>();
-
- // TODO(alexfandrianto): mSharedTo will not be live-updated, so the dialog can show
- // stale shares. Perhaps this dialog should return a notifier object.
- UIUtil.showShareDialog(this, mSharedTo, fakeNearby,
- new UIUtil.ShareDialogResponseListener() {
- @Override
- public void handleShareChanges(Set<String> emailsAdded, Set<String>
- emailsRemoved) {
- Log.d("SHARE COMPLETE!", emailsAdded.toString() + emailsRemoved
- .toString());
- if (emailsAdded.size() > 0) {
- mPersistence.shareTodoList(emailsAdded);
- // TODO(alexfandrianto): We may need to advertise this somehow.
- }
- // TODO(alexfandrianto): We can't actually handle removing
- // members yet, so it may be better to hide the ability to remove in
- // the share dialog.
- }
- });
- return true;
}
return super.onOptionsItemSelected(item);
diff --git a/app/src/main/java/io/v/todos/UIUtil.java b/app/src/main/java/io/v/todos/UIUtil.java
index 645f357..7760ea9 100644
--- a/app/src/main/java/io/v/todos/UIUtil.java
+++ b/app/src/main/java/io/v/todos/UIUtil.java
@@ -100,143 +100,4 @@
public void handleDelete() {
}
}
-
- // 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 static void showShareDialog(Context context, List<String> existing, List<String> nearby,
- final ShareDialogResponseListener shareListener) {
- final Set<String> emailsRemoved = new HashSet<>();
- final Set<String> emailsAdded = new HashSet<>();
- final List<String> manualEmailsTyped = new ArrayList<>();
-
- View sharingView = View.inflate(context, R.layout.sharing, null);
-
- final RecyclerView rvAlready = (RecyclerView) sharingView.findViewById(R.id
- .recycler_already);
- rvAlready.setAdapter(new ContactAdapter(existing, emailsRemoved, true));
- final RecyclerView rvPossible = (RecyclerView) sharingView.findViewById(R.id
- .recycler_possible);
- rvPossible.setAdapter(new ContactAdapter(nearby, manualEmailsTyped, emailsAdded, false));
-
- final EditText editText = (EditText) sharingView.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();
- if (!manualEmailsTyped.contains(email)) {
- manualEmailsTyped.add(email);
- }
- emailsAdded.add(email);
- rvPossible.getAdapter().notifyDataSetChanged();
- editText.setText("");
- handled = true;
- }
- return handled;
- }
- });
-
- new AlertDialog.Builder(context)
- .setView(sharingView)
- .setPositiveButton("Save", new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int whichButton) {
- shareListener.handleShareChanges(emailsAdded, emailsRemoved);
- }
- })
- .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int whichButton) {
- }
- }).show();
- }
-
- public interface ShareDialogResponseListener {
- void handleShareChanges(Set<String> emailsAdded, Set<String> emailsRemoved);
- }
-
- private static class ContactAdapter extends RecyclerView.Adapter<ContactViewHolder> {
- private final List<String> backup;
- private final List<String> bonus;
- private final Set<String> toggledOn;
- private final boolean strikethrough; // If false, then bold.
-
- public ContactAdapter(List<String> backup, Set<String> toggledOn, boolean strikethrough) {
- super();
- this.backup = backup;
- this.bonus = null;
- this.toggledOn = toggledOn;
- this.strikethrough = strikethrough;
- }
-
- public ContactAdapter(List<String> backup, List<String> bonus, Set<String> toggledOn,
- boolean strikethrough) {
- super();
- this.backup = backup;
- this.bonus = bonus;
- this.toggledOn = toggledOn;
- this.strikethrough = strikethrough;
- }
-
- @Override
- public ContactViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- return new ContactViewHolder(new TextView(parent.getContext()));
- }
-
- @Override
- public void onBindViewHolder(final ContactViewHolder holder, int position) {
- final String name = position < backup.size() ? backup.get(position) :
- bonus.get(position - backup.size());
- final boolean present = toggledOn.contains(name);
- holder.bindString(name, present, strikethrough, new View.OnClickListener() {
-
- @Override
- public void onClick(View view) {
- if (present) {
- toggledOn.remove(name);
- } else {
- toggledOn.add(name);
- }
- notifyItemChanged(holder.getAdapterPosition());
- }
- });
- }
-
- @Override
- public int getItemCount() {
- int extra = bonus == null ? 0 : bonus.size();
- return backup.size() + extra;
- }
- }
-
- private static class ContactViewHolder extends RecyclerView.ViewHolder {
- public ContactViewHolder(View itemView) {
- super(itemView);
- }
-
- public void bindString(String name, boolean isActive, boolean strikethrough, View
- .OnClickListener listener) {
- TextView text = (TextView) itemView;
-
- text.setText(name);
- text.setTextSize(18);
- if (strikethrough) {
- if (isActive) {
- text.setPaintFlags(text.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
- } else {
- text.setPaintFlags(text.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
- }
- } else {
- // We should bold!
- if (isActive) {
- text.setTypeface(null, 1); // 1 is bold
- } else {
- text.setTypeface(null, 0); // 0 is default text style
- }
- }
-
- text.setOnClickListener(listener);
- }
- }
}
diff --git a/app/src/main/java/io/v/todos/persistence/TodoListListener.java b/app/src/main/java/io/v/todos/persistence/TodoListListener.java
index 069bd0e..fa9e355 100644
--- a/app/src/main/java/io/v/todos/persistence/TodoListListener.java
+++ b/app/src/main/java/io/v/todos/persistence/TodoListListener.java
@@ -4,8 +4,6 @@
package io.v.todos.persistence;
-import java.util.List;
-
import io.v.todos.model.ListSpec;
import io.v.todos.model.Task;
@@ -13,6 +11,4 @@
void onUpdate(ListSpec value);
void onDelete();
void onUpdateShowDone(boolean showDone);
-
- void onShareChanged(List<String> sharedTo);
}
diff --git a/app/src/main/res/menu/menu_task.xml b/app/src/main/res/menu/menu_task.xml
index c405b5b..449ae38 100644
--- a/app/src/main/res/menu/menu_task.xml
+++ b/app/src/main/res/menu/menu_task.xml
@@ -13,11 +13,6 @@
android:title="@string/action_edit"
app:showAsAction="never" />
<item
- android:id="@+id/action_share"
- android:orderInCategory="103"
- android:title="@string/action_share"
- app:showAsAction="never" />
- <item
android:id="@+id/action_debug"
android:orderInCategory="104"
android:title="@string/action_debug"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5c7280c..5b062a7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -11,10 +11,6 @@
<string name="just_now">Just now</string>
<string name="email_debug_subject">Debug the persistence layer</string>
<string name="no_email_client">An app to share emails is not installed, cannot send debug request</string>
- <!-- For Sharing Menu -->
- <string name="sharing_already">Sharing With</string>
- <string name="sharing_possible">Nearby</string>
- <string name="sharing_custom_hint">Add an email address...</string>
<!-- Errors -->
<string name="err_init">Unable to initialize persistence</string>
<string name="err_sync">Unable to sync</string>
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 d20c44e..ed6fd8f 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
@@ -84,31 +84,27 @@
mIdGenerator.registerId(change.getRowName().substring(LISTS_PREFIX.length()));
Log.d(TAG, "Found a list id from userdata watch: " + listId);
- Futures.catchingAsync(joinListSyncgroup(listId),
+ trap(Futures.catchingAsync(joinListSyncgroup(listId),
SyncgroupJoinFailedException.class, new
AsyncFunction<SyncgroupJoinFailedException, SyncgroupSpec>() {
- public ListenableFuture<SyncgroupSpec> apply(@Nullable
- SyncgroupJoinFailedException
- input) throws
- Exception {
- Log.d(TAG, "Join failed. Sleeping and trying again: "
- + listId);
- return sExecutor.schedule(new Callable<SyncgroupSpec>
- () {
+ public ListenableFuture<SyncgroupSpec> apply(@Nullable
+ SyncgroupJoinFailedException
+ input) throws
+ Exception {
+ Log.d(TAG, "Join failed. Sleeping and trying again: " + listId);
+ return sExecutor.schedule(new Callable<SyncgroupSpec>() {
- @Override
- public SyncgroupSpec call() throws Exception {
- Log.d(TAG, "Sleep done. Trying again: " +
- listId);
+ @Override
+ public SyncgroupSpec call() throws Exception {
+ Log.d(TAG, "Sleep done. Trying again: " + listId);
- // If this errors, then we will not get
- // another chance to see
- // this syncgroup until the app is restarted.
- return joinListSyncgroup(listId).get();
- }
- }, RETRY_DELAY, TimeUnit.MILLISECONDS);
- }
- });
+ // If this errors, then we will not get another chance to see
+ // this syncgroup until the app is restarted.
+ return joinListSyncgroup(listId).get();
+ }
+ }, RETRY_DELAY, TimeUnit.MILLISECONDS);
+ }
+ }));
MainListTracker listTracker = new MainListTracker(getVContext(), getDatabase(),
listId, listener);
@@ -120,18 +116,17 @@
}
// If the watch fails with NoExistException, the collection has been deleted.
- Futures.addCallback(listTracker.watchFuture,
- new TrappingCallback<Void>(getErrorReporter()) {
- @Override
- public void onFailure(@NonNull Throwable t) {
- if (t instanceof NoExistException) {
- // (this is idempotent)
- trap(getUserCollection().delete(getVContext(), listId));
- } else {
- super.onFailure(t);
- }
- }
- });
+ Futures.addCallback(listTracker.watchFuture, new SyncTrappingCallback<Void>() {
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ if (t instanceof NoExistException) {
+ // (this is idempotent)
+ trap(getUserCollection().delete(getVContext(), listId));
+ } else {
+ super.onFailure(t);
+ }
+ }
+ });
}
return null;
}
@@ -149,11 +144,11 @@
// collections anyway. If https://github.com/vanadium/issues/issues/1326 is done, however,
// we won't need to change this code.
Futures.addCallback(listCollection.create(getVContext(), null),
- new TrappingCallback<Void>(getErrorReporter()) {
+ new SyncTrappingCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
Futures.addCallback(createListSyncgroup(listCollection.id()),
- new TrappingCallback<Void>(getErrorReporter()) {
+ new SyncTrappingCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
// These can happen in either order.
@@ -171,7 +166,7 @@
private ListenableFuture<SyncgroupSpec> joinListSyncgroup(String listId) {
SyncgroupMemberInfo memberInfo = getDefaultMemberInfo();
String sgName = computeListSyncgroupName(listId);
- String blessingStr = getPersonalBlessingsString(getVContext());
+ String blessingStr = getPersonalBlessingsString();
return getDatabase().getSyncgroup(new Id(blessingStr, sgName)).join(getVContext(),
CLOUD_NAME, CLOUD_BLESSING, memberInfo);
}
@@ -180,7 +175,7 @@
String listId = id.getName();
final String sgName = computeListSyncgroupName(listId);
Permissions permissions =
- computePermissionsFromBlessings(getPersonalBlessings(getVContext()));
+ computePermissionsFromBlessings(getPersonalBlessings());
SyncgroupMemberInfo memberInfo = getDefaultMemberInfo();
@@ -188,7 +183,7 @@
"TODOs User Data Collection", CLOUD_NAME, permissions,
ImmutableList.of(new CollectionRow(id, "")),
ImmutableList.of(MOUNTPOINT), false);
- String blessingStr = getPersonalBlessingsString(getVContext());
+ String blessingStr = getPersonalBlessingsString();
return getDatabase().getSyncgroup(new Id(blessingStr, sgName)).create(getVContext(),
spec, memberInfo);
}
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 df27fea..e0692f9 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
@@ -5,11 +5,13 @@
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.NonNull;
+import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
import android.util.Log;
import com.google.common.base.Supplier;
@@ -17,7 +19,6 @@
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
@@ -29,22 +30,23 @@
import org.joda.time.format.DateTimeFormat;
import java.io.File;
+import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
-
-import javax.annotation.Nullable;
+import java.util.concurrent.Future;
import io.v.android.inspectors.RemoteInspectors;
+import io.v.android.ManagedVAndroidContext;
import io.v.android.VAndroidContext;
import io.v.android.VAndroidContexts;
-import io.v.android.security.BlessingsManager;
import io.v.android.error.ErrorReporter;
+import io.v.android.error.ToastingErrorReporter;
+import io.v.android.security.BlessingsManager;
import io.v.android.v23.V;
import io.v.impl.google.services.syncbase.SyncbaseServer;
import io.v.todos.R;
import io.v.todos.persistence.Persistence;
import io.v.todos.sharing.NeighborhoodFragment;
-import io.v.v23.OptionDefs;
-import io.v.v23.Options;
+import io.v.todos.sharing.Sharing;
import io.v.v23.VFutures;
import io.v.v23.context.VContext;
import io.v.v23.rpc.Server;
@@ -64,7 +66,6 @@
import io.v.v23.syncbase.SyncbaseService;
import io.v.v23.syncbase.Syncgroup;
import io.v.v23.vdl.VdlStruct;
-import io.v.v23.verror.CanceledException;
import io.v.v23.verror.ExistException;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;
@@ -107,11 +108,34 @@
protected static final ListeningScheduledExecutorService sExecutor =
MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(10));
+ private static final Object sVContextMutex = new Object();
+ private static VAndroidContext<Context> sVAndroidContext;
+
private static final Object sSyncbaseMutex = new Object();
- private static VContext sVContext;
private static SyncbaseService sSyncbase;
private static RemoteInspectors sRemoteInspectors;
+ private static void appVInit(Context appContext) {
+ synchronized (sVContextMutex) {
+ if (sVAndroidContext == null) {
+ sVAndroidContext = new ManagedVAndroidContext<>(appContext,
+ new ToastingErrorReporter(appContext));
+ }
+ }
+ }
+
+ public static Context getAppContext() {
+ return sVAndroidContext.getAndroidContext();
+ }
+
+ public static VContext getAppVContext() {
+ return sVAndroidContext.getVContext();
+ }
+
+ public static ErrorReporter getAppErrorReporter() {
+ return sVAndroidContext.getErrorReporter();
+ }
+
private static String startSyncbaseServer(VContext vContext, Context appContext,
Permissions serverPermissions)
throws SyncbaseServer.StartException {
@@ -161,14 +185,10 @@
throws SyncbaseServer.StartException, VException {
synchronized (sSyncbaseMutex) {
if (sSyncbase == null) {
- final Context appContext = androidContext.getApplicationContext();
- VContext singletonContext = V.init(appContext, new Options()
- .set(OptionDefs.LOG_VLEVEL, 0)
- .set(OptionDefs.LOG_VMODULE, "vsync*=0"));
-
+ VContext serverRun = getAppVContext().withCancel();
try {
// Retrieve this context's personal permissions to set ACLs on the server.
- Blessings personalBlessings = getPersonalBlessings(singletonContext);
+ Blessings personalBlessings = getPersonalBlessings();
if (personalBlessings == null) {
throw new IllegalStateException("Blessings must be attached to the " +
"Vanadium principal before Syncbase initialization.");
@@ -176,39 +196,44 @@
Permissions permissions = computePermissionsFromBlessings(personalBlessings);
sSyncbase = Syncbase.newService(startSyncbaseServer(
- singletonContext, appContext, permissions));
+ serverRun, getAppContext(), permissions));
} catch (SyncbaseServer.StartException | RuntimeException e) {
- singletonContext.cancel();
+ serverRun.cancel();
throw e;
}
- sVContext = singletonContext;
}
}
}
- protected static Blessings getPersonalBlessings(VContext ctx) {
- return V.getPrincipal(ctx).blessingStore().defaultBlessings();
+ // TODO(rosswang): Factor into v23
+ public static Blessings getPersonalBlessings() {
+ return V.getPrincipal(getAppVContext()).blessingStore().defaultBlessings();
}
- protected static String getPersonalBlessingsString(VContext ctx) {
- return getPersonalBlessings(ctx).toString();
+ public static String getPersonalBlessingsString() {
+ return getPersonalBlessings().toString();
}
- protected static String getEmailFromBlessings(Blessings blessings) {
- String[] split = blessings.toString().split(":");
+ public static String getEmailFromBlessings(Blessings blessings) {
+ // TODO(alexfandrianto): This should be in v23, but it should also verify that the app
+ // component is the right one in the blessing.
+ return getEmailFromBlessingsString(blessings.toString());
+ }
+
+ public static String getEmailFromPattern(BlessingPattern pattern) {
+ return getEmailFromBlessingsString(pattern.toString());
+ }
+
+ public static String getEmailFromBlessingsString(String blessingsStr) {
+ String[] split = blessingsStr.split(":");
return split[split.length - 1];
}
- protected static String getEmailFromPattern(BlessingPattern pattern) {
- String[] split = pattern.toString().split(":");
- return split[split.length - 1];
+ public static String getPersonalEmail() {
+ return getEmailFromBlessings(getPersonalBlessings());
}
- protected static String getPersonalEmail(VContext ctx) {
- return getEmailFromBlessings(getPersonalBlessings(ctx));
- }
-
- protected static String blessingsStringFromEmail(String email) {
+ public static String blessingsStringFromEmail(String email) {
// TODO(alexfandrianto): We may need a more sophisticated method for producing this
// blessings string. Currently, the app's id is fixed to the anonymous Android app.
return DEFAULT_BLESSING_STRING + email;
@@ -232,10 +257,10 @@
private static void ensureDatabaseExists() throws VException {
synchronized (sDatabaseMutex) {
if (sDatabase == null) {
- final Database db = sSyncbase.getDatabase(sVContext, DATABASE, null);
+ final Database db = sSyncbase.getDatabase(getAppVContext(), DATABASE, null);
try {
- VFutures.sync(db.create(sVContext, null));
+ VFutures.sync(db.create(getAppVContext(), null));
} catch (ExistException e) {
// This is fine.
}
@@ -245,15 +270,15 @@
}
private static final Object sUserCollectionMutex = new Object();
- private static volatile Collection sUserCollection;
+ private static Collection sUserCollection;
private static void ensureUserCollectionExists() throws VException {
synchronized (sUserCollectionMutex) {
if (sUserCollection == null) {
Collection userCollection = sDatabase.getCollection(
- new Id(getPersonalBlessingsString(sVContext), USER_COLLECTION_NAME));
+ new Id(getPersonalBlessingsString(), USER_COLLECTION_NAME));
try {
- VFutures.sync(userCollection.create(sVContext, null));
+ VFutures.sync(userCollection.create(getAppVContext(), null));
} catch (ExistException e) {
// This is fine.
}
@@ -263,17 +288,16 @@
}
private static final Object sCloudDatabaseMutex = new Object();
- private static volatile Database sCloudDatabase;
-
+ private static Database sCloudDatabase;
private static void ensureCloudDatabaseExists() {
synchronized (sCloudDatabaseMutex) {
if (sCloudDatabase == null) {
SyncbaseService cloudService = Syncbase.newService(CLOUD_NAME);
- Database db = cloudService.getDatabase(sVContext, DATABASE, null);
+ Database db = cloudService.getDatabase(getAppVContext(), DATABASE, null);
try {
- VFutures.sync(db.create(sVContext.withTimeout(Duration.millis(SHORT_TIMEOUT))
- , null));
+ VFutures.sync(db.create(getAppVContext()
+ .withTimeout(Duration.millis(SHORT_TIMEOUT)), null));
} catch (ExistException e) {
// This is acceptable. No need to do it again.
} catch (Exception e) {
@@ -285,12 +309,12 @@
}
private static final Object sUserSyncgroupMutex = new Object();
- private static volatile Syncgroup sUserSyncgroup;
+ private static Syncgroup sUserSyncgroup;
private static void ensureUserSyncgroupExists() throws VException {
synchronized (sUserSyncgroupMutex) {
if (sUserSyncgroup == null) {
- Blessings clientBlessings = getPersonalBlessings(sVContext);
+ Blessings clientBlessings = getPersonalBlessings();
String email = getEmailFromBlessings(clientBlessings);
Log.d(TAG, email);
@@ -304,7 +328,8 @@
try {
Log.d(TAG, "Trying to join the syncgroup: " + sgName);
- VFutures.sync(sgHandle.join(sVContext, CLOUD_NAME, CLOUD_BLESSING, memberInfo));
+ VFutures.sync(sgHandle.join(getAppVContext(), CLOUD_NAME, CLOUD_BLESSING,
+ memberInfo));
Log.d(TAG, "JOINED the syncgroup: " + sgName);
} catch (SyncgroupJoinFailedException e) {
Log.w(TAG, "Failed join. Trying to create the syncgroup: " + sgName, e);
@@ -313,7 +338,7 @@
ImmutableList.of(new CollectionRow(sUserCollection.id(), "")),
ImmutableList.of(MOUNTPOINT), false);
try {
- VFutures.sync(sgHandle.create(sVContext, spec, memberInfo));
+ VFutures.sync(sgHandle.create(getAppVContext(), spec, memberInfo));
} catch (BadAdvertisementException e2) {
Log.d(TAG, "Bad advertisement exception. Can we fix this?");
}
@@ -336,7 +361,7 @@
}
- protected String computeListSyncgroupName(String listId) {
+ protected static String computeListSyncgroupName(String listId) {
return LIST_COLLECTION_SYNCGROUP_SUFFIX + listId;
}
@@ -347,39 +372,6 @@
}
/**
- * A {@link FutureCallback} that reports persistence errors by toasting a short message to the
- * user and logging the exception trace and the call stack from where the future was invoked.
- */
- public static class TrappingCallback<T> implements FutureCallback<T> {
- private static final int FIRST_SIGNIFICANT_STACK_ELEMENT = 3;
- private final ErrorReporter mErrorReporter;
- private final StackTraceElement[] mCaller;
-
- public TrappingCallback(ErrorReporter errorReporter) {
- mErrorReporter = errorReporter;
- mCaller = Thread.currentThread().getStackTrace();
- }
-
- @Override
- public void onSuccess(@Nullable T result) {
- }
-
- @Override
- public void onFailure(@NonNull Throwable t) {
- if (!(t instanceof CanceledException || t instanceof ExistException)) {
- mErrorReporter.onError(R.string.err_sync, t);
-
- StringBuilder traceBuilder = new StringBuilder(t.getMessage())
- .append("\n invoked at ").append(mCaller[FIRST_SIGNIFICANT_STACK_ELEMENT]);
- for (int i = FIRST_SIGNIFICANT_STACK_ELEMENT + 1; i < mCaller.length; i++) {
- traceBuilder.append("\n\tat ").append(mCaller[i]);
- }
- Log.e(TAG, traceBuilder.toString());
- }
- }
- }
-
- /**
* Extracts the value from a watch change or scan stream.
* TODO(rosswang): This method is a temporary hack, awaiting resolution of the following issues:
* <ul>
@@ -401,6 +393,12 @@
}
}
+ protected class SyncTrappingCallback<T> extends TrappingCallback<T> {
+ public SyncTrappingCallback() {
+ super(R.string.err_sync, TAG, getErrorReporter());
+ }
+ }
+
private final VAndroidContext<Activity> mVAndroidContext;
public VContext getVContext() {
@@ -428,11 +426,21 @@
* @see TrappingCallback
*/
protected void trap(ListenableFuture<?> future) {
- Futures.addCallback(future, new TrappingCallback<>(getErrorReporter()));
+ Futures.addCallback(future, new SyncTrappingCallback<>());
}
- protected void addFeatureFragments(FragmentTransaction fragments) {
- fragments.add(new NeighborhoodFragment(), NeighborhoodFragment.FRAGMENT_TAG);
+ /**
+ * 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,
+ @Nullable FragmentTransaction transaction) {
+ if (transaction != null) {
+ transaction.add(new NeighborhoodFragment(), NeighborhoodFragment.FRAGMENT_TAG);
+ }
}
/**
@@ -442,10 +450,13 @@
throws VException, SyncbaseServer.StartException {
mVAndroidContext = VAndroidContexts.withDefaults(activity, savedInstanceState);
+ FragmentManager mgr = activity.getFragmentManager();
if (savedInstanceState == null) {
- FragmentTransaction fragments = activity.getFragmentManager().beginTransaction();
- addFeatureFragments(fragments);
- fragments.commit();
+ FragmentTransaction t = mgr.beginTransaction();
+ addFeatureFragments(mgr, t);
+ t.commit();
+ } else {
+ addFeatureFragments(mgr, null);
}
// We might not actually have to seek blessings each time, but getBlessings does not
@@ -466,11 +477,26 @@
}
VFutures.sync(Futures.dereference(blessings));
+ appVInit(activity.getApplicationContext());
+ Future<?> initDiscovery = sExecutor.submit(new Callable<Void>() {
+ @Override
+ public Void call() throws VException {
+ Sharing.initDiscovery();
+ return null;
+ }
+ });
+ final Future<?> ensureCloudDatabaseExists = sExecutor.submit(new Runnable() {
+ @Override
+ public void run() {
+ ensureCloudDatabaseExists();
+ }
+ });
ensureSyncbaseStarted(activity);
ensureDatabaseExists();
ensureUserCollectionExists();
- ensureCloudDatabaseExists();
+ VFutures.sync(ensureCloudDatabaseExists); // must finish before syncgroup setup
ensureUserSyncgroupExists();
+ VFutures.sync(initDiscovery);
sInitialized = true;
}
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 f735fc4..6fad570 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
@@ -5,6 +5,8 @@
package io.v.todos.persistence.syncbase;
import android.app.Activity;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.Log;
@@ -29,6 +31,7 @@
import io.v.todos.model.TaskSpec;
import io.v.todos.persistence.TodoListListener;
import io.v.todos.persistence.TodoListPersistence;
+import io.v.todos.sharing.ShareListMenuFragment;
import io.v.v23.InputChannel;
import io.v.v23.InputChannelCallback;
import io.v.v23.InputChannels;
@@ -65,6 +68,20 @@
private final IdGenerator mIdGenerator = new IdGenerator(IdAlphabets.ROW_NAME, true);
private final Set<String> mTaskIds = new HashSet<>();
private final Timer mMemberTimer;
+ private ShareListMenuFragment mShareListMenuFragment;
+
+ @Override
+ protected void addFeatureFragments(FragmentManager manager, FragmentTransaction transaction) {
+ super.addFeatureFragments(manager, transaction);
+ if (transaction == null) {
+ mShareListMenuFragment = ShareListMenuFragment.find(manager);
+ } else {
+ mShareListMenuFragment = new ShareListMenuFragment();
+ transaction.add(mShareListMenuFragment, ShareListMenuFragment.FRAGMENT_TAG);
+ }
+ mShareListMenuFragment.persistence = this;
+ mShareListMenuFragment.setEmail(getPersonalEmail());
+ }
/**
* This assumes that the collection for this list already exists.
@@ -85,7 +102,7 @@
return null;
}
});
- Futures.addCallback(listWatchFuture, new TrappingCallback<Void>(getErrorReporter()) {
+ Futures.addCallback(listWatchFuture, new SyncTrappingCallback<Void>() {
@Override
public void onFailure(@NonNull Throwable t) {
if (t instanceof NoExistException) {
@@ -128,7 +145,7 @@
// Analyze these patterns to construct the emails, and fire the listener!
List<String> emails = parseEmailsFromPatterns(patterns);
- mListener.onShareChanged(emails);
+ mShareListMenuFragment.setSharedTo(emails);
}
}, MEMBER_TIMER_DELAY, MEMBER_TIMER_PERIOD);
@@ -153,7 +170,7 @@
// Skip. It's the cloud, and that doesn't count.
continue;
}
- if (pattern.toString().endsWith(getPersonalEmail(getVContext()))) {
+ if (pattern.toString().endsWith(getPersonalEmail())) {
// Skip. It's you, and that doesn't count.
continue;
}
@@ -205,7 +222,7 @@
}
private Syncgroup getListSyncgroup() {
- return getDatabase().getSyncgroup(new Id(getPersonalBlessingsString(getVContext()),
+ return getDatabase().getSyncgroup(new Id(getPersonalBlessingsString(),
computeListSyncgroupName(mList.id().getName())));
}
@@ -236,7 +253,7 @@
// Analyze these patterns to construct the emails, and fire the listener!
List<String> specEmails = parseEmailsFromPatterns(
perms.get(Constants.READ.getValue()).getIn());
- mListener.onShareChanged(specEmails);
+ mShareListMenuFragment.setSharedTo(specEmails);
// Add read and write access to the collection permissions.
perms = VFutures.sync(mList.getPermissions(getVContext()));
@@ -251,6 +268,8 @@
// TODO(alexfandrianto): We should consider moving this helper into the main Java repo.
// https://github.com/vanadium/issues/issues/1321
+ // TODO(alexfandrianto): This allows you to repeatedly add the same blessings to the permission
+ // multiple times.
private static void addPermissions(Permissions perms, Iterable<String> emails, String tag) {
AccessList acl = perms.get(tag);
List<BlessingPattern> patterns = acl.getIn();
diff --git a/app/src/syncbase/java/io/v/todos/persistence/syncbase/TrappingCallback.java b/app/src/syncbase/java/io/v/todos/persistence/syncbase/TrappingCallback.java
new file mode 100644
index 0000000..969bbbc
--- /dev/null
+++ b/app/src/syncbase/java/io/v/todos/persistence/syncbase/TrappingCallback.java
@@ -0,0 +1,57 @@
+// 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.syncbase;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import javax.annotation.Nullable;
+
+import io.v.android.error.ErrorReporter;
+import io.v.v23.verror.CanceledException;
+import io.v.v23.verror.ExistException;
+
+/**
+ * A {@link FutureCallback} that reports persistence errors by toasting a short message to the
+ * user and logging the exception trace and the call stack from where the future was invoked.
+ *
+ * TODO(rosswang): Factor into V23.
+ */
+public class TrappingCallback<T> implements FutureCallback<T> {
+ private static final int FIRST_SIGNIFICANT_STACK_ELEMENT = 3;
+ private final @StringRes int mFailureMessage;
+ private final String mLogTag;
+ private final ErrorReporter mErrorReporter;
+ private final StackTraceElement[] mCaller;
+
+ public TrappingCallback(@StringRes int failureMessage, String logTag,
+ ErrorReporter errorReporter) {
+ mFailureMessage = failureMessage;
+ mLogTag = logTag;
+ mErrorReporter = errorReporter;
+ mCaller = Thread.currentThread().getStackTrace();
+ }
+
+ @Override
+ public void onSuccess(@Nullable T result) {
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ if (!(t instanceof CanceledException || t instanceof ExistException)) {
+ mErrorReporter.onError(mFailureMessage, t);
+
+ StringBuilder traceBuilder = new StringBuilder(t.getMessage())
+ .append("\n invoked at ").append(mCaller[FIRST_SIGNIFICANT_STACK_ELEMENT]);
+ for (int i = FIRST_SIGNIFICANT_STACK_ELEMENT + 1; i < mCaller.length; i++) {
+ traceBuilder.append("\n\tat ").append(mCaller[i]);
+ }
+ Log.e(mLogTag, traceBuilder.toString());
+ }
+ }
+}
diff --git a/app/src/syncbase/java/io/v/todos/sharing/NeighborhoodFragment.java b/app/src/syncbase/java/io/v/todos/sharing/NeighborhoodFragment.java
index c7ab727..a7c478e 100644
--- a/app/src/syncbase/java/io/v/todos/sharing/NeighborhoodFragment.java
+++ b/app/src/syncbase/java/io/v/todos/sharing/NeighborhoodFragment.java
@@ -5,55 +5,156 @@
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+
import io.v.todos.R;
+import io.v.todos.persistence.syncbase.SyncbasePersistence;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Advertisement;
+import io.v.v23.verror.VException;
/**
* A fragment encapsulating menu options and functionality related to list sharing.
*/
-public class NeighborhoodFragment extends Fragment {
- public static final String FRAGMENT_TAG = NeighborhoodFragment.class.getSimpleName();
+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 SharedPreferences mPrefs;
+ private static SharedPreferences sPrefs;
+ private static VContext sAdvertiseContext;
+ private static Advertisement sAd;
- private boolean isAdvertising() {
- return mPrefs.getBoolean(PREF_ADVERTISE_NEIGHBORHOOD, false);
+ // 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 static void initSharePresence() {
+ sPrefs = sPrefs != null ? sPrefs : PreferenceManager.getDefaultSharedPreferences
+ (SyncbasePersistence.getAppContext());
+
+ sAd = new Advertisement();
+ sAd.setInterfaceName(Sharing.getPresenceInterface());
+ // TODO(alexfandrianto): Revisit why we must put an address inside the advertisement.
+ // If we are advertising our presence, then there isn't any need for it.
+ // For now, put our email address in the addresses, despite it being an attribute.
+ sAd.getAddresses().add(SyncbasePersistence.getPersonalEmail());
+
+ sPrefs.registerOnSharedPreferenceChangeListener(sSharedPrefListener);
+ updateSharePresence();
}
+ private static void updateSharePresence() {
+ if (isAdvertising()) {
+ if (sAdvertiseContext == null) {
+ sAdvertiseContext = SyncbasePersistence.getAppVContext().withCancel();
+ try {
+ Futures.addCallback(Sharing.getDiscovery().advertise(sAdvertiseContext, sAd,
+ // TODO(rosswang): Restrict to contacts only. However, per discussion
+ // with mattr@ and ashankar@, this could be a scalability challenge as
+ // the advertisement would need to be IB-encrypted for each possible
+ // recipient. This can be broken into multiple advertisements but at the
+ // increased risk of over-the-air hash collision.
+ null),
+ new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ handleAdvertisingError(t);
+ }
+ });
+ } catch (VException e) {
+ handleAdvertisingError(e);
+ }
+ }
+ } else if (sAdvertiseContext != null) {
+ sAdvertiseContext.cancel();
+ sAdvertiseContext = null;
+ }
+ }
+
+ private static void handleAdvertisingError(Throwable t) {
+ SyncbasePersistence.getAppErrorReporter().onError(R.string.err_share_location, t);
+ setAdvertiseNeighborhood(false);
+ }
+
+ private static void setAdvertiseNeighborhood(boolean value) {
+ sPrefs.edit()
+ .putBoolean(PREF_ADVERTISE_NEIGHBORHOOD, value)
+ .apply();
+ }
+
+ private static boolean isAdvertising() {
+ return sPrefs.getBoolean(PREF_ADVERTISE_NEIGHBORHOOD, false);
+ }
+
+ private MenuItem mAdvertiseNeighborhoodMenuItem;
+
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
-
- mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- inflater.inflate(R.menu.sharing, menu);
- setAdvertiseNeighborhoodChecked(menu.findItem(R.id.advertise_neighborhood), isAdvertising());
+ inflater.inflate(R.menu.neighborhood, menu);
+ mAdvertiseNeighborhoodMenuItem = menu.findItem(R.id.advertise_neighborhood);
+ sPrefs = sPrefs != null ? sPrefs : PreferenceManager.getDefaultSharedPreferences
+ (getActivity());
+ sPrefs.registerOnSharedPreferenceChangeListener(this);
+ updateAdvertiseNeighborhoodChecked();
}
- private void setAdvertiseNeighborhoodChecked(MenuItem menuItem, boolean value) {
- menuItem.setChecked(value);
- menuItem.setIcon(value ? R.drawable.ic_advertise_neighborhood_on_white_24dp :
+ @Override
+ public void onDestroyOptionsMenu() {
+ sPrefs.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (PREF_ADVERTISE_NEIGHBORHOOD.equals(key)) {
+ updateAdvertiseNeighborhoodChecked();
+ }
+ }
+
+ private void updateAdvertiseNeighborhoodChecked() {
+ boolean value = isAdvertising();
+ mAdvertiseNeighborhoodMenuItem.setChecked(value);
+ mAdvertiseNeighborhoodMenuItem.setIcon(value ?
+ R.drawable.ic_advertise_neighborhood_on_white_24dp :
R.drawable.ic_advertise_neighborhood_off_white_24dp);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.advertise_neighborhood) {
- boolean advertiseNeighborhood = !item.isChecked();
- mPrefs.edit()
- .putBoolean(PREF_ADVERTISE_NEIGHBORHOOD, advertiseNeighborhood)
- .apply();
- setAdvertiseNeighborhoodChecked(item, advertiseNeighborhood);
+ setAdvertiseNeighborhood(!item.isChecked());
return true;
} else {
return super.onOptionsItemSelected(item);
diff --git a/app/src/syncbase/java/io/v/todos/sharing/ShareListDialogFragment.java b/app/src/syncbase/java/io/v/todos/sharing/ShareListDialogFragment.java
new file mode 100644
index 0000000..e380e05
--- /dev/null
+++ b/app/src/syncbase/java/io/v/todos/sharing/ShareListDialogFragment.java
@@ -0,0 +1,296 @@
+// 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.graphics.Paint;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.RecyclerView;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import io.v.todos.R;
+import io.v.todos.persistence.syncbase.SyncbasePersistence;
+import io.v.v23.InputChannelCallback;
+import io.v.v23.InputChannels;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Update;
+import io.v.v23.verror.VException;
+
+/**
+ * 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 {
+ public static final String FRAGMENT_TAG = ShareListDialogFragment.class.getSimpleName();
+
+ public static ShareListDialogFragment find(FragmentManager fragmentManager) {
+ return (ShareListDialogFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
+ }
+
+ private Set<String> mRemoved;
+ private List<String> mNearby = new ArrayList<>();
+ private ArrayList<String> mTyped;
+ private Set<String> mAdded;
+
+ private static final String
+ REMOVED_KEY = "removedShares",
+ TYPED_KEY = "explicitShares",
+ ADDED_KEY = "addedShares";
+
+ private ContactAdapter mAlreadyAdapter, mPossibleAdapter;
+
+ private VContext mScanContext;
+
+ private ShareListMenuFragment getParent() {
+ return ((ShareListMenuFragment) getParentFragment());
+ }
+
+ @Override
+ public void onDestroyView() {
+ mScanContext.cancel();
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putStringArrayList(REMOVED_KEY, new ArrayList<>(mRemoved));
+ outState.putStringArrayList(TYPED_KEY, mTyped);
+ outState.putStringArrayList(ADDED_KEY, new ArrayList<>(mAdded));
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ View view = getActivity().getLayoutInflater().inflate(R.layout.sharing, null);
+
+ if (savedInstanceState == null) {
+ mRemoved = new HashSet<>();
+ mAdded = new HashSet<>();
+ mTyped = new ArrayList<>();
+ } else {
+ mRemoved = new HashSet<>(savedInstanceState.getStringArrayList(REMOVED_KEY));
+ mAdded = new HashSet<>(savedInstanceState.getStringArrayList(ADDED_KEY));
+ mTyped = savedInstanceState.getStringArrayList(TYPED_KEY);
+ }
+
+ mAlreadyAdapter = new ContactAdapter(getParent().getSharedTo(), mRemoved, true);
+ RecyclerView rvAlready = (RecyclerView) view.findViewById(R.id.recycler_already);
+ rvAlready.setAdapter(mAlreadyAdapter);
+ mNearby.clear();
+ final RecyclerView rvPossible = (RecyclerView) view.findViewById(R.id.recycler_possible);
+ mPossibleAdapter = new ContactAdapter(mNearby, mTyped, mAdded, false);
+ rvPossible.setAdapter(mPossibleAdapter);
+
+ mScanContext = SyncbasePersistence.getAppVContext().withCancel();
+ try {
+ ListenableFuture<Void> scan = InputChannels.withCallback(
+ Sharing.getDiscovery().scan(mScanContext,
+ "v.InterfaceName = \"" + Sharing.getPresenceInterface() + "\""),
+ new InputChannelCallback<Update>() {
+ private final Map<String, Integer> counterMap = new HashMap<>();
+
+ @Override
+ public ListenableFuture<Void> onNext(Update result) {
+ final String email = Iterables.getOnlyElement(result.getAddresses());
+ if (email == null) {
+ return null;
+ }
+ // Note: binarySearch returns -|correct insert index| - 1 if it fails
+ // to find a match. For Java ints, this is the bitwise complement of the
+ // "correct" insertion index.
+ int searchIndex = Collections.binarySearch(mNearby, email);
+ if (result.isLost()) {
+ Integer old = counterMap.get(email);
+ counterMap.put(email, old == null ? 0 : Math.max(0, counterMap
+ .get(email) - 1));
+ // Remove the email if the counter indicates that we should.
+ if (counterMap.get(email) == 0 && searchIndex >= 0) {
+ mNearby.remove(searchIndex);
+ mPossibleAdapter.notifyItemRemoved(searchIndex);
+ }
+ } else {
+ Integer old = counterMap.get(email);
+ counterMap.put(email, old == null ? 1 : counterMap.get(email) + 1);
+ // Show the email if it's a new one and not equal to our email.
+ // TODO(alexfandrianto): This still lets you see emails of those
+ // nearby who you've already invited.
+ if (searchIndex < 0 && !email.equals(getParent().getEmail())) {
+ int insertIndex = ~searchIndex;
+ mNearby.add(insertIndex, email);
+ //mNearby.add(email);
+ mPossibleAdapter.notifyItemInserted(insertIndex);
+ }
+ }
+ return null;
+ }
+ });
+ Futures.addCallback(scan, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ handleScanningError(t);
+ }
+ });
+ } catch (VException e) {
+ handleScanningError(e);
+ }
+
+ 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();
+ if (!mTyped.contains(email)) {
+ mTyped.add(email);
+ }
+ mAdded.add(email);
+ rvPossible.getAdapter().notifyDataSetChanged();
+ 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) {
+ getParent().persistence.shareTodoList(mAdded);
+ // TODO(alexfandrianto/rosswang): removal
+ }
+ })
+ .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ }
+ })
+ .create();
+ }
+
+ private void handleScanningError(Throwable t) {
+ //TODO(rosswang): indicate error in the view
+ SyncbasePersistence.getAppErrorReporter().onError(R.string.err_scan_nearby, t);
+ }
+
+ public void onSharedToChanged() {
+ //TODO(rosswang)
+ }
+
+ private static class ContactAdapter extends RecyclerView.Adapter<ContactViewHolder> {
+ private final List<String> backup;
+ private final List<String> bonus;
+ private final Set<String> toggledOn;
+ private final boolean strikethrough; // If false, then bold.
+
+ public ContactAdapter(List<String> backup, Set<String> toggledOn, boolean strikethrough) {
+ super();
+ this.backup = backup;
+ this.bonus = null;
+ this.toggledOn = toggledOn;
+ this.strikethrough = strikethrough;
+ }
+
+ public ContactAdapter(List<String> backup, List<String> bonus, Set<String> toggledOn,
+ boolean strikethrough) {
+ super();
+ this.backup = backup;
+ this.bonus = bonus;
+ this.toggledOn = toggledOn;
+ this.strikethrough = strikethrough;
+ }
+
+ @Override
+ public ContactViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new ContactViewHolder(new TextView(parent.getContext()));
+ }
+
+ @Override
+ public void onBindViewHolder(final ContactViewHolder holder, int position) {
+ final String name = position < backup.size() ? backup.get(position) :
+ bonus.get(position - backup.size());
+ final boolean present = toggledOn.contains(name);
+ holder.bindString(name, present, strikethrough, new View.OnClickListener() {
+
+ @Override
+ public void onClick(View view) {
+ if (present) {
+ toggledOn.remove(name);
+ } else {
+ toggledOn.add(name);
+ }
+ notifyItemChanged(holder.getAdapterPosition());
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ int extra = bonus == null ? 0 : bonus.size();
+ return backup.size() + extra;
+ }
+ }
+
+ private static class ContactViewHolder extends RecyclerView.ViewHolder {
+ public ContactViewHolder(View itemView) {
+ super(itemView);
+ }
+
+ public void bindString(String name, boolean isActive, boolean strikethrough, View
+ .OnClickListener listener) {
+ TextView text = (TextView) itemView;
+
+ text.setText(name);
+ text.setTextSize(18);
+ if (strikethrough) {
+ if (isActive) {
+ text.setPaintFlags(text.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
+ } else {
+ text.setPaintFlags(text.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
+ }
+ } else {
+ // We should bold!
+ if (isActive) {
+ text.setTypeface(null, 1); // 1 is bold
+ } else {
+ text.setTypeface(null, 0); // 0 is default text style
+ }
+ }
+
+ text.setOnClickListener(listener);
+ }
+ }
+}
diff --git a/app/src/syncbase/java/io/v/todos/sharing/ShareListMenuFragment.java b/app/src/syncbase/java/io/v/todos/sharing/ShareListMenuFragment.java
new file mode 100644
index 0000000..70cc498
--- /dev/null
+++ b/app/src/syncbase/java/io/v/todos/sharing/ShareListMenuFragment.java
@@ -0,0 +1,79 @@
+// 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<>();
+
+ public void setSharedTo(List<String> sharedTo) {
+ mSharedTo = sharedTo;
+
+ ShareListDialogFragment dialog = ShareListDialogFragment.find(getChildFragmentManager());
+ if (dialog != null) {
+ dialog.onSharedToChanged();
+ }
+ }
+
+ public List<String> getSharedTo() {
+ return mSharedTo;
+ }
+
+ private String mEmail;
+ public void setEmail(String email) {
+ mEmail = email;
+ }
+ public String getEmail() {
+ return mEmail;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.menu_share, menu);
+ }
+
+ @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/syncbase/java/io/v/todos/sharing/Sharing.java b/app/src/syncbase/java/io/v/todos/sharing/Sharing.java
new file mode 100644
index 0000000..73bc757
--- /dev/null
+++ b/app/src/syncbase/java/io/v/todos/sharing/Sharing.java
@@ -0,0 +1,44 @@
+// 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 io.v.android.v23.V;
+import io.v.todos.persistence.syncbase.SyncbasePersistence;
+import io.v.v23.discovery.Discovery;
+import io.v.v23.verror.VException;
+
+public final class Sharing {
+ private Sharing(){}
+
+ private static final Object sDiscoveryMutex = new Object();
+ private static Discovery sDiscovery;
+
+ public static Discovery getDiscovery() {
+ return sDiscovery;
+ }
+
+ public static void initDiscovery() throws VException {
+ synchronized (sDiscoveryMutex) {
+ if (sDiscovery == null) {
+ sDiscovery = V.newDiscovery(SyncbasePersistence.getAppVContext());
+
+ // Rely on the neighborhood fragment to initialize presence advertisement.
+ NeighborhoodFragment.initSharePresence();
+ }
+ }
+ }
+
+ private static String getRootInterface() {
+ return SyncbasePersistence.getAppContext().getPackageName();
+ }
+
+ public static String getPresenceInterface() {
+ return getRootInterface() + ".presence";
+ }
+
+ public static String getInvitationInterface() {
+ return getRootInterface() + ".invitation";
+ }
+}
diff --git a/app/src/main/res/layout/sharing.xml b/app/src/syncbase/res/layout/sharing.xml
similarity index 100%
rename from app/src/main/res/layout/sharing.xml
rename to app/src/syncbase/res/layout/sharing.xml
diff --git a/app/src/syncbase/res/menu/menu_share.xml b/app/src/syncbase/res/menu/menu_share.xml
new file mode 100644
index 0000000..f74bd86
--- /dev/null
+++ b/app/src/syncbase/res/menu/menu_share.xml
@@ -0,0 +1,10 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context="io.v.todos.TodoListActivity">
+ <item
+ android:id="@+id/action_share"
+ android:orderInCategory="103"
+ android:title="@string/action_share"
+ app:showAsAction="never" />
+</menu>
diff --git a/app/src/syncbase/res/menu/sharing.xml b/app/src/syncbase/res/menu/neighborhood.xml
similarity index 100%
rename from app/src/syncbase/res/menu/sharing.xml
rename to app/src/syncbase/res/menu/neighborhood.xml
diff --git a/app/src/syncbase/res/values/strings.xml b/app/src/syncbase/res/values/strings.xml
index d6d2a51..c4538d2 100644
--- a/app/src/syncbase/res/values/strings.xml
+++ b/app/src/syncbase/res/values/strings.xml
@@ -1,4 +1,11 @@
<resources>
<string name="app_name">Syncbase Todos</string>
<string name="share_location">Share Location</string>
+ <!-- For Sharing Menu -->
+ <string name="sharing_already">Sharing With</string>
+ <string name="sharing_possible">Nearby</string>
+ <string name="sharing_custom_hint">Add an email address...</string>
+ <!-- Errors -->
+ <string name="err_share_location">Could not share location</string>
+ <string name="err_scan_nearby">Unable to scan for nearby users</string>
</resources>