blob: 69f8a747651643d9227a4f2a403d8261313a0c00 [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.todos.sharing;
import android.support.annotation.NonNull;
import android.util.Log;
import com.google.common.base.Function;
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.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import io.v.android.v23.V;
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.Advertisement;
import io.v.v23.discovery.Discovery;
import io.v.v23.discovery.Update;
import io.v.v23.security.BlessingPattern;
import io.v.v23.services.syncbase.Id;
import io.v.v23.syncbase.ChangeType;
import io.v.v23.syncbase.Syncbase;
import io.v.v23.syncbase.WatchChange;
import io.v.v23.verror.VException;
public final class Sharing {
private Sharing() {
}
private static final String TAG = "SHARING";
private static final String OWNER_KEY = "owner";
private static final Object sDiscoveryMutex = new Object();
private static Discovery sDiscovery;
private static VContext sScanContext;
private static VContext sAdvertiseContext;
private final static Map<String, VContext> sAdContextMap = new HashMap<>();
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();
sScanContext = initScanForInvites();
sAdvertiseContext = initAdvertiseLists();
}
}
}
// TODO(alexfandrianto): Nobody calls this, so we never stop sharing.
public static void stopDiscovery() {
synchronized (sDiscoveryMutex) {
sScanContext.cancel();
sAdvertiseContext.cancel();
sAdContextMap.clear();
}
}
private static String getRootInterface() {
return SyncbasePersistence.getAppContext().getPackageName();
}
// TODO(alexfandrianto): Make this "presence" and "invitation" once everyone migrates over.
public static String getPresenceInterface() {
return getRootInterface() + ".presence2";
}
public static String getInvitationInterface() {
return getRootInterface() + ".invitation2";
}
/**
* Starts a scanner seeking advertisements that invite this user to a todo list. When an invite
* is found, the app will automatically accept it.
*/
public static VContext initScanForInvites()
throws VException {
VContext vContext = SyncbasePersistence.getAppVContext().withCancel();
try {
ListenableFuture<Void> scan = InputChannels.withCallback(
Sharing.getDiscovery().scan(vContext,
"v.InterfaceName = \"" + Sharing.getInvitationInterface() + "\""),
new InputChannelCallback<Update>() {
@Override
public ListenableFuture<Void> onNext(Update result) {
final String listName = Iterables.getOnlyElement(result.getAddresses());
if (listName == null) {
return null;
}
String owner = result.getAttribute(OWNER_KEY);
Log.d("SHARING", "Noticed advertised list: " + listName + " by: " +
owner);
// TODO(alexfandrianto): Remove hack.
// https://github.com/vanadium/issues/issues/1328
if (result.getAttribute(SyncbasePersistence
.getPersonalBlessingsString()) == null) {
Log.d(TAG, "...but the ad was not meant for this user.");
return null; // ignore; this isn't meant for us
}
// Never mind about losses, just handle found advertisements.
if (!result.isLost()) {
Log.d(TAG, "...and will accept it.");
SyncbasePersistence.acceptSharedTodoList(new Id(owner, listName));
}
return null;
}
});
Futures.addCallback(scan, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
}
@Override
public void onFailure(Throwable t) {
handleScanListsError(t);
}
});
} catch (VException e) {
handleScanListsError(e);
}
return vContext;
}
private static void handleScanListsError(Throwable t) {
SyncbasePersistence.getAppErrorReporter().onError(R.string.err_scan_lists, t);
}
/**
* Creates advertisements based on the todo lists this user has created thus far and those that
* are created in the future. The advertisements will need to be targeted to the users that have
* been invited to the list.
*
* @return
* @throws VException
*/
public static VContext initAdvertiseLists()
throws VException {
final VContext vContext = SyncbasePersistence.getAppVContext().withCancel();
// Prepare a watch on top of the userdata collection to determine which todo lists need to
// be tracked by this application.
SyncbasePersistence.watchUserCollection(new InputChannelCallback<WatchChange>() {
@Override
public ListenableFuture<Void> onNext(WatchChange change) {
try {
final String listIdStr = change.getRowName();
final Id listId = SyncbasePersistence.convertStringToId(listIdStr);
if (change.getChangeType() == ChangeType.DELETE_CHANGE) {
VContext ctx = sAdContextMap.remove(listIdStr);
if (ctx != null) { // TODO(alexfandrianto): ctx might be null if ad failed?
ctx.cancel(); // Stop advertising the list; it's been deleted.
}
} else {
final String owner = listId.getBlessing();
if (!owner.equals(SyncbasePersistence.getPersonalBlessingsString())) {
return Futures.immediateFuture((Void) null);
}
// We should probably start to advertise this collection and check its spec.
SyncbasePersistence.watchSharedTo(listId, new Function<List<BlessingPattern>,
Void>() {
@Override
public Void apply(List<BlessingPattern> patterns) {
// Make a copy of the patterns list that excludes the cloud and this
// user's blessings.
List<BlessingPattern> filteredPatterns = new ArrayList<>();
for (BlessingPattern pattern : patterns) {
String pStr = pattern.toString();
if (pStr.equals(SyncbasePersistence.getPersonalBlessingsString()) ||
pStr.equals(SyncbasePersistence.CLOUD_BLESSING)) {
continue;
}
filteredPatterns.add(pattern);
}
// Advertise to the remaining patterns.
if (filteredPatterns.size() > 0) {
Log.d(TAG, "Must advertise for " + listIdStr + " to " +
filteredPatterns.toString());
advertiseList(vContext, listId, filteredPatterns);
}
return null;
}
});
}
} catch (Exception e) {
Log.w(TAG, "Error during watch handle", e);
}
return null;
}
});
return vContext;
}
/**
* Advertises that this list is available to this set of people. Cancels the old advertisement
* if one exists. Only called by initAdvertiseLists.
*
* @param baseContext The context for all advertisements
* @param listId The list to be advertised
* @param patterns Blessings that the advertisement should target
*/
private static void advertiseList(VContext baseContext, Id listId, List<BlessingPattern>
patterns) {
if (baseContext.isCanceled()) {
Log.w(TAG, "Base context was canceled; cannot advertise");
return;
}
// Swap out the ad context...
String key = SyncbasePersistence.convertIdToString(listId);
VContext oldAdContext = sAdContextMap.remove(key);
if (oldAdContext != null) {
oldAdContext.cancel();
}
VContext newAdContext = baseContext.withCancel();
sAdContextMap.put(key, newAdContext);
try {
Advertisement ad = new Advertisement();
ad.setInterfaceName(Sharing.getInvitationInterface());
ad.getAddresses().add(listId.getName());
ad.getAttributes().put(OWNER_KEY, listId.getBlessing());
// TODO(alexfandrianto): Remove hack. https://github.com/vanadium/issues/issues/1328
for (BlessingPattern pattern : patterns) {
ad.getAttributes().put(pattern.toString(), "");
}
Futures.addCallback(Sharing.getDiscovery().advertise(sAdvertiseContext, ad,
// TODO(alexfandrianto): Crypto crash if I use patterns instead of null.
// https://github.com/vanadium/issues/issues/1328 and
// https://github.com/vanadium/issues/issues/1331
null),
//patterns),
new FutureCallback<Void>() {
@Override
public void onSuccess(@android.support.annotation.Nullable Void result) {
}
@Override
public void onFailure(@NonNull Throwable t) {
handleAdvertiseListError(t);
}
});
} catch (VException e) {
handleAdvertiseListError(e);
}
}
private static void handleAdvertiseListError(Throwable t) {
SyncbasePersistence.getAppErrorReporter().onError(R.string.err_advertise_list, t);
}
}