Merge changes from topic 'baku-old'
* changes:
TBR: Renaming SyncbaseXAdapter -> RxXAdapter
More support for using ID list bindings
diff --git a/.gitignore b/.gitignore
index e5bb932..9ab0193 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
/.jiri
-#TODO(nlacasse): Get rid of .v23 below once v23->jiri transition is complete.
+
# Gradle dirs
build/
/buildSrc/schema
@@ -16,9 +16,6 @@
# vdl files
/src
-# Vanadium
-/.v23
-
# Generated source
generated-src/
-android-lib/src/main/jniLibs/
\ No newline at end of file
+android-lib/src/main/jniLibs/
diff --git a/android-lib/.gitignore b/android-lib/.gitignore
index e8fdac8..b8bd3b4 100644
--- a/android-lib/.gitignore
+++ b/android-lib/.gitignore
@@ -4,9 +4,6 @@
/bin/
/vdl_bin/
-# Vanadium
-/.v23
-
# Gradle
.gradle/
build/
diff --git a/android-lib/build.gradle b/android-lib/build.gradle
index 1f0511b..e96a85f 100644
--- a/android-lib/build.gradle
+++ b/android-lib/build.gradle
@@ -8,6 +8,7 @@
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4'
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0'
+ classpath 'io.v:gradle-plugin:1.4'
}
}
@@ -16,10 +17,11 @@
apply plugin: 'maven-publish'
apply plugin: 'wrapper'
apply plugin: 'com.jfrog.bintray'
+apply plugin: 'io.v.vdl'
// You should change this after releasing a new version of the library. See the
// list of published versions at https://repo1.maven.org/maven2/io/v/vanadium-android.
-def releaseVersion = '1.5'
+def releaseVersion = '1.7'
android {
buildToolsVersion '23.0.1'
@@ -46,6 +48,7 @@
compile project(':lib')
compile 'com.android.support:support-v4:23.0.1'
compile 'com.android.support:appcompat-v7:23.0.1'
+ compile "com.google.android.gms:play-services:8.3.0"
androidTestCompile 'junit:junit:4.12'
androidTestCompile 'com.google.truth:truth:0.25'
// This dependency exists only to work around an issue in the sdkmanager plugin v0.12.0. This
@@ -56,6 +59,10 @@
}
}
+vdl {
+ inputPaths += 'src/main/java'
+}
+
public static isDarwin() {
return getOS().contains("os x")
}
diff --git a/android-lib/src/main/AndroidManifest.xml b/android-lib/src/main/AndroidManifest.xml
index 800e17f..e83fd05 100644
--- a/android-lib/src/main/AndroidManifest.xml
+++ b/android-lib/src/main/AndroidManifest.xml
@@ -14,10 +14,45 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.NFC" />
+
<application>
- <activity
- android:name="io.v.android.impl.google.services.blessing.BlessingActivity"
+ <activity android:name="io.v.android.impl.google.services.blessing.BlessingActivity"
android:excludeFromRecents="true"/>
+ <receiver
+ android:name="com.google.android.gms.gcm.GcmReceiver"
+ android:exported="true"
+ android:permission="com.google.android.c2dm.permission.SEND" >
+ <intent-filter>
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ <category android:name="io.v.android" />
+ </intent-filter>
+ </receiver>
+ <service
+ android:name="io.v.android.impl.google.services.gcm.GcmTokenRefreshListenerService"
+ android:exported="false" >
+ <intent-filter>
+ <action android:name="com.google.android.gms.iid.InstanceID" />
+ </intent-filter>
+ </service>
+ <service
+ android:name="io.v.android.impl.google.services.gcm.GcmReceiveListenerService"
+ android:exported="false" >
+ </service>
+ <service
+ android:name="io.v.android.impl.google.services.gcm.GcmRegistrationService"
+ android:exported="false" >
+ </service>
+ <activity android:name="io.v.android.impl.google.services.beam.BeamActivity"
+ android:excludeFromRecents="true">
+ <intent-filter>
+ <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <data android:scheme="vnd.android.nfc"
+ android:host="ext"
+ android:pathPrefix="/io.v.android.vbeam:vbs"/>
+ </intent-filter>
+ </activity>
</application>
</manifest>
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/beam/BeamActivity.java b/android-lib/src/main/java/io/v/android/impl/google/services/beam/BeamActivity.java
new file mode 100644
index 0000000..2f973f5
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/beam/BeamActivity.java
@@ -0,0 +1,107 @@
+// 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.android.impl.google.services.beam;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.nfc.NdefMessage;
+import android.nfc.NfcAdapter;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.app.ActivityCompat;
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.joda.time.Duration;
+
+import io.v.android.v23.V;
+import io.v.android.v23.VBeam;
+import io.v.v23.OptionDefs;
+import io.v.v23.Options;
+import io.v.v23.context.VContext;
+import io.v.v23.security.VSecurity;
+
+/**
+ * Handles the NDEF discovered intent on the receiver phone.
+ * It contacts the VBeam server on the sending phone, then retrieves and starts the shared intent.
+ */
+public class BeamActivity extends Activity {
+ private static final String TAG = "BeamActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ NdefMessage msgs[] = null;
+ Intent intent = getIntent();
+
+ if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
+ Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
+ if (rawMsgs != null) {
+ msgs = new NdefMessage[rawMsgs.length];
+ for (int i = 0; i < rawMsgs.length; i++) {
+ msgs[i] = (NdefMessage) rawMsgs[i];
+ }
+ }
+ }
+ if (msgs == null) {
+ Log.d(TAG, "No ndef messages");
+ finish();
+ return;
+ }
+ VBeamManager.Data data = null;
+ for (NdefMessage m : msgs) {
+ data = VBeamManager.decodeMessage(m);
+ if (data != null)
+ break;
+ }
+ if (data == null) {
+ Log.w(TAG, "Unable to deserialize data");
+ finish();
+ return;
+ }
+ Log.d(TAG, "connecting to " + data.name);
+ VContext ctx = V.init(this).withTimeout(Duration.standardSeconds(2));
+ Options opts = new Options();
+
+ opts.set(OptionDefs.SERVER_AUTHORIZER, VSecurity.newPublicKeyAuthorizer(data.key));
+ IntentBeamerClient client = IntentBeamerClientFactory.getIntentBeamerClient(data.name);
+ ListenableFuture<IntentBeamerClient.GetIntentOut> out =
+ client.getIntent(ctx, data.secret, opts);
+ Futures.addCallback(out, new FutureCallback<IntentBeamerClient.GetIntentOut>() {
+ @Override
+ public void onSuccess(IntentBeamerClient.GetIntentOut result) {
+ try {
+ Log.d(TAG, "got intent " + result.intentUri);
+ int flags = 0;
+ if (result.intentUri.startsWith("intent:")) {
+ flags = Intent.URI_INTENT_SCHEME;
+ } else {
+ flags = Intent.URI_ANDROID_APP_SCHEME;
+ }
+ Intent resultIntent = Intent.parseUri(result.intentUri, flags);
+ resultIntent.putExtra(VBeamManager.EXTRA_VBEAM_PAYLOAD, result.payload);
+ startActivity(resultIntent);
+ finish();
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ t.printStackTrace();
+ finish();
+ }
+ });
+ }
+}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamManager.java b/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamManager.java
new file mode 100644
index 0000000..d6c008e
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamManager.java
@@ -0,0 +1,144 @@
+// 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.android.impl.google.services.beam;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcEvent;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.security.interfaces.ECPublicKey;
+
+import io.v.android.v23.VBeam;
+import io.v.v23.V;
+import io.v.v23.context.VContext;
+import io.v.v23.naming.Endpoint;
+import io.v.v23.security.VSecurity;
+import io.v.v23.verror.VException;
+
+/**
+ * Internal implementation of {@link VBeam} for an activity.
+ */
+public class VBeamManager implements NfcAdapter.CreateNdefMessageCallback {
+
+ static final String EXTERNAL_DOMAIN = "io.v.android.vbeam";
+ static final String EXTERNAL_TYPE = "vbs";
+ private static final String EXTERNAL_PATH = "/" + EXTERNAL_DOMAIN + ":" + EXTERNAL_TYPE;
+ private static final String TAG = "VBeamManager";
+ private final VBeamServer server;
+ private final VContext context;
+ private final String packageName;
+
+ /**
+ * Key for the payload bytes in a VBeam intent.
+ */
+ public static final String EXTRA_VBEAM_PAYLOAD = "io.v.intent.vbeam.payload";
+
+ /**
+ * Starts the VBeam server and registers the activity to send NDEF messages.
+ * @param context
+ * @param activity
+ * @param creator
+ * @throws VException
+ */
+ public VBeamManager(VContext context, Activity activity, VBeam.IntentCreator creator)
+ throws VException {
+ this.server = new VBeamServer(creator);
+ this.packageName = activity.getApplicationContext().getPackageName();
+ this.context = V.withNewServer(
+ context, "", this.server, VSecurity.newAllowEveryoneAuthorizer());
+ }
+
+ @Override
+ public NdefMessage createNdefMessage(NfcEvent event) {
+ try {
+ String requestID = server.newRequest();
+
+ byte[] payload;
+ try {
+ Data data = new Data();
+ data.secret = requestID;
+ data.key = V.getPrincipal(context).publicKey();
+ data.name = getEndpointName();
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(bos);
+ oos.writeObject(data);
+ oos.flush();
+ payload = bos.toByteArray();
+ oos.close();
+ bos.close();
+ } catch (IOException e) {
+ Log.e(TAG, "unable to encode ndef message", e);
+ return null;
+ }
+ return new NdefMessage(
+ new NdefRecord[]{
+ NdefRecord.createExternal(EXTERNAL_DOMAIN, EXTERNAL_TYPE, payload),
+ NdefRecord.createApplicationRecord(packageName)
+ }
+ );
+ } catch (Throwable t) {
+ Log.e(TAG, "createNdefMessage failed", t);
+ throw t;
+ }
+ }
+
+ static Data decodeMessage(NdefMessage m) {
+ for (NdefRecord r : m.getRecords()) {
+ Uri uri = r.toUri();
+ if (uri == null || !EXTERNAL_PATH.equals(uri.getPath())) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ StringBuilder description = new StringBuilder("ignoring ");
+ if (uri == null) {
+ description.append(r);
+ } else {
+ description.append(uri.getPath());
+ description.append(" != ");
+ description.append(EXTERNAL_PATH);
+ }
+ Log.d(TAG, description.toString());
+ }
+ continue;
+ }
+ byte[] payload = r.getPayload();
+ if (payload != null) {
+ ByteArrayInputStream bis = new ByteArrayInputStream(payload);
+ ObjectInputStream ois = null;
+ try {
+ ois = new ObjectInputStream(bis);
+ Object d = ois.readObject();
+ if (d != null && d instanceof Data) {
+ return (Data)d;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "decodeMessage", e);
+ } catch (ClassNotFoundException e) {
+ Log.e(TAG, "decodeMessage", e);
+ }
+ }
+ }
+ return null;
+ }
+
+ private String getEndpointName() {
+ Endpoint[] endpoints = V.getServer(context).getStatus().getEndpoints();
+ return endpoints[0].name();
+ }
+
+ static class Data implements Serializable {
+ String name;
+ String secret;
+ ECPublicKey key;
+ }
+}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamServer.java b/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamServer.java
new file mode 100644
index 0000000..3ace363
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/beam/VBeamServer.java
@@ -0,0 +1,84 @@
+// 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.android.impl.google.services.beam;
+
+import android.util.Pair;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import io.v.android.v23.VBeam;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.ServerCall;
+import io.v.v23.verror.VException;
+
+class VBeamServer implements IntentBeamerServer {
+ private static final long EXPIRATION_MS = 60000;
+
+ private final Map<String, Long> requestMap = new LinkedHashMap<>();
+ private final VBeam.IntentCreator callback;
+
+ public VBeamServer(VBeam.IntentCreator creator) throws VException {
+ this.callback = creator;
+ }
+
+ @Override
+ public synchronized ListenableFuture<GetIntentOut> getIntent(
+ VContext ctx, ServerCall call, String secret) {
+ evictStaleRequests();
+ if (requestMap.remove(secret) != null) {
+ try {
+ return Futures.transform(
+ callback.createIntent(ctx, call),
+ new AsyncFunction<Pair<String, byte[]>, GetIntentOut>() {
+
+ @Override
+ public ListenableFuture<GetIntentOut> apply(Pair<String, byte[]> input)
+ throws Exception {
+ return convertIntent(input);
+ }
+ });
+ } catch (Throwable t) {
+ return Futures.immediateFailedFuture(t);
+ }
+ }
+ return Futures.immediateFailedFuture(new VException("Bad request"));
+ }
+
+ private static ListenableFuture<GetIntentOut> convertIntent(Pair<String, byte[]> intent) {
+ GetIntentOut out = new GetIntentOut();
+ out.intentUri = intent.first;
+ out.payload = intent.second;
+ return Futures.immediateFuture(out);
+ }
+
+ private synchronized void evictStaleRequests() {
+ long now = System.currentTimeMillis();
+ for (Iterator<Long> it = requestMap.values().iterator(); it.hasNext();) {
+ long l = it.next();
+ if (l > now || l < now - EXPIRATION_MS) {
+ it.remove();
+ } else {
+ // LinkedHashMap is sorted by insertion order, so oldest entry is first.
+ break;
+ }
+ }
+ }
+
+ synchronized String newRequest() {
+ evictStaleRequests();
+ String secret = UUID.randomUUID().toString();
+ synchronized(requestMap) {
+ requestMap.put(secret, System.currentTimeMillis());
+ }
+ return secret;
+ }
+}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/beam/beamer.vdl b/android-lib/src/main/java/io/v/android/impl/google/services/beam/beamer.vdl
new file mode 100644
index 0000000..a61f8cd
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/beam/beamer.vdl
@@ -0,0 +1,9 @@
+// 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 beam
+
+type IntentBeamer interface {
+ GetIntent(secret string) (intentUri string, payload []byte | error)
+}
\ No newline at end of file
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmReceiveListenerService.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmReceiveListenerService.java
new file mode 100644
index 0000000..2b2844b
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmReceiveListenerService.java
@@ -0,0 +1,28 @@
+// 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.android.impl.google.services.gcm;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import com.google.android.gms.gcm.GcmListenerService;
+
+import io.v.v23.verror.VException;
+
+/**
+ * Listens for GCM messages and wakes up services upon their receipt.
+ */
+public class GcmReceiveListenerService extends GcmListenerService {
+ private static final String TAG = "GcmRecvListenerService";
+
+ @Override
+ public void onMessageReceived(String from, Bundle data) {
+ try {
+ Util.wakeupServices(this, false);
+ } catch (VException e) {
+ Log.e(TAG, "Couldn't wakeup services.", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmRegistrationService.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmRegistrationService.java
new file mode 100644
index 0000000..45936b7
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmRegistrationService.java
@@ -0,0 +1,53 @@
+// 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.android.impl.google.services.gcm;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import com.google.android.gms.gcm.GoogleCloudMessaging;
+import com.google.android.gms.iid.InstanceID;
+
+import java.io.IOException;
+
+import io.v.v23.verror.VException;
+
+/**
+ * Communicates with GCM servers to obtain a new registration token and then starts
+ * all app services that have registered themselves as wake-able.
+ */
+public class GcmRegistrationService extends IntentService {
+ private static final String TAG = "GcmRegistrationService";
+ public static final String GCM_TOKEN_PREF_KEY = "io.v.android.impl.google.services.gcm.TOKEN";
+ static final String EXTRA_RESTART_SERVICES = "RESTART_SERVICES";
+
+ public GcmRegistrationService() {
+ super("GcmRegistrationService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ try {
+ InstanceID instanceID = InstanceID.getInstance(this);
+ String token = instanceID.getToken("*", GoogleCloudMessaging.INSTANCE_ID_SCOPE);
+ // Store registration token in SharedPreferences.
+ SharedPreferences.Editor editor =
+ PreferenceManager.getDefaultSharedPreferences(this).edit();
+ editor.putString(GCM_TOKEN_PREF_KEY, token);
+ editor.commit();
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't fetch GCM registration token: ", e);
+ }
+ boolean restartServices = intent.getBooleanExtra(EXTRA_RESTART_SERVICES, false);
+ try {
+ Util.wakeupServices(this, restartServices);
+ } catch (VException e) {
+ Log.e(TAG, "Couldn't wakeup services.", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmTokenRefreshListenerService.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmTokenRefreshListenerService.java
new file mode 100644
index 0000000..a95eb5f
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/GcmTokenRefreshListenerService.java
@@ -0,0 +1,21 @@
+// 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.android.impl.google.services.gcm;
+
+import android.content.Intent;
+
+import com.google.android.gms.iid.InstanceIDListenerService;
+
+/**
+ * Listens for server token change notifications and initiates token (and services) refresh.
+ */
+public class GcmTokenRefreshListenerService extends InstanceIDListenerService {
+ @Override
+ public void onTokenRefresh() {
+ Intent intent = new Intent(this, GcmRegistrationService.class);
+ intent.putExtra(GcmRegistrationService.EXTRA_RESTART_SERVICES, true);
+ startService(intent);
+ }
+}
\ No newline at end of file
diff --git a/android-lib/src/main/java/io/v/android/impl/google/services/gcm/Util.java b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/Util.java
new file mode 100644
index 0000000..a0ecc23
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/gcm/Util.java
@@ -0,0 +1,90 @@
+// 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.android.impl.google.services.gcm;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.v23.verror.VException;
+
+/**
+ * Utility GCM methods.
+ */
+class Util {
+ // Wakes up all services that have marked themselves for wakeup.
+ // If restart==true, services are first stopped and then started; otherwise, they
+ // are just started.
+ static void wakeupServices(Context context, boolean restart) throws VException {
+ for (ServiceInfo service : getWakeableServices(context)) {
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(service.packageName, service.name));
+ if (restart) {
+ context.stopService(intent);
+ }
+ context.startService(intent);
+ }
+ }
+
+ // Returns a list of all services that have marked themselves for wakeup.
+ static List<ServiceInfo> getWakeableServices(Context context) throws VException {
+ try {
+ ServiceInfo[] services = context.getPackageManager().getPackageInfo(
+ context.getPackageName(),
+ PackageManager.GET_META_DATA|PackageManager.GET_SERVICES).services;
+ if (services == null) {
+ throw new VException("Couldn't get services information for package: " +
+ context.getPackageName());
+ }
+ ArrayList<ServiceInfo> ret = new ArrayList<>();
+ for (ServiceInfo service : services) {
+ if (service == null) continue;
+ if (!service.packageName.equals(context.getPackageName())) continue;
+ if (service.metaData == null) continue;
+ boolean wakeup = service.metaData.getBoolean("wakeup", false);
+ if (!wakeup) continue;
+ ret.add(service);
+ }
+ return ret;
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new VException(String.format(
+ "Couldn't get package information for package %s: %s",
+ context.getPackageName(), e.getMessage()));
+ }
+ }
+
+ /**
+ * Returns {@code true} iff the provided service is "wakeable", i.e., if it has the
+ * {@code "wakeup"} metadata attached to it.
+ *
+ * @param context Android context representing the service
+ * @return {@code true} iff the provided service is "wakeable"
+ * @throws VException if there was an error figuring out if a service is wakeable
+ */
+ public static boolean isServiceWakeable(Context context) throws VException {
+ if (!(context instanceof Service)) {
+ return false;
+ }
+ Service service = (Service) context;
+ try {
+ ServiceInfo info = service.getPackageManager().getServiceInfo(
+ new ComponentName(service, service.getClass()), PackageManager.GET_META_DATA);
+ if (info.metaData == null) {
+ return false;
+ }
+ return info.metaData.getBoolean("wakeup", false);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new VException(e.getMessage());
+ }
+ }
+
+ private Util() {}
+}
diff --git a/android-lib/src/main/java/io/v/android/v23/V.java b/android-lib/src/main/java/io/v/android/v23/V.java
index 6bd8920..4c849d8 100644
--- a/android-lib/src/main/java/io/v/android/v23/V.java
+++ b/android-lib/src/main/java/io/v/android/v23/V.java
@@ -5,12 +5,13 @@
package io.v.android.v23;
import android.content.Context;
+import android.content.Intent;
import com.google.common.base.Preconditions;
+import io.v.android.impl.google.services.gcm.GcmRegistrationService;
import io.v.v23.Options;
import io.v.v23.context.VContext;
-import io.v.v23.rpc.Server;
import io.v.v23.security.Blessings;
import io.v.v23.security.BlessingStore;
import io.v.v23.security.Constants;
@@ -80,11 +81,18 @@
throw new RuntimeException("Couldn't setup Vanadium principal", e);
}
globalContext = ctx;
+ // Start the GCM registration service, which obtains the GCM token for the app
+ // (if the app is configured to use GCM).
+ // NOTE: this call may lead to a recursive call to V.init() (in a separate thread),
+ // so keep this code below the line where 'globalContext' is set, or we may run
+ // into an infinite recursion.
+ Intent intent = new Intent(androidCtx, GcmRegistrationService.class);
+ androidCtx.startService(intent);
return ctx;
}
}
- // Inializes Vanadium state that's local to the invoking activity/service.
+ // Initializes Vanadium state that's local to the invoking activity/service.
private static VContext initAndroidLocal(VContext ctx, Context androidCtx, Options opts) {
return ctx.withValue(new AndroidContextKey(), androidCtx);
}
diff --git a/android-lib/src/main/java/io/v/android/v23/VBeam.java b/android-lib/src/main/java/io/v/android/v23/VBeam.java
new file mode 100644
index 0000000..41ee7b6
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/v23/VBeam.java
@@ -0,0 +1,92 @@
+// 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.android.v23;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.nfc.NfcAdapter;
+import android.util.Pair;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import io.v.android.impl.google.services.beam.VBeamManager;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.ServerCall;
+import io.v.v23.security.Call;
+import io.v.v23.verror.VException;
+
+/**
+ * Enables sending authenticated data over Android Beam.
+ * <p>
+ * To support Beam in an activity you register an {@link io.v.android.v23.VBeam.IntentCreator}.
+ * When the user initiates an NFC tap, the framework will call
+ * {@link io.v.android.v23.VBeam.IntentCreator#createIntent(VContext, ServerCall)}. You should
+ * return an intent encapsulating the data you wish to share.
+ * <p>
+ * Internally this starts a Vanadium service on this phone. After tapping the app will start on
+ * the destination phone and contact the sender. You may then inspect the credentials the app
+ * is using on the destination phone and decide what data to send. For example, you may
+ * add their blessing to an ACL and then send the name of the object you are trying to share.
+ * <p>
+ * Beaming will fail if the app is in the foreground on both phones. The destination
+ * phone should not have the app open.
+ * <p>
+ * If the app is not installed on the destination phone, it will open in the Play store.
+ */
+public class VBeam {
+ private final static String TAG = "VBeam";
+
+ /**
+ * A callback to be invoked when Beam has been inititaed to another device.
+ */
+ public interface IntentCreator {
+ /**
+ * Create the intent to transfer to the destination phone.
+ * <p>
+ * This runs on the sender phone. The credentials for the receiver phone are available
+ * in {@code call}. For example you can use
+ * {@link io.v.v23.security.VSecurity#getRemoteBlessingNames(VContext, Call)} to find the
+ * receiver's blessings.
+ * <p>
+ * You should return an intent URI (as returned by {@link Intent#toUri(int)}), and
+ * optionally a byte array payload. This intent will be started on the receiver phone.
+ */
+ ListenableFuture<Pair<String, byte[]>> createIntent(VContext context, ServerCall call);
+ }
+
+ /**
+ * Set a callback that dynamically generates an Intent to send using Android Beam.
+ * <p>
+ * You should call this method during your Activity's onCreate(). You should cancel the context
+ * in your Activity's onDestroy(), but not before.
+ * <p>
+ * Do not mix calls to VBeam and NfcAdapter for the same activity.
+ */
+ public static boolean setBeamIntentCallback(VContext context,
+ Activity activity,
+ IntentCreator creator) throws VException {
+ NfcAdapter nfc = NfcAdapter.getDefaultAdapter(activity);
+ if (nfc == null)
+ return false;
+ if (creator == null) {
+ nfc.setNdefPushMessageCallback(null, activity);
+ return true;
+ }
+
+ final VBeamManager vBeamManager = new VBeamManager(context, activity, creator);
+ nfc.setNdefPushMessageCallback(vBeamManager, activity);
+ return true;
+ }
+
+ private VBeam() { } // static
+
+ /**
+ * Returns the {@code byte[]} payload from an intent sent using VBeam.
+ * Call this on the receiving device to retrieve your payload.
+ */
+ public static byte[] getBeamPayload(Intent intent) {
+ return intent.getByteArrayExtra(VBeamManager.EXTRA_VBEAM_PAYLOAD);
+ }
+}
diff --git a/benchmarks/rpcbench/android/build.gradle b/benchmarks/rpcbench/android/build.gradle
index 120f1fb..11e6b95 100644
--- a/benchmarks/rpcbench/android/build.gradle
+++ b/benchmarks/rpcbench/android/build.gradle
@@ -6,7 +6,7 @@
dependencies {
classpath 'com.android.tools.build:gradle:1.3.0'
classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0'
- classpath 'io.v:gradle-plugin:1.3'
+ classpath 'io.v:gradle-plugin:1.4'
}
}
@@ -43,7 +43,7 @@
}
dependencies {
- compile 'io.v:vanadium-android:1.0'
+ compile 'io.v:vanadium-android:1.6'
compile 'com.android.support:appcompat-v7:23.1.0'
androidTestCompile 'junit:junit:4.12'
}
diff --git a/benchmarks/rpcbench/android/src/androidTest/java/io/v/rpcbench/AndroidRpcBenchmark.java b/benchmarks/rpcbench/android/src/androidTest/java/io/v/rpcbench/AndroidRpcBenchmark.java
index 2652baf..b77fa5f 100644
--- a/benchmarks/rpcbench/android/src/androidTest/java/io/v/rpcbench/AndroidRpcBenchmark.java
+++ b/benchmarks/rpcbench/android/src/androidTest/java/io/v/rpcbench/AndroidRpcBenchmark.java
@@ -4,22 +4,24 @@
package io.v.rpcbench;
-import android.os.Debug;
import android.test.AndroidTestCase;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Arrays;
-import java.util.Iterator;
import io.v.android.v23.V;
+import io.v.v23.InputChannelCallback;
+import io.v.v23.InputChannels;
import io.v.v23.VFutures;
import io.v.v23.context.VContext;
import io.v.v23.naming.Endpoint;
import io.v.v23.rpc.ListenSpec;
-import io.v.v23.rpc.ReflectInvoker;
import io.v.v23.rpc.Server;
import io.v.v23.rpc.ServerCall;
import io.v.v23.security.VSecurity;
@@ -39,23 +41,19 @@
VContext serverContext = io.v.v23.V
.withNewServer(listenSpec, "", new EchoServer() {
@Override
- public byte[] echo(VContext ctx, ServerCall call, byte[] payload)
- throws VException {
- return payload;
+ public ListenableFuture<byte[]> echo(VContext ctx, ServerCall call, byte[] payload) {
+ return Futures.immediateFuture(payload);
}
@Override
- public void echoStream(VContext ctx, ServerCall call,
- ServerStream<byte[], byte[]> stream)
- throws VException {
- Iterator<byte[]> byteIterator = stream.iterator();
- while (byteIterator.hasNext()) {
- byte[] payload = byteIterator.next();
- stream.send(payload);
- }
- if (stream.error() != null) {
- throw stream.error();
- }
+ public ListenableFuture<Void> echoStream(VContext ctx, ServerCall call,
+ final ServerStream<byte[], byte[]> stream) {
+ return InputChannels.withCallback(stream, new InputChannelCallback<byte[]>() {
+ @Override
+ public ListenableFuture<Void> onNext(byte[] result) {
+ return stream.send(result);
+ }
+ });
}
}, VSecurity.newAllowEveryoneAuthorizer());
Server echoServer = V.getServer(serverContext);
@@ -75,11 +73,9 @@
VFutures.sync(echoClient.echo(baseContext, payload));
}
for (int i = 0; i < 1000; i++) {
- if (i == 999) { Debug.startMethodTracing(); }
long start = System.nanoTime();
VFutures.sync(echoClient.echo(baseContext, payload));
long end = System.nanoTime();
- if (i == 999) { Debug.stopMethodTracing(); }
long duration = end - start;
if (i == 0) {
cma = duration;
@@ -91,7 +87,7 @@
}
public void testSingleEncoder() throws IOException, ConversionException {
- int nreps = 1;
+ int nreps = 1000;
PipedOutputStream outStream = new PipedOutputStream();
PipedInputStream inStream = new PipedInputStream(outStream);
BinaryEncoder encoder = new BinaryEncoder(outStream);
@@ -120,7 +116,7 @@
}
public void testNewEncoderEachTime() throws VException {
- int nreps = 1;
+ int nreps = 1000;
byte[] payload = new byte[] { 0x01 };
double cma = 0;
for (int i = 0; i < nreps; i++) {
diff --git a/benchmarks/rpcbench/build.gradle b/benchmarks/rpcbench/build.gradle
index 939196d..3048e68 100644
--- a/benchmarks/rpcbench/build.gradle
+++ b/benchmarks/rpcbench/build.gradle
@@ -26,6 +26,10 @@
compile 'com.google.caliper:caliper:1.0-beta-2'
}
+static isDarwin() {
+ return System.properties['os.name'].toLowerCase().contains("os x")
+}
+
task wrapper(type: Wrapper) {
gradleVersion = '2.7'
}
@@ -42,3 +46,9 @@
tasks.run.dependsOn libProject.tasks.copyVanadiumLib
tasks.run.jvmArgs "-Djava.library.path=${libProject.buildDir}/libs"
+tasks.run.jvmArgs "-XX:-TieredCompilation" // needed for caliper
+if (isDarwin()) {
+ // See: https://github.com/vanadium/issues/issues/567
+ tasks.run.jvmArgs "-XX:+UnlockDiagnosticVMOptions"
+ tasks.run.jvmArgs "-XX:-LogEvents"
+}
diff --git a/benchmarks/syncbench/build.gradle b/benchmarks/syncbench/build.gradle
index 1b5f34c..3d56f09 100644
--- a/benchmarks/syncbench/build.gradle
+++ b/benchmarks/syncbench/build.gradle
@@ -13,6 +13,10 @@
compile 'com.google.caliper:caliper:1.0-beta-2'
}
+static isDarwin() {
+ return System.properties['os.name'].toLowerCase().contains("os x")
+}
+
task copyLib(type: Copy) {
from([project(':lib').buildDir.getAbsolutePath(), 'libs', 'libv23.so'].join(File.separator))
destinationDir = new File(['src', 'main', 'resources'].join(File.separator))
@@ -27,3 +31,14 @@
clean {
delete 'src/main/resources/libv23.so'
}
+
+def libProject = rootProject.findProject(':lib')
+
+tasks.run.dependsOn libProject.tasks.copyVanadiumLib
+tasks.run.jvmArgs "-Djava.library.path=${libProject.buildDir}/libs"
+tasks.run.jvmArgs "-XX:-TieredCompilation" // needed for caliper
+if (isDarwin()) {
+ // See: https://github.com/vanadium/issues/issues/567
+ tasks.run.jvmArgs "-XX:+UnlockDiagnosticVMOptions"
+ tasks.run.jvmArgs "-XX:-LogEvents"
+}
\ No newline at end of file
diff --git a/benchmarks/syncbench/src/main/java/io/v/syncbench/SyncbaseBenchmark.java b/benchmarks/syncbench/src/main/java/io/v/syncbench/SyncbaseBenchmark.java
index 2095c3e..11978b2 100644
--- a/benchmarks/syncbench/src/main/java/io/v/syncbench/SyncbaseBenchmark.java
+++ b/benchmarks/syncbench/src/main/java/io/v/syncbench/SyncbaseBenchmark.java
@@ -79,7 +79,7 @@
@AfterExperiment
public void tearDown() throws VException {
- syncbaseServer.stop();
+ baseContext.cancel();
}
@Benchmark
diff --git a/lib/.gitignore b/lib/.gitignore
index 4cf7f3a..ebdc8b5 100644
--- a/lib/.gitignore
+++ b/lib/.gitignore
@@ -2,9 +2,6 @@
/bin/
/vdl_bin/
-# Vanadium
-/.v23
-
# Gradle
.gradle/
build/
diff --git a/lib/build.gradle b/lib/build.gradle
index 72d3cb4..daa8186 100644
--- a/lib/build.gradle
+++ b/lib/build.gradle
@@ -23,7 +23,7 @@
// You should change this after releasing a new version of the library. See the
// list of published versions at https://repo1.maven.org/maven2/io/v/vanadium.
-def releaseVersion = '1.5'
+def releaseVersion = '1.7'
dependencies {
compile group: 'joda-time', name: 'joda-time', version: '2.7'
diff --git a/lib/src/main/java/io/v/v23/rpc/ReflectInvoker.java b/lib/src/main/java/io/v/v23/rpc/ReflectInvoker.java
index 7a05351..a8e802d 100644
--- a/lib/src/main/java/io/v/v23/rpc/ReflectInvoker.java
+++ b/lib/src/main/java/io/v/v23/rpc/ReflectInvoker.java
@@ -282,7 +282,7 @@
} catch (InvocationTargetException | IllegalAccessException e) {
ret.setException(new VException(String.format(
"Error invoking method %s: %s",
- method, e.getCause().getMessage())));
+ method, e.getCause().toString())));
}
}
});
diff --git a/lib/src/main/java/io/v/v23/syncbase/nosql/Database.java b/lib/src/main/java/io/v/v23/syncbase/nosql/Database.java
index 9367729..fe2d0f3 100644
--- a/lib/src/main/java/io/v/v23/syncbase/nosql/Database.java
+++ b/lib/src/main/java/io/v/v23/syncbase/nosql/Database.java
@@ -210,9 +210,7 @@
/**
* Compares the current schema version of the database with the schema version provided while
- * creating this database handle. If the current database schema version is lower, then the
- * {@link SchemaUpgrader} associated with the schema is called. If {@link SchemaUpgrader} is
- * successful, this method stores the new schema metadata in database.
+ * creating this database handle and updates the schema metadata if required.
* <p>
* This method also registers a conflict resolver with syncbase to receive conflicts.
* <p>
diff --git a/lib/src/main/java/io/v/v23/syncbase/nosql/Schema.java b/lib/src/main/java/io/v/v23/syncbase/nosql/Schema.java
index 8d007f6..0a6da8e 100644
--- a/lib/src/main/java/io/v/v23/syncbase/nosql/Schema.java
+++ b/lib/src/main/java/io/v/v23/syncbase/nosql/Schema.java
@@ -12,23 +12,20 @@
* Each database has a schema associated with it which defines the current version of the
* database. When a new version of an app wishes to change its data in a way that it is not
* compatible with the old app's data, this app must change the schema version and provide relevant
- * upgrade logic in the specified {@link SchemaUpgrader}. The conflict resolution rules are also
- * associated with the schema version. Hence if the conflict resolution rules change then the schema
- * version also must be bumped.
+ * upgrade logic. The conflict resolution rules are also associated with the schema version. Hence
+ * if the conflict resolution rules change then the schema version also must be bumped.
*/
public class Schema {
private final SchemaMetadata metadata;
- private final SchemaUpgrader upgrader;
private final ConflictResolver resolver;
/**
- * Creates a new database schema with the specified metadata and schema upgrader.
+ * Creates a new database schema with the specified metadata and conflict resolver.
* <p>
- * Note: {@link SchemaUpgrader} is purely local and is not persisted.
+ * Note: {@link ConflictResolver} is purely local and is not persisted.
*/
- public Schema(SchemaMetadata metadata, SchemaUpgrader upgrader, ConflictResolver resolver) {
+ public Schema(SchemaMetadata metadata, ConflictResolver resolver) {
this.metadata = metadata;
- this.upgrader = upgrader;
this.resolver = resolver;
}
@@ -38,12 +35,6 @@
public SchemaMetadata getMetadata() { return this.metadata; }
/**
- * Returns the upgrade logic used for upgrading the schema when an app's schema version differs
- * from the database's schema version.
- */
- public SchemaUpgrader getUpgrader() { return this.upgrader; }
-
- /**
* Returns a resolver that is used for conflict resolution.
*/
public ConflictResolver getResolver() { return this.resolver; }
diff --git a/lib/src/main/java/io/v/v23/syncbase/nosql/SchemaUpgrader.java b/lib/src/main/java/io/v/v23/syncbase/nosql/SchemaUpgrader.java
deleted file mode 100644
index 2d113eb..0000000
--- a/lib/src/main/java/io/v/v23/syncbase/nosql/SchemaUpgrader.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright 2015 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.v23.syncbase.nosql;
-
-import io.v.v23.verror.VException;
-
-/**
- * An interface that must be implemented by an app in order to upgrade the database schema from
- * a lower version to a higher version.
- */
-public interface SchemaUpgrader {
- /**
- * Updgrades database from an old to the new schema version.
- * <p>
- * This method must be idempotent.
- *
- * @param db database to be upgraded
- * @param oldVersion old schema version
- * @param newVersion new schema version
- * @throws VException if the database couldn't be upgraded
- */
- void run(Database db, int oldVersion, int newVersion) throws VException;
-}
\ No newline at end of file
diff --git a/projects/moments/app/build.gradle b/projects/moments/app/build.gradle
index c87af77..9637052 100644
--- a/projects/moments/app/build.gradle
+++ b/projects/moments/app/build.gradle
@@ -1,4 +1,3 @@
-
buildscript {
repositories {
jcenter()
@@ -34,6 +33,8 @@
targetSdkVersion 23
versionCode 1
versionName "1.0"
+ // Enabling multidex support.
+ multiDexEnabled true
}
buildTypes {
release {
@@ -50,7 +51,8 @@
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile 'com.android.support:support-v4:23.1.1'
- compile 'io.v:vanadium-android:1.6'
+ compile 'com.android.support:multidex:1.0.0'
+ compile 'io.v:vanadium-android:1.7'
}
vdl {
diff --git a/projects/moments/app/src/main/AndroidManifest.xml b/projects/moments/app/src/main/AndroidManifest.xml
index 27cf748..1b19978 100644
--- a/projects/moments/app/src/main/AndroidManifest.xml
+++ b/projects/moments/app/src/main/AndroidManifest.xml
@@ -23,6 +23,7 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:name="android.support.multidex.MultiDexApplication"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/AdConverter.java b/projects/moments/app/src/main/java/io/v/moments/ifc/AdConverter.java
deleted file mode 100644
index 13dcecf..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/AdConverter.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2015 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.moments.ifc;
-
-import io.v.v23.discovery.Service;
-
-/**
- * The io.v.v23.discovery.Service isn't a service, it's the *description* of a
- * service found in a discovery advertisement.
- *
- * Implementations of this interface construct an instance of T from the
- * attributes in Service, and/or by making an RPC to the real underlying service
- * described by the advertisement to get data needed to make a T.
- */
-public interface AdConverter<T> {
- T make(Service service);
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/Advertiser.java b/projects/moments/app/src/main/java/io/v/moments/ifc/Advertiser.java
deleted file mode 100644
index f3e3f26..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/Advertiser.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2015 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.moments.ifc;
-
-import com.google.common.util.concurrent.FutureCallback;
-
-/**
- * Advertiser controls, intended to be similar to Scanner controls.
- */
-public interface Advertiser {
- /**
- * Asynchronously start advertising.
- *
- * Callbacks can be expected to run on the UX thread.
- *
- * @param startupCallback executed on success or failure of advertising
- * startup.
- * @param completionCallback executed on success or failure of advertising
- * completion. An advertisement might shutdown
- * for reasons other than a call to stop, e.g. a
- * timeout.
- */
- void start(FutureCallback<Void> startupCallback,
- FutureCallback<Void> completionCallback);
-
- /**
- * True if stop could usefully be called.
- */
- boolean isAdvertising();
-
- /**
- * Synchronously stop advertising. Should result in execution of
- * completionCallback.
- */
- void stop();
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/Moment.java b/projects/moments/app/src/main/java/io/v/moments/ifc/Moment.java
index bf0fba1..74a5fb1 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/Moment.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ifc/Moment.java
@@ -11,12 +11,13 @@
import org.joda.time.format.DateTimeFormatter;
import io.v.moments.lib.Id;
-import io.v.v23.discovery.Attributes;
/**
* A photo with ancillary information.
*/
public interface Moment extends HasId {
+ DateTimeFormatter FMT = DateTimeFormat.forPattern("yyyyMMdd_HHmmss");
+
/**
* A unique moment ID valid for the life of the app.
*/
@@ -55,8 +56,8 @@
/**
* An advertiser can be scheduled to start advertising, but not actually be
* advertising yet. There's also a lag to stop advertising. This member
- * tracks the desired eventual state, to maintain sensible UX, through
- * phone rotations and whatnot.
+ * tracks the desired eventual state, to maintain sensible UX, through phone
+ * rotations and whatnot.
*/
AdState getDesiredAdState();
@@ -70,12 +71,6 @@
*/
boolean hasPhoto(Kind kind, Style style);
-
- /**
- * From this, make a set of discovery 'attributes'.
- */
- Attributes makeAttributes();
-
/**
* Get the specified photo.
*/
@@ -104,7 +99,5 @@
HUGE, FULL, THUMB
}
- enum AdState { ON, OFF }
-
- DateTimeFormatter FMT = DateTimeFormat.forPattern("yyyyMMdd_HHmmss");
+ enum AdState {ON, OFF}
}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/MomentFactory.java b/projects/moments/app/src/main/java/io/v/moments/ifc/MomentFactory.java
index 2b9dd01..6315030 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/MomentFactory.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ifc/MomentFactory.java
@@ -14,17 +14,19 @@
* Makes moments, converts them to and from other formats.
*/
public interface MomentFactory {
+ Moment make(Id id, int index, String author, String caption);
+
void toPrefs(SharedPreferences.Editor editor, String prefix, Moment m);
+ Moment fromPrefs(SharedPreferences p, String prefix);
+
void toBundle(Bundle b, String prefix, Moment m);
Moment fromBundle(Bundle b, String prefix);
- Moment make(Id id, int index, String author, String caption);
+ Attributes toAttributes(Moment moment);
- Moment makeFromAttributes(Id id, int ordinal, Attributes attr);
-
- Moment fromPrefs(SharedPreferences p, String prefix);
+ Moment fromAttributes(Id id, int ordinal, Attributes attr);
enum F {
DATE, AUTHOR, CAPTION, ORDINAL, ID, ADVERTISING
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/ScanListener.java b/projects/moments/app/src/main/java/io/v/moments/ifc/ScanListener.java
deleted file mode 100644
index e7eed01..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/ScanListener.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2015 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.moments.ifc;
-
-import io.v.v23.discovery.Update;
-
-/**
- * Implementations of this interface can be registered with
- * {@link io.v.moments.lib.V23Manager#scan} to receive notifications when discovery events occur.
- */
-public interface ScanListener {
- void scanUpdateReceived(Update result);
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/Scanner.java b/projects/moments/app/src/main/java/io/v/moments/ifc/Scanner.java
deleted file mode 100644
index b9084d9..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/Scanner.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2015 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.moments.ifc;
-
-import com.google.common.util.concurrent.FutureCallback;
-
-import io.v.v23.InputChannelCallback;
-import io.v.v23.discovery.Update;
-
-/**
- * Scanner controls, intended to be similar to Advertiser controls.
- */
-public interface Scanner {
- /**
- * Asynchronously start scanning.
- *
- * Callbacks can be expected to run on the UX thread.
- *
- * @param startupCallback executed on success or failure of scan
- * startup.
- * @param updateCallback executed on each scan update (each found or
- * lost advertisement).
- * @param completionCallback executed on success or failure of scan
- * completion. A scan might shutdown for reasons
- * other than a call to stop, e.g. a timeout.
- */
- void start(FutureCallback<Void> startupCallback,
- InputChannelCallback<Update> updateCallback,
- FutureCallback<Void> completionCallback);
-
- /**
- * True if stop could usefully be called.
- */
- boolean isScanning();
-
- /**
- * Synchronously stop scanning. Should result in execution of
- * completionCallback.
- */
- void stop();
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ifc/package-info.java b/projects/moments/app/src/main/java/io/v/moments/ifc/package-info.java
index f7e96e4..6e57bcc 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ifc/package-info.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ifc/package-info.java
@@ -3,6 +3,6 @@
// license that can be found in the LICENSE file.
/**
- * Iterfaces used by the moments app.
+ * Interfaces used by the moments app.
*/
package io.v.moments.ifc;
diff --git a/projects/moments/app/src/main/java/io/v/moments/lib/DiscoveredList.java b/projects/moments/app/src/main/java/io/v/moments/lib/DiscoveredList.java
index c4cd093..dbc3f22 100644
--- a/projects/moments/app/src/main/java/io/v/moments/lib/DiscoveredList.java
+++ b/projects/moments/app/src/main/java/io/v/moments/lib/DiscoveredList.java
@@ -6,17 +6,19 @@
import android.os.Handler;
-import io.v.moments.ifc.AdConverter;
import io.v.moments.ifc.HasId;
import io.v.moments.ifc.IdSet;
-import io.v.moments.ifc.ScanListener;
+import io.v.moments.v23.ifc.AdConverter;
+import io.v.moments.v23.ifc.AdvertisementFoundListener;
+import io.v.moments.v23.ifc.AdvertisementLostListener;
import io.v.v23.discovery.Service;
-import io.v.v23.discovery.Update;
/**
* List that updates itself in response to found or lost advertisements.
*/
-public class DiscoveredList<T extends HasId> extends ObservedList<T> implements ScanListener {
+public class DiscoveredList<T extends HasId>
+ extends ObservedList<T>
+ implements AdvertisementFoundListener, AdvertisementLostListener {
private static final String TAG = "DiscoveredList";
private final Handler mHandler;
@@ -43,32 +45,19 @@
mHandler = handler;
}
- @Override
- public void scanUpdateReceived(Update update) {
- if (update instanceof Update.Found) {
- maybeInsertItem((Update.Found) update);
- return;
- }
- removeItem((Update.Lost) update);
- }
-
/**
* Accept the advertisement if it's not on the reject list.
*/
- private void maybeInsertItem(Update.Found found) {
- Service service = found.getElem().getService();
- final Id id = Id.fromString(service.getInstanceId());
+ @Override
+ public void handleFoundAdvertisement(Service advertisement) {
+ final Id id = Id.fromString(advertisement.getInstanceId());
if (mRejects.contains(id)) {
return;
}
- final T item = mConverter.make(service);
+ final T item = mConverter.make(advertisement);
if (item == null) {
return;
}
- insertItem(id, item);
- }
-
- private void insertItem(final Id id, final T item) {
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -77,8 +66,12 @@
});
}
- private void removeItem(Update.Lost lost) {
- final Id id = Id.fromString(lost.getElem().getService().getInstanceId());
+ /**
+ * Remove the lost advertisement.
+ */
+ @Override
+ public void handleLostAdvertisement(Service advertisement) {
+ final Id id = Id.fromString(advertisement.getInstanceId());
mHandler.post(new Runnable() {
@Override
public void run() {
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/FileUtil.java b/projects/moments/app/src/main/java/io/v/moments/lib/FileUtil.java
similarity index 98%
rename from projects/moments/app/src/main/java/io/v/moments/model/FileUtil.java
rename to projects/moments/app/src/main/java/io/v/moments/lib/FileUtil.java
index 4bb073b..b2b6d2a 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/FileUtil.java
+++ b/projects/moments/app/src/main/java/io/v/moments/lib/FileUtil.java
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package io.v.moments.model;
+package io.v.moments.lib;
import java.io.File;
diff --git a/projects/moments/app/src/main/java/io/v/moments/lib/V23Manager.java b/projects/moments/app/src/main/java/io/v/moments/lib/V23Manager.java
deleted file mode 100644
index d627698..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/lib/V23Manager.java
+++ /dev/null
@@ -1,183 +0,0 @@
-// Copyright 2015 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.moments.lib;
-
-import android.app.Activity;
-import android.content.Context;
-import android.util.Log;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import org.joda.time.Duration;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import io.v.android.libs.security.BlessingsManager;
-import io.v.android.v23.V;
-import io.v.v23.InputChannelCallback;
-import io.v.v23.InputChannels;
-import io.v.v23.context.VContext;
-import io.v.v23.discovery.Service;
-import io.v.v23.discovery.Update;
-import io.v.v23.discovery.VDiscovery;
-import io.v.v23.rpc.Server;
-import io.v.v23.security.BlessingPattern;
-import io.v.v23.security.Blessings;
-import io.v.v23.security.VSecurity;
-import io.v.v23.verror.VException;
-
-/**
- * Various static V23 utilities gathered into an instantiatable class.
- *
- * This allows v23 usage to be injected and mocked. The class is a singleton to
- * avoid confusion about init, shutdown and the 'base context'.
- */
-public class V23Manager {
- private static final String TAG = "V23Manager";
- private static final String BLESSINGS_KEY = "BlessingsKey";
- private static final List<BlessingPattern> NO_PATTERNS = new ArrayList<>();
- private Context mAndroidCtx;
- private VContext mV23Ctx = null;
- private VDiscovery mDiscovery = null;
-
- // Singleton.
- private V23Manager() {
- }
-
- public void scan(
- String query,
- Duration duration,
- FutureCallback<VContext> startupCallback,
- InputChannelCallback<Update> updateCallback,
- FutureCallback<Void> completionCallback) {
- Log.d(TAG, "Starting scan with q=[" + query + "]");
- if (mDiscovery == null) {
- startupCallback.onFailure(
- new IllegalStateException("Discovery not ready."));
- return;
- }
- VContext context = contextWithTimeout(duration);
- Futures.addCallback(
- InputChannels.withCallback(
- mDiscovery.scan(context, query), updateCallback),
- completionCallback);
- startupCallback.onSuccess(context);
- }
-
- public void advertise(
- Service advertisement,
- Duration duration,
- FutureCallback<VContext> startupCallback,
- FutureCallback<Void> completionCallback) {
- if (mDiscovery == null) {
- startupCallback.onFailure(
- new IllegalStateException("Discovery not ready."));
- return;
- }
- VContext context = contextWithTimeout(duration);
- ListenableFuture<ListenableFuture<Void>> hey =
- mDiscovery.advertise(context, advertisement, NO_PATTERNS);
- Futures.addCallback(hey, makeWrapped(context, startupCallback, completionCallback));
- }
-
- /**
- * This exists to allow the scan and advertise interfaces to behave the same
- * way to clients, and to deliver the context via the "success" callback
- * so there's no chance of a race condition between usage of that context
- * and code in the callback.
- */
- private FutureCallback<ListenableFuture<Void>> makeWrapped(
- final VContext context,
- final FutureCallback<VContext> startup,
- final FutureCallback<Void> completion) {
- return new FutureCallback<ListenableFuture<Void>>() {
- @Override
- public void onSuccess(ListenableFuture<Void> result) {
- startup.onSuccess(context);
- Futures.addCallback(result, completion);
- }
-
- @Override
- public void onFailure(final Throwable t) {
- startup.onFailure(t);
- }
- };
- }
-
-
- public synchronized void init(
- Activity activity, FutureCallback<Blessings> blessingCallback) {
- Log.d(TAG, "init");
- if (mAndroidCtx != null) {
- if (mAndroidCtx == activity.getApplicationContext()) {
- Log.d(TAG, "Initialization already started.");
- return;
- } else {
- Log.d(TAG, "Initialization with new context.");
- shutdown();
- }
- }
- mAndroidCtx = activity.getApplicationContext();
- // Must call V.init before attempting to load blessings, so that proper
- // code is loaded.
- mV23Ctx = V.init(mAndroidCtx);
- Log.d(TAG, "Attempting to get blessings.");
- ListenableFuture<Blessings> f = BlessingsManager.getBlessings(
- mV23Ctx, activity, BLESSINGS_KEY, true);
- Futures.addCallback(f, blessingCallback);
- try {
- mDiscovery = V.newDiscovery(mV23Ctx);
- } catch (VException e) {
- Log.d(TAG, "Unable to get discovery object.", e);
- }
- }
-
- public void shutdown() {
- Log.d(TAG, "Shutdown");
- if (mAndroidCtx == null) {
- Log.d(TAG, "Was never initialized.");
- return;
- }
- mV23Ctx.cancel();
- mAndroidCtx = null;
- }
-
- public VContext makeServerContext(
- String mountName, Object server) throws VException {
- return V.withNewServer(
- mV23Ctx.withCancel(),
- mountName,
- server,
- VSecurity.newAllowEveryoneAuthorizer());
- }
-
- public Server getServer(VContext mServerCtx) {
- return V.getServer(mServerCtx);
- }
-
- public VContext contextWithTimeout(Duration timeout) {
- return mV23Ctx.withTimeout(timeout);
- }
-
- public static class Singleton {
- private static volatile V23Manager instance;
-
- public static V23Manager get() {
- V23Manager result = instance;
- if (instance == null) {
- synchronized (Singleton.class) {
- result = instance;
- if (result == null) {
- instance = result = new V23Manager();
- }
- }
- }
- return result;
- }
- }
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/AdConverterMoment.java b/projects/moments/app/src/main/java/io/v/moments/model/AdConverterMoment.java
index b07a8d3..3387786 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/AdConverterMoment.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/AdConverterMoment.java
@@ -15,7 +15,6 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
-import io.v.moments.ifc.AdConverter;
import io.v.moments.ifc.Moment;
import io.v.moments.ifc.Moment.Kind;
import io.v.moments.ifc.Moment.Style;
@@ -23,7 +22,8 @@
import io.v.moments.ifc.MomentFactory;
import io.v.moments.lib.Id;
import io.v.moments.lib.ObservedList;
-import io.v.moments.lib.V23Manager;
+import io.v.moments.v23.ifc.AdConverter;
+import io.v.moments.v23.ifc.V23Manager;
import io.v.v23.discovery.Service;
/**
@@ -76,7 +76,7 @@
if (mRemoteMomentCache.containsKey(id)) {
return mRemoteMomentCache.get(id);
}
- final Moment moment = mMomentFactory.makeFromAttributes(
+ final Moment moment = mMomentFactory.fromAttributes(
id, nextOrdinal(), descriptor.getAttrs());
mRemoteMomentCache.put(id, moment);
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserFactory.java b/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserFactory.java
index 522ce76..9a3a98b 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserFactory.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserFactory.java
@@ -7,30 +7,42 @@
import java.util.HashMap;
import java.util.Map;
-import io.v.moments.ifc.Advertiser;
import io.v.moments.ifc.IdSet;
import io.v.moments.ifc.Moment;
+import io.v.moments.ifc.MomentFactory;
import io.v.moments.lib.Id;
-import io.v.moments.lib.V23Manager;
+import io.v.moments.v23.ifc.Advertiser;
+import io.v.moments.v23.ifc.V23Manager;
/**
- * Makes advertisers. Keeps a record of all of them for the life of the app.
- * Can use this record to reject local advertisements when scanning, or to shut
- * down all advertising.
+ * Makes moment advertisers.
+ *
+ * Keeps a record of all of them for the life of the app. This record used to
+ * reject locally created advertisements when scanning, or to shut down all
+ * advertising.
*/
public class AdvertiserFactory implements IdSet {
private final V23Manager mV23Manager;
private final Map<Id, Advertiser> mLocalAds = new HashMap<>();
+ private final MomentFactory mFactory;
- public AdvertiserFactory(V23Manager v23Manager) {
+ public AdvertiserFactory(V23Manager v23Manager, MomentFactory factory) {
+ if (v23Manager == null) {
+ throw new IllegalArgumentException("Null v23Manager");
+ }
+ if (factory == null) {
+ throw new IllegalArgumentException("Null factory");
+ }
mV23Manager = v23Manager;
+ mFactory = factory;
}
public Advertiser getOrMake(Moment moment) {
if (contains(moment.getId())) {
return mLocalAds.get(moment.getId());
}
- Advertiser result = new AdvertiserImpl(mV23Manager, moment);
+ Advertiser result = mV23Manager.makeAdvertiser(
+ new MomentAdCampaign(moment, mFactory));
mLocalAds.put(moment.getId(), result);
return result;
}
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserImpl.java b/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserImpl.java
deleted file mode 100644
index ee08e5b..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/model/AdvertiserImpl.java
+++ /dev/null
@@ -1,223 +0,0 @@
-// Copyright 2015 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.moments.model;
-
-import android.graphics.Bitmap;
-import android.support.annotation.NonNull;
-import android.util.Log;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.io.ByteArrayOutputStream;
-import java.util.ArrayList;
-import java.util.List;
-
-import io.v.moments.ifc.Advertiser;
-import io.v.moments.ifc.Moment;
-import io.v.moments.ifc.Moment.Kind;
-import io.v.moments.ifc.Moment.Style;
-import io.v.moments.lib.V23Manager;
-import io.v.v23.context.VContext;
-import io.v.v23.discovery.Attachments;
-import io.v.v23.discovery.Attributes;
-import io.v.v23.discovery.Service;
-import io.v.v23.naming.Endpoint;
-import io.v.v23.rpc.ServerCall;
-import io.v.v23.verror.VException;
-
-/**
- * Handles the advertising of a moment.
- *
- * Main role of class is to manage the advertising context and moment service
- * lifetimes. This code too complex to leak into the UX code, and too moment
- * specific to be part of v23Manager. This class should have no knowledge of UX,
- * and UX should have no access to VContext, because said context is under
- * active development, and it's too complex to cover possible interactions with
- * tests. This approaches hides it all behind the simple Advertiser interface.
- */
-public class AdvertiserImpl implements Advertiser {
- static final String NO_MOUNT_NAME = "";
- private static final String TAG = "AdvertiserImpl";
-
- private final V23Manager mV23Manager;
- private final Moment mMoment;
-
- private VContext mAdvCtx;
- private VContext mServerCtx;
-
- public AdvertiserImpl(V23Manager v23Manager, Moment moment) {
- if (v23Manager == null) {
- throw new IllegalArgumentException("Null v23Manager");
- }
- if (moment == null) {
- throw new IllegalArgumentException("Null moment");
- }
- mV23Manager = v23Manager;
- mMoment = moment;
- }
-
- @Override
- public String toString() {
- return mMoment.getCaption();
- }
-
- @Override
- public void start(
- FutureCallback<Void> startupCallback,
- FutureCallback<Void> completionCallback) {
- Log.d(TAG, "Entering start.");
- if (isAdvertising()) {
- startupCallback.onFailure(
- new IllegalStateException("Already advertising."));
- return;
- }
- try {
- mServerCtx = mV23Manager.makeServerContext(
- NO_MOUNT_NAME, new MomentServer());
- } catch (VException e) {
- mServerCtx = null;
- startupCallback.onFailure(
- new IllegalStateException("Unable to start service.", e));
- return;
- }
- mV23Manager.advertise(
- makeAdvertisement(
- mMoment.makeAttributes(), makeServerAddressList()),
- Config.Discovery.DURATION,
- makeWrapped(startupCallback), completionCallback);
- Log.d(TAG, "Exiting start.");
- }
-
- @Override
- public boolean isAdvertising() {
- return mAdvCtx != null && !mAdvCtx.isCanceled();
- }
-
- @NonNull
- private List<String> makeServerAddressList() {
- List<String> addresses = new ArrayList<>();
- Endpoint[] points = mV23Manager.getServer(
- mServerCtx).getStatus().getEndpoints();
- for (Endpoint point : points) {
- addresses.add(point.toString());
- }
- return addresses;
- }
-
- /**
- * A service must be started before advertising begins, which creates a
- * cleanup problem should advertising fail to start. This callback accepts
- * the advertising context on startup success, and cancels the already
- * started service on failure. This wrapper necessary to cleanly kill the
- * service should advertising fail to start. This callback feeds info to
- * the startup callback, which presumably has some effect on UX.
- */
- private FutureCallback<VContext> makeWrapped(
- final FutureCallback<Void> startup) {
- return new FutureCallback<VContext>() {
- @Override
- public void onSuccess(VContext context) {
- mAdvCtx = context;
- startup.onSuccess(null);
- }
-
- @Override
- public void onFailure(final Throwable t) {
- mAdvCtx = null;
- cancelService();
- startup.onFailure(t);
- }
- };
- }
-
- @Override
- public void stop() {
- Log.d(TAG, "Entering stop");
- if (mAdvCtx != null) {
- Log.d(TAG, "Cancelling advertising.");
- if (!mAdvCtx.isCanceled()) {
- mAdvCtx.cancel();
- }
- mAdvCtx = null;
- }
- cancelService();
- Log.d(TAG, "Exiting stop");
- }
-
- private void cancelService() {
- if (mServerCtx != null) {
- Log.d(TAG, "Cancelling service.");
- if (!mServerCtx.isCanceled()) {
- mServerCtx.cancel();
- }
- mServerCtx = null;
- }
- }
-
- /**
- * Makes an instance of 'Service', which is actually a service description,
- * i.e. an advertisement.
- */
- private Service makeAdvertisement(Attributes attrs,
- List<String> addresses) {
- return new Service(
- mMoment.getId().toString(),
- mMoment.toString(),
- Config.Discovery.INTERFACE_NAME,
- attrs,
- addresses,
- new Attachments());
- }
-
- /**
- * Serves moment data over RPC.
- */
- class MomentServer implements MomentIfcServer {
- private static final String TAG = "MomentServer";
- private byte[] mRawBytes = null; // lazy init
- private byte[] mThumbBytes = null; // lazy init
-
- public ListenableFuture<MomentWireData> getBasics(
- VContext ctx, ServerCall call) {
- MomentWireData data = new MomentWireData();
- data.setAuthor(mMoment.getAuthor());
- data.setCaption(mMoment.getCaption());
- data.setCreationTime(mMoment.getCreationTime().getMillis());
- return Futures.immediateFuture(data);
- }
-
- private byte[] makeBytes(Bitmap bitmap) {
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- bitmap.compress(Bitmap.CompressFormat.PNG, 60, stream);
- return stream.toByteArray();
- }
-
- private synchronized byte[] getFullBytes() {
- if (mRawBytes == null) {
- mRawBytes = makeBytes(mMoment.getPhoto(Kind.LOCAL, Style.FULL));
- }
- return mRawBytes;
- }
-
- private synchronized byte[] getThumbBytes() {
- if (mThumbBytes == null) {
- mThumbBytes = makeBytes(mMoment.getPhoto(Kind.LOCAL, Style.THUMB));
- }
- return mThumbBytes;
- }
-
- public ListenableFuture<byte[]> getThumbImage(
- VContext ctx, ServerCall call) {
- return Futures.immediateFuture(getThumbBytes());
- }
-
- public ListenableFuture<byte[]> getFullImage(VContext ctx, ServerCall call) {
- return Futures.immediateFuture(getFullBytes());
- }
- }
-
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/BitMapper.java b/projects/moments/app/src/main/java/io/v/moments/model/BitMapper.java
index 131587f..24d9169 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/BitMapper.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/BitMapper.java
@@ -17,6 +17,7 @@
import io.v.moments.ifc.Moment;
import io.v.moments.ifc.Moment.Kind;
import io.v.moments.ifc.Moment.Style;
+import io.v.moments.lib.FileUtil;
import io.v.moments.lib.ObservedList;
/**
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/Config.java b/projects/moments/app/src/main/java/io/v/moments/model/Config.java
index 8761a52..bbd530e 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/Config.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/Config.java
@@ -10,8 +10,6 @@
import android.os.Environment;
import android.os.Handler;
-import org.joda.time.Duration;
-
import java.io.File;
import io.v.moments.R;
@@ -60,24 +58,4 @@
R.dimen.moment_image_width)
);
}
-
- /** Constants related to discovery. */
- public static class Discovery {
- /**
- * Required type/interface name, probably a URL into a web-based
- * ontology. Necessary for querying.
- */
- public static final String INTERFACE_NAME = "v.io/x/ref.Moments";
- /**
- * To limit scans to see only this service.
- */
- public static final String QUERY = "v.InterfaceName=\"" + INTERFACE_NAME + "\"";
-
- /**
- * After this duration an advertisement or scan for an advertisement
- * will automatically stop. Choice is arbitrary. A nice exercise would
- * be to add this to a settings menu.
- */
- public static final Duration DURATION = Duration.standardMinutes(5);
- }
}
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/MomentAdCampaign.java b/projects/moments/app/src/main/java/io/v/moments/model/MomentAdCampaign.java
new file mode 100644
index 0000000..443a570
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/model/MomentAdCampaign.java
@@ -0,0 +1,153 @@
+// Copyright 2015 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.moments.model;
+
+import android.graphics.Bitmap;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.moments.ifc.Moment;
+import io.v.moments.ifc.MomentFactory;
+import io.v.moments.v23.ifc.AdCampaign;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Attachments;
+import io.v.v23.discovery.Attributes;
+import io.v.v23.rpc.ServerCall;
+import io.v.v23.security.BlessingPattern;
+
+/**
+ * Makes objects that support the advertisement of a Moment.
+ */
+public class MomentAdCampaign implements AdCampaign {
+ /**
+ * Required type/interface name, probably a URL into a web-based ontology.
+ * Necessary for querying.
+ */
+ public static final String INTERFACE_NAME = "v.io/x/ref.Moments";
+ /**
+ * To limit scans to see only this service.
+ */
+ public static final String QUERY = "v.InterfaceName=\"" + INTERFACE_NAME + "\"";
+ /**
+ * Used for public advertisements (no limits on who can see them).
+ */
+ public static final List<BlessingPattern> NO_PATTERNS = new ArrayList<>();
+
+ private final Moment mMoment;
+ private final MomentFactory mFactory;
+
+ public MomentAdCampaign(Moment moment, MomentFactory factory) {
+ if (moment == null) {
+ throw new IllegalArgumentException("Null moment");
+ }
+ if (factory == null) {
+ throw new IllegalArgumentException("Null factory");
+ }
+ mMoment = moment;
+ mFactory = factory;
+ }
+
+ @Override
+ public String getInstanceId() {
+ return mMoment.getId().toString();
+ }
+
+ @Override
+ public String getInstanceName() {
+ return mMoment.toString();
+ }
+
+ @Override
+ public String getInterfaceName() {
+ return INTERFACE_NAME;
+ }
+
+ @Override
+ public Attributes getAttributes() {
+ return mFactory.toAttributes(mMoment);
+ }
+
+ /**
+ * No attachments (empty list).
+ */
+ @Override
+ public Attachments getAttachments() {
+ return new Attachments();
+ }
+
+ /**
+ * Empty string means make no attempt to mount a server in a mount table.
+ */
+ @Override
+ public String getMountName() {
+ return "";
+ }
+
+ @Override
+ public Object makeService() {
+ return new MomentServer();
+ }
+
+ /**
+ * A set of blessing patterns for whom this advertisement is meant; any
+ * entity not matching a pattern here won't see the advertisement.
+ */
+ @Override
+ public List<BlessingPattern> getVisibility() {
+ return NO_PATTERNS;
+ }
+
+ /**
+ * Serves moment data over RPC.
+ */
+ private class MomentServer implements MomentIfcServer {
+ private static final String TAG = "MomentServer";
+ private byte[] mRawBytes = null; // lazy init
+ private byte[] mThumbBytes = null; // lazy init
+
+ public ListenableFuture<MomentWireData> getBasics(
+ VContext ctx, ServerCall call) {
+ MomentWireData data = new MomentWireData();
+ data.setAuthor(mMoment.getAuthor());
+ data.setCaption(mMoment.getCaption());
+ data.setCreationTime(mMoment.getCreationTime().getMillis());
+ return Futures.immediateFuture(data);
+ }
+
+ private byte[] makeBytes(Bitmap bitmap) {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.PNG, 60, stream);
+ return stream.toByteArray();
+ }
+
+ private synchronized byte[] getFullBytes() {
+ if (mRawBytes == null) {
+ mRawBytes = makeBytes(mMoment.getPhoto(Moment.Kind.LOCAL, Moment.Style.FULL));
+ }
+ return mRawBytes;
+ }
+
+ private synchronized byte[] getThumbBytes() {
+ if (mThumbBytes == null) {
+ mThumbBytes = makeBytes(mMoment.getPhoto(Moment.Kind.LOCAL, Moment.Style.THUMB));
+ }
+ return mThumbBytes;
+ }
+
+ public ListenableFuture<byte[]> getThumbImage(
+ VContext ctx, ServerCall call) {
+ return Futures.immediateFuture(getThumbBytes());
+ }
+
+ public ListenableFuture<byte[]> getFullImage(VContext ctx, ServerCall call) {
+ return Futures.immediateFuture(getFullBytes());
+ }
+ }
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/MomentFactoryImpl.java b/projects/moments/app/src/main/java/io/v/moments/model/MomentFactoryImpl.java
index c87d576..f341775 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/MomentFactoryImpl.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/MomentFactoryImpl.java
@@ -34,7 +34,7 @@
}
@Override
- public Moment makeFromAttributes(Id id, int ordinal, Attributes attr) {
+ public Moment fromAttributes(Id id, int ordinal, Attributes attr) {
return new MomentImpl(
mBitMapper, id, ordinal,
attr.get(F.AUTHOR.toString()),
@@ -44,6 +44,16 @@
}
@Override
+ public Attributes toAttributes(Moment moment) {
+ Attributes attr = new Attributes();
+ attr.put(MomentFactory.F.AUTHOR.toString(), moment.getAuthor());
+ attr.put(MomentFactory.F.CAPTION.toString(), moment.getCaption());
+ attr.put(MomentFactory.F.DATE.toString(),
+ Moment.FMT.print(moment.getCreationTime()));
+ return attr;
+ }
+
+ @Override
public void toBundle(Bundle b, String prefix, Moment m) {
KeyMaker km = new KeyMaker(prefix);
b.putString(km.get(F.ID), m.getId().toString());
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/MomentImpl.java b/projects/moments/app/src/main/java/io/v/moments/model/MomentImpl.java
index 96e9569..ed521cd 100644
--- a/projects/moments/app/src/main/java/io/v/moments/model/MomentImpl.java
+++ b/projects/moments/app/src/main/java/io/v/moments/model/MomentImpl.java
@@ -8,16 +8,13 @@
import org.joda.time.DateTime;
-import io.v.moments.ifc.Advertiser;
-import io.v.moments.ifc.MomentFactory;
-import io.v.moments.lib.Id;
import io.v.moments.ifc.Moment;
-import io.v.v23.discovery.Attributes;
+import io.v.moments.lib.Id;
/**
* A photo and ancillary information.
*/
-public class MomentImpl implements Moment {
+class MomentImpl implements Moment {
private static final String NOT_LETTERS_DIGITS = "[^a-zA-Z0-9]";
protected final BitMapper mBitMapper;
private final DateTime mCreationTime;
@@ -48,15 +45,6 @@
mDesiredAdState = value;
}
- @Override
- public Attributes makeAttributes() {
- Attributes attr = new Attributes();
- attr.put(MomentFactory.F.AUTHOR.toString(), getAuthor());
- attr.put(MomentFactory.F.CAPTION.toString(), getCaption());
- attr.put(MomentFactory.F.DATE.toString(), FMT.print(getCreationTime()));
- return attr;
- }
-
public boolean hasPhoto(Kind kind, Style style) {
return mBitMapper.exists(getOrdinal(), kind, style);
}
diff --git a/projects/moments/app/src/main/java/io/v/moments/model/ScannerImpl.java b/projects/moments/app/src/main/java/io/v/moments/model/ScannerImpl.java
deleted file mode 100644
index 82f14f3..0000000
--- a/projects/moments/app/src/main/java/io/v/moments/model/ScannerImpl.java
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright 2015 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.moments.model;
-
-import android.util.Log;
-
-import com.google.common.util.concurrent.FutureCallback;
-
-import io.v.moments.ifc.Scanner;
-import io.v.moments.lib.V23Manager;
-import io.v.v23.InputChannelCallback;
-import io.v.v23.context.VContext;
-import io.v.v23.discovery.Update;
-
-/**
- * Handles scanning for moments.
- *
- * This class similar to AdvertiserImpl - see that class for more commentary -
- * in that it functions as decoupling of the UX from the moving target that is
- * the V23/VContext API
- *
- * At the moment, the complexity is far less than it is for advertising
- * (scanning doesn't require a running service), so this class doesn't add much
- * value over using an instance of V23Manager and VContext directly in whatever
- * class uses a Scanner. To make this class more useful in decoupling moments
- * specific code from v23 specifics, this class could tease apart a scan Update,
- * and feed advertisement data from the Update.Found and Update.Lost object
- * directly into, say, the appropriate instances of java.util.function.Consumer<T>
- * that were passed to #start en lieu of the InputChannelCallback<Update>
- * updateCallback. That way the caller would have no exposure to v23 classes.
- */
-public class ScannerImpl implements Scanner {
- private static final String TAG = "ScannerImpl";
- private final V23Manager mV23Manager;
- private final String mQuery;
- private VContext mScanCtx;
-
- public ScannerImpl(V23Manager v23Manager, String query) {
- if (v23Manager == null) {
- throw new IllegalArgumentException("Null v23Manager.");
- }
- if (query == null || query.isEmpty()) {
- throw new IllegalArgumentException("Empty query.");
- }
- mV23Manager = v23Manager;
- mQuery = query;
- }
-
- @Override
- public String toString() {
- return "scan(" + mQuery + "," + isScanning() + ")";
- }
-
- /**
- * Accepts the scanning context on success (or assures that it's null on
- * failure), then activates the callback supplied by the client (likely to
- * change the UX).
- */
- private FutureCallback<VContext> makeWrapped(
- final FutureCallback<Void> startup) {
- return new FutureCallback<VContext>() {
- @Override
- public void onSuccess(VContext context) {
- mScanCtx = context;
- startup.onSuccess(null);
- }
-
- @Override
- public void onFailure(final Throwable t) {
- mScanCtx = null;
- startup.onFailure(t);
- }
- };
- }
-
- @Override
- public void start(
- FutureCallback<Void> startupCallback,
- InputChannelCallback<Update> updateCallback,
- FutureCallback<Void> completionCallback) {
- Log.d(TAG, "Entering start.");
- if (isScanning()) {
- startupCallback.onFailure(
- new IllegalStateException("Already scanning."));
- return;
- }
- mV23Manager.scan(
- mQuery, Config.Discovery.DURATION,
- makeWrapped(startupCallback),
- updateCallback,
- completionCallback);
- Log.d(TAG, "Exiting start.");
- }
-
- @Override
- public boolean isScanning() {
- return mScanCtx != null && !mScanCtx.isCanceled();
- }
-
- @Override
- public void stop() {
- Log.d(TAG, "Entering stop");
- if (mScanCtx != null) {
- Log.d(TAG, "Cancelling scan.");
- if (!mScanCtx.isCanceled()) {
- mScanCtx.cancel();
- }
- mScanCtx = null;
- }
- Log.d(TAG, "Exiting stop");
- }
-}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/DividerItemDecoration.java b/projects/moments/app/src/main/java/io/v/moments/ux/DividerItemDecoration.java
index bc2a56a..507c6fc 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/DividerItemDecoration.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/DividerItemDecoration.java
@@ -31,7 +31,7 @@
import android.support.v7.widget.RecyclerView;
import android.view.View;
-public class DividerItemDecoration extends RecyclerView.ItemDecoration {
+class DividerItemDecoration extends RecyclerView.ItemDecoration {
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java b/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java
index f1a91c0..1c85820 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/MainActivity.java
@@ -34,24 +34,25 @@
import java.util.concurrent.Executors;
import io.v.moments.R;
-import io.v.moments.ifc.Advertiser;
import io.v.moments.ifc.Moment;
import io.v.moments.ifc.MomentFactory;
-import io.v.moments.ifc.Scanner;
import io.v.moments.lib.DiscoveredList;
+import io.v.moments.lib.FileUtil;
import io.v.moments.lib.Id;
import io.v.moments.lib.ObservedList;
import io.v.moments.lib.PermissionManager;
-import io.v.moments.lib.V23Manager;
import io.v.moments.model.AdConverterMoment;
import io.v.moments.model.AdvertiserFactory;
import io.v.moments.model.BitMapper;
import io.v.moments.model.Config;
-import io.v.moments.model.FileUtil;
+import io.v.moments.model.MomentAdCampaign;
import io.v.moments.model.MomentFactoryImpl;
-import io.v.moments.model.ScannerImpl;
import io.v.moments.model.StateStore;
import io.v.moments.model.Toaster;
+import io.v.moments.v23.ifc.Advertiser;
+import io.v.moments.v23.ifc.Scanner;
+import io.v.moments.v23.ifc.V23Manager;
+import io.v.moments.v23.impl.V23ManagerImpl;
import io.v.v23.security.Blessings;
/**
@@ -81,9 +82,9 @@
*
* TODO: when reloading from prefs, don't change advertise or scan state. only
* do that when reloading from bundle. TODO: ScannerImpl should handle the
- * Update parsing currently done by DiscoveredList. TODO: unit tests. TODO:
- * Add version number to prefs, ignore and overwrite state if old version (to
- * avoid need to manually wipe data to avoid crashes).
+ * Update parsing currently done by DiscoveredList. TODO: unit tests. TODO: Add
+ * version number to prefs, ignore and overwrite state if old version (to avoid
+ * need to manually wipe data to avoid crashes).
*/
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@@ -106,7 +107,7 @@
// For changes to UX.
private final Handler mHandler = new Handler(Looper.getMainLooper());
// For discovery, serving and behaving as a client.
- private final V23Manager mV23Manager = V23Manager.Singleton.get();
+ private final V23Manager mV23Manager = V23ManagerImpl.Singleton.get();
// See wireUxToDataModel for discussion of the following.
private StateStore mStateStore;
@@ -188,12 +189,12 @@
// Compresses byte data, converts byte[] to bitmap, manages file storage.
mBitMapper = Config.makeBitmapper(this);
- // Makes advertisers. Needs v23Manager to do advertising.
- mAdvertiserFactory = new AdvertiserFactory(mV23Manager);
-
// Makes moments. Each moment needs a bitmapper to read its BitMaps.
mMomentFactory = new MomentFactoryImpl(mBitMapper);
+ // Makes advertisers. Needs v23Manager to do advertising.
+ mAdvertiserFactory = new AdvertiserFactory(mV23Manager, mMomentFactory);
+
// Local moments, with photos taken by the local device.
mLocalMoments = new ObservedList<>();
@@ -212,7 +213,7 @@
Toaster toaster = new Toaster(this);
- mScanner = new ScannerImpl(mV23Manager, Config.Discovery.QUERY);
+ mScanner = mV23Manager.makeScanner(MomentAdCampaign.QUERY);
mScanSwitchHolder = new ScanSwitchHolder(
toaster, mScanner, mRemoteMoments);
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java b/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java
index 3950771..b85050f 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/MomentAdapter.java
@@ -5,7 +5,6 @@
package io.v.moments.ux;
import android.content.Context;
-import android.os.Handler;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
@@ -21,7 +20,7 @@
/**
* Stacks two moment lists in a recycler view.
*/
-public class MomentAdapter extends RecyclerView.Adapter<MomentHolder>
+class MomentAdapter extends RecyclerView.Adapter<MomentHolder>
implements ListObserver {
private final ObservedList<Moment> mRemoteMoments;
private final ObservedList<Moment> mLocalMoments;
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java b/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java
index e172f1e..df8b18a 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/MomentHolder.java
@@ -13,26 +13,32 @@
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.TextView;
-import android.widget.Toast;
import com.google.common.util.concurrent.FutureCallback;
+import org.joda.time.Duration;
+
import java.util.concurrent.CancellationException;
import io.v.moments.R;
-import io.v.moments.ifc.Advertiser;
import io.v.moments.ifc.Moment;
import io.v.moments.ifc.Moment.Kind;
import io.v.moments.ifc.Moment.Style;
import io.v.moments.model.Toaster;
+import io.v.moments.v23.ifc.Advertiser;
import static io.v.moments.ifc.Moment.AdState;
/**
* Holds the views comprising a Moment for a RecyclerView.
*/
-public class MomentHolder extends RecyclerView.ViewHolder {
+class MomentHolder extends RecyclerView.ViewHolder {
private static final String TAG = "MomentHolder";
+ /**
+ * After this duration a advertisement automatically stop. Choice is
+ * arbitrary.
+ */
+ private static final Duration DURATION = Duration.standardMinutes(5);
private final TextView mAuthorTextView;
private final TextView mCaptionTextView;
private final SwitchCompat mAdvertiseButton;
@@ -106,7 +112,8 @@
if (!advertiser.isAdvertising()) {
advertiser.start(
makeAdvertiseStartCallback(moment),
- makeAdvertiseStopCallback(moment));
+ makeAdvertiseStopCallback(moment),
+ DURATION);
} else {
Log.d(TAG, "Advertiser already on.");
}
diff --git a/projects/moments/app/src/main/java/io/v/moments/ux/ScanSwitchHolder.java b/projects/moments/app/src/main/java/io/v/moments/ux/ScanSwitchHolder.java
index c5187f4..bb660d5 100644
--- a/projects/moments/app/src/main/java/io/v/moments/ux/ScanSwitchHolder.java
+++ b/projects/moments/app/src/main/java/io/v/moments/ux/ScanSwitchHolder.java
@@ -4,24 +4,20 @@
package io.v.moments.ux;
-import android.app.Activity;
import android.support.v7.widget.SwitchCompat;
import android.util.Log;
import android.widget.CompoundButton;
-import android.widget.Toast;
import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
+
+import org.joda.time.Duration;
import java.util.concurrent.CancellationException;
import io.v.moments.ifc.Moment;
-import io.v.moments.ifc.Scanner;
import io.v.moments.lib.DiscoveredList;
import io.v.moments.model.Toaster;
-import io.v.v23.InputChannelCallback;
-import io.v.v23.discovery.Update;
+import io.v.moments.v23.ifc.Scanner;
/**
* Manages scanning UX.
@@ -29,8 +25,12 @@
* The callbacks provided will be run on the UX thread, so its safe to perform
* UX operations in the callbacks.
*/
-public class ScanSwitchHolder implements CompoundButton.OnCheckedChangeListener {
+class ScanSwitchHolder implements CompoundButton.OnCheckedChangeListener {
private static final String TAG = "ScanSwitchHolder";
+ /**
+ * After this duration a scan automatically stop. Choice is arbitrary.
+ */
+ private static final Duration DURATION = Duration.standardMinutes(5);
private final Scanner mScanner;
private final Toaster mToaster;
private final DiscoveredList<Moment> mRemoteMoments;
@@ -64,7 +64,12 @@
Log.d(TAG, "Asked to start scanning, but already scanning.");
return;
}
- mScanner.start(makeStartupCallback(), makeUpdateCallback(), makeCompletionCallback());
+ mScanner.start(
+ makeStartupCallback(),
+ mRemoteMoments,
+ mRemoteMoments,
+ makeCompletionCallback(),
+ DURATION);
} else {
if (!mScanner.isScanning()) {
Log.d(TAG, "Asked to stop scanning, but already not scanning.");
@@ -99,17 +104,9 @@
};
}
- private InputChannelCallback<Update> makeUpdateCallback() {
- return new InputChannelCallback<Update>() {
- @Override
- public ListenableFuture<Void> onNext(Update result) {
- mRemoteMoments.scanUpdateReceived(result);
- return Futures.immediateFuture(null);
- }
- };
- }
-
- /** Verify that scanning is off and that the UX reflects that fact. */
+ /**
+ * Verify that scanning is off and that the UX reflects that fact.
+ */
private void cleanUpPostStop() {
Log.d(TAG, "cleanUpPostStop");
if (mScanner.isScanning()) {
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdCampaign.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdCampaign.java
new file mode 100644
index 0000000..bc8155d
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdCampaign.java
@@ -0,0 +1,69 @@
+// Copyright 2015 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.moments.v23.ifc;
+
+import java.util.List;
+
+import io.v.v23.discovery.Attachments;
+import io.v.v23.discovery.Attributes;
+import io.v.v23.security.BlessingPattern;
+
+/**
+ * Provides the data needed to run an advertisement.
+ */
+public interface AdCampaign {
+ /**
+ * Unique Id associated with the advertisement, used to discriminate when an
+ * ad is found or lost.
+ */
+ String getInstanceId();
+
+ /**
+ * Optional human readable name, can be used in query discrimination.
+ */
+ String getInstanceName();
+
+ /**
+ * Optional service interface name, can be used in query discrimination. The
+ * name, if provided, should be the name of the service interface associated
+ * with the result of #makeServer().
+ */
+ String getInterfaceName();
+
+ /**
+ * Map of 'smallish' name/value pairs to send with the advertisement.
+ */
+ Attributes getAttributes();
+
+ /**
+ * Larger blobs of data to made available asynchronously to scanners.
+ */
+ Attachments getAttachments();
+
+ /**
+ * Makes an instance of a service (a set of handlers) that will be run
+ * during the life of the advertisement.
+ *
+ * I.e., every time an advertisement is started using this campaign, this
+ * factory method will be called to create a new service object with a clean
+ * state. The service object will be used to start an actual server that
+ * will serve requests only as long as the advertisement.
+ *
+ * If null returned, no server is launched.
+ */
+ Object makeService();
+
+ /**
+ * Name at which the service associated with #makeService() should be
+ * mounted. Can be empty.
+ */
+ String getMountName();
+
+ /**
+ * A set of blessing patterns for whom this advertisement is meant; any
+ * entity not matching a pattern here won't see the advertisement.
+ */
+ List<BlessingPattern> getVisibility();
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdConverter.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdConverter.java
new file mode 100644
index 0000000..24db2ea
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdConverter.java
@@ -0,0 +1,18 @@
+// Copyright 2015 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.moments.v23.ifc;
+
+import io.v.v23.discovery.Service;
+
+/**
+ * Implementations of this interface construct an instance of T from the
+ * attributes in the advertisement, and/or by making RPCs to services associated
+ * with or otherwise mentioned by the advertisement.
+ *
+ * TODO(jregan): This method should return ListenableFuture<T>.
+ */
+public interface AdConverter<T> {
+ T make(Service advertisement);
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementFoundListener.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementFoundListener.java
new file mode 100644
index 0000000..887284f
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementFoundListener.java
@@ -0,0 +1,14 @@
+// Copyright 2015 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.moments.v23.ifc;
+
+import io.v.v23.discovery.Service;
+
+/**
+ * Something that handles recently found advertisements.
+ */
+public interface AdvertisementFoundListener {
+ void handleFoundAdvertisement(Service advertisement);
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementLostListener.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementLostListener.java
new file mode 100644
index 0000000..da8be17
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/AdvertisementLostListener.java
@@ -0,0 +1,14 @@
+// Copyright 2015 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.moments.v23.ifc;
+
+import io.v.v23.discovery.Service;
+
+/**
+ * Something that handles recently lost advertisements.
+ */
+public interface AdvertisementLostListener {
+ void handleLostAdvertisement(Service advertisement);
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Advertiser.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Advertiser.java
new file mode 100644
index 0000000..10c5c2d
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Advertiser.java
@@ -0,0 +1,44 @@
+// Copyright 2015 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.moments.v23.ifc;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import org.joda.time.Duration;
+
+/**
+ * Advertiser - can start, stop, and restart advertisements.
+ *
+ * The start and stop cycle is appropriate for connection to a toggle button.
+ */
+public interface Advertiser {
+ /**
+ * Asynchronously start advertising.
+ *
+ * Callbacks can be expected to run on the UX thread.
+ *
+ * @param onStart Callback with success or failure handlers for advertiser
+ * startup. A success can switch a toggle button to "on".
+ * @param onStop Callback with success or failure handlers for advertiser
+ * shutdown. An advertiser might shutdown for reasons other
+ * than a call to stop, e.g. a timeout. The callback can,
+ * say, switch a toggle button back to "off".
+ * @param timeout Amount of time until the advertisement self-cancels.
+ */
+ void start(FutureCallback<Void> onStart,
+ FutureCallback<Void> onStop,
+ Duration timeout);
+
+ /**
+ * True if stop could usefully be called.
+ */
+ boolean isAdvertising();
+
+ /**
+ * Synchronously stop advertising. Should result in execution of onStop
+ * callback.
+ */
+ void stop();
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Scanner.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Scanner.java
new file mode 100644
index 0000000..057f42d
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/Scanner.java
@@ -0,0 +1,51 @@
+// Copyright 2015 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.moments.v23.ifc;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import org.joda.time.Duration;
+
+/**
+ * Scanner - can start, stop, and restart a scan for advertisements.
+ *
+ * The start and stop cycle is appropriate for connection to a toggle button.
+ */
+public interface Scanner {
+ /**
+ * Asynchronously start scanning.
+ *
+ * Callbacks can be expected to run on the UX thread.
+ *
+ * @param onStart Callback with success or failure handlers for scan
+ * startup. A success can switch a toggle button to
+ * "on".
+ * @param foundListener Handler executed on each found newly found
+ * advertisement.
+ * @param lostListener Handler executed on each previously seen but now
+ * lost advertisement.
+ * @param onStop Callback with success or failure handlers for scan
+ * shutdown. A scan might shutdown for reasons other
+ * than a call to stop, e.g. a timeout. The callback
+ * can, say, switch a toggle button back to "off".
+ * @param timeout Amount of time until the scan self-cancels.
+ */
+ void start(FutureCallback<Void> onStart,
+ AdvertisementFoundListener foundListener,
+ AdvertisementLostListener lostListener,
+ FutureCallback<Void> onStop,
+ Duration timeout);
+
+ /**
+ * True if stop could usefully be called.
+ */
+ boolean isScanning();
+
+ /**
+ * Synchronously stop scanning. Should result in execution of onStop
+ * callback.
+ */
+ void stop();
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/ifc/V23Manager.java b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/V23Manager.java
new file mode 100644
index 0000000..00949e1
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/ifc/V23Manager.java
@@ -0,0 +1,66 @@
+// Copyright 2015 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.moments.v23.ifc;
+
+import android.app.Activity;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import org.joda.time.Duration;
+
+import io.v.v23.context.VContext;
+import io.v.v23.security.Blessings;
+
+/**
+ * Secure distributed computing via underlying v23 APIs.
+ *
+ * This and other interfaces in the encompassing package comprise an API that
+ * might feel more comfortable to java Android developers. It wraps the
+ * underlying static v23 methods in a framework of injectable, mockable
+ * instances.
+ */
+public interface V23Manager {
+ /**
+ * Start V23 runtime bound to the given activity, and give it a callback via
+ * which it will get its blessings. This should be called on onCreate().
+ *
+ * When the blessings come in, the app can safely use v23 operations that
+ * require a notion of identity, but until that time should make no attempt
+ * to do so.
+ */
+ void init(Activity activity, FutureCallback<Blessings> blessingCallback);
+
+ /**
+ * Shutdown the v23 runtime. This should be called in onDestroy() to cancel
+ * any lingering contexts associated with v23 operations (advertising,
+ * scanning, serving etc.), so that a subsequent call to init - say, during
+ * destroy/create lifecycle event series - will start with clean state in
+ * the v23 runtime.
+ */
+ void shutdown();
+
+ /**
+ * Used by v23 clients to make v23 RPCs.
+ *
+ * @param duration Amount of time until the operation self-cancels.
+ */
+ VContext contextWithTimeout(Duration duration);
+
+ /**
+ * Returns an advertiser bound to the given adCampaign.
+ *
+ * @param adCampaign Immutable description of the ad to run.
+ * @return Advertiser that can start, stop and restart the advertisement.
+ */
+ Advertiser makeAdvertiser(AdCampaign adCampaign);
+
+ /**
+ * Returns a scanner that will look for advertisements matching the query.
+ *
+ * @param query Query limiting the ads that are processed.
+ * @return Scanner that can start, stop and restart the scan.
+ */
+ Scanner makeScanner(String query);
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/impl/V23ManagerImpl.java b/projects/moments/app/src/main/java/io/v/moments/v23/impl/V23ManagerImpl.java
new file mode 100644
index 0000000..b64e262
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/impl/V23ManagerImpl.java
@@ -0,0 +1,417 @@
+// Copyright 2015 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.moments.v23.impl;
+
+import android.app.Activity;
+import android.content.Context;
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.joda.time.Duration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.android.libs.security.BlessingsManager;
+import io.v.android.v23.V;
+import io.v.moments.v23.ifc.AdCampaign;
+import io.v.moments.v23.ifc.AdvertisementFoundListener;
+import io.v.moments.v23.ifc.AdvertisementLostListener;
+import io.v.moments.v23.ifc.Advertiser;
+import io.v.moments.v23.ifc.Scanner;
+import io.v.moments.v23.ifc.V23Manager;
+import io.v.v23.InputChannelCallback;
+import io.v.v23.InputChannels;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Service;
+import io.v.v23.discovery.Update;
+import io.v.v23.discovery.VDiscovery;
+import io.v.v23.naming.Endpoint;
+import io.v.v23.security.Blessings;
+import io.v.v23.security.VSecurity;
+import io.v.v23.verror.VException;
+
+/**
+ * Various static V23 utilities gathered into an instantiatable class.
+ *
+ * This allows V23 usage to be injected and mocked. The class is a singleton to
+ * avoid confusion about init, shutdown and the 'base context'.
+ *
+ * Nothing in here has anything in particular to do with Moments; it's an object
+ * hiding an evolving API based on static functions.
+ */
+public class V23ManagerImpl implements V23Manager {
+ private static final String TAG = "V23ManagerImpl";
+ private static final String BLESSINGS_KEY = "BlessingsKey";
+ private Context mAndroidCtx;
+ private VContext mV23Ctx = null;
+ private VDiscovery mDiscovery = null;
+
+ // Constructor should only be called from tests.
+ /* package private */ V23ManagerImpl() {
+ }
+
+ @Override
+ public synchronized void init(
+ Activity activity, FutureCallback<Blessings> blessingCallback) {
+ Log.d(TAG, "init");
+ if (mAndroidCtx != null) {
+ if (mAndroidCtx == activity.getApplicationContext()) {
+ Log.d(TAG, "Initialization already started.");
+ return;
+ } else {
+ Log.d(TAG, "Initialization with new context.");
+ shutdown();
+ }
+ }
+ mAndroidCtx = activity.getApplicationContext();
+ // Must call V.init before attempting to load blessings, so that proper
+ // code is loaded.
+ mV23Ctx = V.init(mAndroidCtx);
+ Log.d(TAG, "Attempting to get blessings.");
+ ListenableFuture<Blessings> f = BlessingsManager.getBlessings(
+ mV23Ctx, activity, BLESSINGS_KEY, true);
+ Futures.addCallback(f, blessingCallback);
+ try {
+ mDiscovery = V.newDiscovery(mV23Ctx);
+ } catch (VException e) {
+ Log.d(TAG, "Unable to get discovery object.", e);
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ Log.d(TAG, "Shutdown");
+ if (mAndroidCtx == null) {
+ Log.d(TAG, "Was never initialized.");
+ return;
+ }
+ mV23Ctx.cancel();
+ mAndroidCtx = null;
+ }
+
+ private VContext makeServerContext(
+ String mountName, Object server) throws VException {
+ return V.withNewServer(
+ mV23Ctx.withCancel(),
+ mountName,
+ server,
+ VSecurity.newAllowEveryoneAuthorizer());
+ }
+
+ private List<String> makeServerAddressList(VContext serverCtx) {
+ List<String> addresses = new ArrayList<>();
+ Endpoint[] points = V.getServer(
+ serverCtx).getStatus().getEndpoints();
+ for (Endpoint point : points) {
+ addresses.add(point.toString());
+ }
+ return addresses;
+ }
+
+ @Override
+ public VContext contextWithTimeout(Duration timeout) {
+ return mV23Ctx.withTimeout(timeout);
+ }
+
+ @Override
+ public Advertiser makeAdvertiser(AdCampaign adCampaign) {
+ return new AdvertiserImpl(adCampaign);
+ }
+
+ @Override
+ public Scanner makeScanner(String query) {
+ return new ScannerImpl(query);
+ }
+
+ public static class Singleton {
+ private static volatile V23ManagerImpl instance;
+
+ public static V23ManagerImpl get() {
+ V23ManagerImpl result = instance;
+ if (instance == null) {
+ synchronized (Singleton.class) {
+ result = instance;
+ if (result == null) {
+ instance = result = new V23ManagerImpl();
+ }
+ }
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Manages one instance of advertising - one ad, one backing service.
+ *
+ * This class hides/manages the two v23 contexts necessary to run a service
+ * and a related advertisement.
+ */
+ class AdvertiserImpl implements Advertiser {
+ private static final String TAG = "AdvertiserImpl";
+
+ private final AdCampaign mAdCampaign;
+
+ private VContext mAdvCtx;
+ private VContext mServerCtx;
+ private Duration mDuration;
+
+ public AdvertiserImpl(AdCampaign adCampaign) {
+ if (adCampaign == null) {
+ throw new IllegalArgumentException("Null adCampaign");
+ }
+ mAdCampaign = adCampaign;
+ }
+
+ @Override
+ public void start(FutureCallback<Void> onStartCallback,
+ FutureCallback<Void> onStopCallback,
+ Duration timeout) {
+ Log.d(TAG, "Entering start.");
+ if (isAdvertising()) {
+ onStartCallback.onFailure(
+ new IllegalStateException("Already advertising."));
+ return;
+ }
+
+ if (timeout == null) {
+ throw new IllegalArgumentException("Null timeout");
+ }
+ mDuration = timeout;
+
+ if (mDiscovery == null) {
+ onStartCallback.onFailure(
+ new IllegalStateException("Discovery not ready."));
+ return;
+ }
+
+ try {
+ mServerCtx = makeServerContext(
+ mAdCampaign.getMountName(),
+ mAdCampaign.makeService());
+ } catch (VException e) {
+ onStartCallback.onFailure(
+ new IllegalStateException("Unable to start service.", e));
+ return;
+ }
+
+
+ VContext context = contextWithTimeout(mDuration);
+
+ Service advertisement = new Service(
+ mAdCampaign.getInstanceId(),
+ mAdCampaign.getInstanceName(),
+ mAdCampaign.getInterfaceName(),
+ mAdCampaign.getAttributes(),
+ makeServerAddressList(mServerCtx),
+ mAdCampaign.getAttachments());
+
+ ListenableFuture<ListenableFuture<Void>> nestedFuture =
+ mDiscovery.advertise(
+ context,
+ advertisement,
+ mAdCampaign.getVisibility());
+
+ Futures.addCallback(
+ nestedFuture,
+ deliverContextCallback(
+ context,
+ confirmCleanStartCallback(onStartCallback),
+ onStopCallback));
+
+ Log.d(TAG, "Exiting start.");
+ }
+
+ /**
+ * A service must be started before advertising begins, which creates a
+ * cleanup problem should advertising fail to start. This callback
+ * accepts the advertising context on startup success, and cancels the
+ * already started service on failure.
+ *
+ * This wrapper necessary to cleanly kill the service should advertising
+ * fail to start. This callback feeds info to the startup callback,
+ * which presumably has some effect on UX.
+ */
+ private FutureCallback<VContext> confirmCleanStartCallback(
+ final FutureCallback<Void> onStart) {
+ return new FutureCallback<VContext>() {
+ @Override
+ public void onSuccess(VContext context) {
+ mAdvCtx = context;
+ onStart.onSuccess(null);
+ }
+
+ @Override
+ public void onFailure(final Throwable t) {
+ mAdvCtx = null;
+ cancelService();
+ onStart.onFailure(t);
+ }
+ };
+ }
+
+ /**
+ * This exists to allow the scan and advertise interfaces to behave the
+ * same way to clients (accepting simple Callback<Void> for both onStart
+ * and onStop), and to deliver the context via the "success" callback so
+ * there's no chance of a race condition between usage of that context
+ * and code in the onStart callback.
+ */
+ private FutureCallback<ListenableFuture<Void>> deliverContextCallback(
+ final VContext context,
+ final FutureCallback<VContext> onStart,
+ final FutureCallback<Void> onStop) {
+ return new FutureCallback<ListenableFuture<Void>>() {
+ @Override
+ public void onSuccess(ListenableFuture<Void> result) {
+ onStart.onSuccess(context);
+ Futures.addCallback(result, onStop);
+ }
+
+ @Override
+ public void onFailure(final Throwable t) {
+ onStart.onFailure(t);
+ }
+ };
+ }
+
+ @Override
+ public boolean isAdvertising() {
+ return mAdvCtx != null && !mAdvCtx.isCanceled();
+ }
+
+ @Override
+ public void stop() {
+ if (!isAdvertising()) {
+ throw new IllegalStateException("Not advertising.");
+ }
+ Log.d(TAG, "Entering stop");
+ if (mAdvCtx != null) {
+ Log.d(TAG, "Cancelling advertising.");
+ if (!mAdvCtx.isCanceled()) {
+ mAdvCtx.cancel();
+ }
+ mAdvCtx = null;
+ }
+ cancelService();
+ Log.d(TAG, "Exiting stop");
+ }
+
+ private void cancelService() {
+ if (mServerCtx != null) {
+ Log.d(TAG, "Cancelling service.");
+ if (!mServerCtx.isCanceled()) {
+ mServerCtx.cancel();
+ }
+ mServerCtx = null;
+ }
+ }
+ }
+
+ /**
+ * Handles scanning for moments.
+ *
+ * To make this class more useful in decoupling moments specific code from
+ * v23 specifics, this class could tease apart a scan Update, and feed
+ * advertisement data from the Update.Found and Update.Lost object directly
+ * into, say, the appropriate instances of java.util.function.Consumer<T>
+ * that were passed to #start en lieu of the InputChannelCallback<Update>
+ * updateCallback. That way the caller would have no exposure to v23
+ * classes (Update and Lost).
+ */
+ class ScannerImpl implements Scanner {
+ private static final String TAG = "ScannerImpl";
+ private final String mQuery;
+ private Duration mDuration;
+ private VContext mScanCtx;
+
+ public ScannerImpl(String query) {
+ if (query == null || query.isEmpty()) {
+ throw new IllegalArgumentException("Empty query.");
+ }
+ mQuery = query;
+ }
+
+ @Override
+ public String toString() {
+ return "scan(" + mQuery + "," + isScanning() + ")";
+ }
+
+ @Override
+ public void start(
+ FutureCallback<Void> onStart,
+ AdvertisementFoundListener foundListener,
+ AdvertisementLostListener lostListener,
+ FutureCallback<Void> onStop,
+ Duration timeout) {
+ Log.d(TAG, "Entering start.");
+ if (isScanning()) {
+ onStart.onFailure(
+ new IllegalStateException("Already scanning."));
+ return;
+ }
+ if (timeout == null) {
+ throw new IllegalArgumentException("Null timeout.");
+ }
+ mDuration = timeout;
+ Log.d(TAG, "Starting scan with q=[" + mQuery + "]");
+ if (mDiscovery == null) {
+ onStart.onFailure(
+ new IllegalStateException("Discovery not ready."));
+ return;
+ }
+ mScanCtx = contextWithTimeout(mDuration);
+
+ Futures.addCallback(
+ InputChannels.withCallback(
+ mDiscovery.scan(mScanCtx, mQuery),
+ makeUpdateCallback(foundListener, lostListener)),
+ onStop);
+
+ onStart.onSuccess(null);
+
+ Log.d(TAG, "Exiting start.");
+ }
+
+ private InputChannelCallback<Update> makeUpdateCallback(
+ final AdvertisementFoundListener foundListener,
+ final AdvertisementLostListener lostListener) {
+ return new InputChannelCallback<Update>() {
+ @Override
+ public ListenableFuture<Void> onNext(Update result) {
+ if (result instanceof Update.Found) {
+ foundListener.handleFoundAdvertisement(
+ ((Update.Found) result).getElem().getService());
+ } else {
+ lostListener.handleLostAdvertisement(
+ ((Update.Lost) result).getElem().getService());
+ }
+ return Futures.immediateFuture(null);
+ }
+ };
+ }
+
+ @Override
+ public boolean isScanning() {
+ return mScanCtx != null && !mScanCtx.isCanceled();
+ }
+
+ @Override
+ public void stop() {
+ Log.d(TAG, "Entering stop");
+ if (mScanCtx != null) {
+ Log.d(TAG, "Cancelling scan.");
+ if (!mScanCtx.isCanceled()) {
+ mScanCtx.cancel();
+ }
+ mScanCtx = null;
+ }
+ Log.d(TAG, "Exiting stop");
+ }
+ }
+}
diff --git a/projects/moments/app/src/main/java/io/v/moments/v23/package-info.java b/projects/moments/app/src/main/java/io/v/moments/v23/package-info.java
new file mode 100644
index 0000000..e658636
--- /dev/null
+++ b/projects/moments/app/src/main/java/io/v/moments/v23/package-info.java
@@ -0,0 +1,13 @@
+// Copyright 2015 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.
+
+/**
+ * Below this point sits code that wraps the underlying, evolving v23 API.
+ *
+ * None of it depends on 'moments'-specific code/data.
+ *
+ * The wrapper facilitates the use of the underlying API, and makes it easier to
+ * test classes that use the API.
+ */
+package io.v.moments.v23;
diff --git a/projects/moments/app/src/test/java/io/v/moments/lib/DiscoveredListTest.java b/projects/moments/app/src/test/java/io/v/moments/lib/DiscoveredListTest.java
index 5a91cdc..4cd4df0 100644
--- a/projects/moments/app/src/test/java/io/v/moments/lib/DiscoveredListTest.java
+++ b/projects/moments/app/src/test/java/io/v/moments/lib/DiscoveredListTest.java
@@ -6,8 +6,6 @@
import android.os.Handler;
-import com.google.common.collect.ImmutableList;
-
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -18,20 +16,10 @@
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.UUID;
-
-import io.v.moments.ifc.AdConverter;
import io.v.moments.ifc.HasId;
import io.v.moments.ifc.IdSet;
import io.v.moments.ifc.ListObserver;
-import io.v.v23.discovery.Attachments;
-import io.v.v23.discovery.Attributes;
-import io.v.v23.discovery.Found;
-import io.v.v23.discovery.Lost;
-import io.v.v23.discovery.Service;
-import io.v.v23.discovery.Update;
+import io.v.moments.v23.ifc.AdConverter;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.never;
@@ -150,7 +138,7 @@
mList.setObserver(mObserver);
when(mConverter.make(mAdvertisement)).thenReturn(THING0);
- mList.scanUpdateReceived(new Update.Found(new Found(mAdvertisement)));
+ mList.handleFoundAdvertisement(mAdvertisement);
verify(mHandler).post(mRunnable.capture());
mRunnable.getValue().run();
@@ -168,7 +156,7 @@
when(mRejects.contains(ID0)).thenReturn(true);
- mList.scanUpdateReceived(new Update.Found(new Found(mAdvertisement)));
+ mList.handleFoundAdvertisement(mAdvertisement);
verifyZeroInteractions(mHandler);
verifyZeroInteractions(mObserver);
@@ -182,7 +170,7 @@
public void handleUnrecognizedLost() throws Exception {
mList.setObserver(mObserver);
- mList.scanUpdateReceived(new Update.Lost(new Lost(mAdvertisement)));
+ mList.handleLostAdvertisement(mAdvertisement);
verify(mHandler).post(mRunnable.capture());
mRunnable.getValue().run();
@@ -202,7 +190,7 @@
when(mRejects.contains(ID0)).thenReturn(true);
- mList.scanUpdateReceived(new Update.Lost(new Lost(mAdvertisement)));
+ mList.handleLostAdvertisement(mAdvertisement);
verify(mHandler).post(mRunnable.capture());
mRunnable.getValue().run();
@@ -223,7 +211,7 @@
verify(mObserver).notifyItemInserted(0);
assertEquals(1, mList.size());
- mList.scanUpdateReceived(new Update.Lost(new Lost(mAdvertisement)));
+ mList.handleLostAdvertisement(mAdvertisement);
verify(mHandler).post(mRunnable.capture());
diff --git a/projects/moments/app/src/test/java/io/v/moments/model/AdConverterMomentTest.java b/projects/moments/app/src/test/java/io/v/moments/model/AdConverterMomentTest.java
index d4b69a1..1097ca0 100644
--- a/projects/moments/app/src/test/java/io/v/moments/model/AdConverterMomentTest.java
+++ b/projects/moments/app/src/test/java/io/v/moments/model/AdConverterMomentTest.java
@@ -29,7 +29,7 @@
import io.v.moments.ifc.MomentFactory;
import io.v.moments.lib.Id;
import io.v.moments.lib.ObservedList;
-import io.v.moments.lib.V23Manager;
+import io.v.moments.v23.ifc.V23Manager;
import io.v.v23.context.VContext;
import static org.junit.Assert.assertEquals;
@@ -87,7 +87,7 @@
when(mMoment.getId()).thenReturn(ID);
when(mAdvertisement.getAttrs()).thenReturn(mAttributes);
when(mAdvertisement.getAddrs()).thenReturn(ADDRESSES);
- when(mMomentFactory.makeFromAttributes(
+ when(mMomentFactory.fromAttributes(
eq(ID), anyInt(), eq(mAttributes))).thenReturn(mMoment);
when(mClientFactory.makeClient(eq("/" + ADDRESS0))).thenReturn(mClient);
when(mV23Manager.contextWithTimeout(
@@ -124,7 +124,7 @@
// Make the moment - this is the call being tested.
assertEquals(mMoment, mConverter.make(mAdvertisement));
- verify(mMomentFactory).makeFromAttributes(
+ verify(mMomentFactory).fromAttributes(
eq(ID), mOrdinal.capture(), eq(mAttributes));
// The ordinal value supplied to the factory should be one.
diff --git a/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserFactoryTest.java b/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserFactoryTest.java
index 28718ce..68f1672 100644
--- a/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserFactoryTest.java
+++ b/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserFactoryTest.java
@@ -4,11 +4,14 @@
package io.v.moments.model;
+import org.joda.time.Duration;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
@@ -16,15 +19,20 @@
import java.util.Iterator;
import java.util.Set;
-import io.v.moments.ifc.Advertiser;
import io.v.moments.ifc.Moment;
import io.v.moments.ifc.MomentFactory;
import io.v.moments.lib.Id;
-import io.v.moments.lib.V23Manager;
+import io.v.moments.v23.ifc.AdCampaign;
+import io.v.moments.v23.ifc.Advertiser;
+import io.v.moments.v23.ifc.V23Manager;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertSame;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
@@ -35,15 +43,29 @@
@Rule
public ExpectedException mThrown = ExpectedException.none();
+ @Captor
+ ArgumentCaptor<AdCampaign> mCampaign;
+ @Captor
+ ArgumentCaptor<Duration> mDuration;
+
@Mock
V23Manager mV23Manager;
@Mock
+ MomentFactory mMomentFactory;
+ @Mock
Moment mMoment;
+ @Mock
+ Advertiser mAdvertiser0;
+ @Mock
+ Advertiser mAdvertiser1;
+
AdvertiserFactory mFactory;
@Before
public void setup() throws Exception {
- mFactory = new AdvertiserFactory(mV23Manager);
+ mFactory = new AdvertiserFactory(mV23Manager, mMomentFactory);
+ when(mV23Manager.makeAdvertiser(
+ any(AdCampaign.class))).thenReturn(mAdvertiser0);
}
@Test
@@ -51,18 +73,27 @@
when(mMoment.getId()).thenReturn(ID0);
Advertiser a0 = mFactory.getOrMake(mMoment);
+ assertSame(mAdvertiser0, a0);
assertTrue(mFactory.contains(ID0));
Iterator<Advertiser> iter = mFactory.allAdvertisers().iterator();
assertEquals(a0, iter.next());
assertFalse(iter.hasNext());
+
+ verify(mV23Manager).makeAdvertiser(mCampaign.capture());
+
+ assertNotNull(mCampaign.getValue());
}
@Test
public void makeTwo() throws Exception {
when(mMoment.getId()).thenReturn(ID0);
Advertiser a0 = mFactory.getOrMake(mMoment);
+
when(mMoment.getId()).thenReturn(ID1);
+ when(mV23Manager.makeAdvertiser(
+ any(AdCampaign.class))).thenReturn(mAdvertiser1);
+
Advertiser a1 = mFactory.getOrMake(mMoment);
assertTrue(mFactory.contains(ID0));
diff --git a/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserImplTest.java b/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserImplTest.java
deleted file mode 100644
index 7ac73fc..0000000
--- a/projects/moments/app/src/test/java/io/v/moments/model/AdvertiserImplTest.java
+++ /dev/null
@@ -1,171 +0,0 @@
-// Copyright 2015 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.moments.model;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.runners.MockitoJUnitRunner;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import io.v.moments.ifc.Moment;
-import io.v.moments.lib.Id;
-import io.v.moments.lib.V23Manager;
-import io.v.v23.context.VContext;
-import io.v.v23.discovery.Attributes;
-import io.v.v23.discovery.Service;
-import io.v.v23.naming.Endpoint;
-import io.v.v23.rpc.Server;
-import io.v.v23.rpc.ServerStatus;
-import io.v.v23.security.BlessingPattern;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-@RunWith(MockitoJUnitRunner.class)
-public class AdvertiserImplTest {
- static final Id ID = Id.makeRandom();
- static final String MOMENT_NAME = "cabbage";
- static final String ADDRESS = "192.168.notrealfoo";
-
- @Rule
- public ExpectedException mThrown = ExpectedException.none();
-
- @Mock
- V23Manager mV23Manager;
- @Mock
- VContext mServerContext;
- @Mock
- Server mServer;
- @Mock
- Moment mMoment;
- @Mock
- Endpoint mEndpoint;
- @Mock
- ServerStatus mServerStatus;
- @Mock
- VContext mContext;
-
- @Captor
- ArgumentCaptor<Service> mAdvertisement;
- @Captor
- ArgumentCaptor<List<BlessingPattern>> mBlessingList;
-
- Attributes mAttrs;
-
- AdvertiserImpl mAdvertiser;
-
- @Before
- public void setup() throws Exception {
- mAttrs = new Attributes(makeFakeAttributes());
-
- when(mV23Manager.makeServerContext(
- eq(AdvertiserImpl.NO_MOUNT_NAME),
- any(AdvertiserImpl.MomentServer.class))).thenReturn(mServerContext);
- when(mV23Manager.getServer(mServerContext)).thenReturn(mServer);
- when(mServer.getStatus()).thenReturn(mServerStatus);
-
- Endpoint[] endpoints = {mEndpoint};
- when(mEndpoint.toString()).thenReturn(ADDRESS);
-
- when(mServerStatus.getEndpoints()).thenReturn(endpoints);
-
- when(mMoment.getId()).thenReturn(ID);
- when(mMoment.toString()).thenReturn(MOMENT_NAME);
- when(mMoment.makeAttributes()).thenReturn(mAttrs);
-
- List<BlessingPattern> list = any();
- when(mV23Manager.advertise(
- any(Service.class),
- list)).thenReturn(mContext);
-
- mAdvertiser = new AdvertiserImpl(mV23Manager, mMoment);
- }
-
- @Test
- public void construction1() {
- mThrown.expect(IllegalArgumentException.class);
- mThrown.expectMessage("Null v23Manager");
- mAdvertiser = new AdvertiserImpl(null, mMoment);
- }
-
- @Test
- public void construction2() {
- mThrown.expect(IllegalArgumentException.class);
- mThrown.expectMessage("Null moment");
- mAdvertiser = new AdvertiserImpl(mV23Manager, null);
- }
-
- @Test
- public void advertiseStartSuccess() {
- assertFalse(mAdvertiser.isAdvertising());
- mAdvertiser.start();
-
- verify(mV23Manager).advertise(
- mAdvertisement.capture(),
- mBlessingList.capture());
-
- assertEquals(0, mBlessingList.getValue().size());
-
- Service service = mAdvertisement.getValue();
- assertEquals(ID.toString(), service.getInstanceId());
- assertEquals(MOMENT_NAME, service.getInstanceName());
- assertEquals(Config.INTERFACE_NAME, service.getInterfaceName());
- assertSame(mAttrs, service.getAttrs());
-
- List<String> addresses = service.getAddrs();
- assertTrue(addresses.contains(ADDRESS));
- assertEquals(1, addresses.size());
-
- assertTrue(mAdvertiser.isAdvertising());
- }
-
- @Test
- public void advertiseStartFailure() {
- assertFalse(mAdvertiser.isAdvertising());
- mAdvertiser.start();
- assertTrue(mAdvertiser.isAdvertising());
- mThrown.expect(IllegalStateException.class);
- mThrown.expectMessage("Already advertising.");
- mAdvertiser.start();
- }
-
- @Test
- public void advertiseStopFailure() {
- mThrown.expect(IllegalStateException.class);
- mThrown.expectMessage("Not advertising.");
- mAdvertiser.stop();
- }
-
- @Test
- public void advertiseStopSuccess() throws Exception {
- advertiseStartSuccess();
- assertTrue(mAdvertiser.isAdvertising());
- mAdvertiser.stop();
- verify(mContext).cancel();
- verify(mServerContext).cancel();
- assertFalse(mAdvertiser.isAdvertising());
- }
-
- static Map<String, String> makeFakeAttributes() {
- Map<String, String> result = new HashMap<>();
- result.put("color", "teal");
- return result;
- }
-}
diff --git a/projects/moments/app/src/test/java/io/v/moments/model/BitMapperTest.java b/projects/moments/app/src/test/java/io/v/moments/model/BitMapperTest.java
index e06f188..3c72241 100644
--- a/projects/moments/app/src/test/java/io/v/moments/model/BitMapperTest.java
+++ b/projects/moments/app/src/test/java/io/v/moments/model/BitMapperTest.java
@@ -19,6 +19,7 @@
import java.io.File;
import io.v.moments.ifc.Moment;
+import io.v.moments.lib.FileUtil;
import static org.junit.Assert.assertEquals;
diff --git a/projects/moments/app/src/test/java/io/v/moments/model/MomentAdCampaignTest.java b/projects/moments/app/src/test/java/io/v/moments/model/MomentAdCampaignTest.java
new file mode 100644
index 0000000..dce6e6a
--- /dev/null
+++ b/projects/moments/app/src/test/java/io/v/moments/model/MomentAdCampaignTest.java
@@ -0,0 +1,105 @@
+// Copyright 2015 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.moments.model;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.joda.time.DateTime;
+
+import io.v.moments.ifc.Moment;
+import io.v.moments.ifc.MomentFactory;
+import io.v.moments.lib.Id;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Attributes;
+import io.v.v23.rpc.ServerCall;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MomentAdCampaignTest {
+ static final Id ID = Id.makeRandom();
+ static final String PIZZA = "pizza";
+ static final String AUTHOR = "shake a spear";
+ static final String CAPTION = "If we pull this off, we'll eat like kings.";
+ static final DateTime CREATION_TIME = DateTime.now();
+
+ @Rule
+ public ExpectedException mThrown = ExpectedException.none();
+
+ @Mock
+ MomentFactory mMomentFactory;
+ @Mock
+ Moment mMoment;
+ @Mock
+ Attributes mAttributes;
+ @Mock
+ VContext mCtx;
+ @Mock
+ ServerCall mCall;
+
+ MomentAdCampaign mCampaign;
+
+ @Before
+ public void setup() throws Exception {
+ when(mMoment.getId()).thenReturn(ID);
+ when(mMoment.toString()).thenReturn(PIZZA);
+ when(mMoment.getCaption()).thenReturn(CAPTION);
+ when(mMoment.getAuthor()).thenReturn(AUTHOR);
+ when(mMoment.getCreationTime()).thenReturn(CREATION_TIME);
+ when(mMomentFactory.toAttributes(mMoment)).thenReturn(mAttributes);
+ mCampaign = new MomentAdCampaign(mMoment, mMomentFactory);
+ }
+
+ @Test
+ public void makeWithoutMomentThrowsException() {
+ mThrown.expect(IllegalArgumentException.class);
+ mThrown.expectMessage("Null moment");
+ mCampaign = new MomentAdCampaign(null, mMomentFactory);
+ }
+
+ @Test
+ public void makeWithoutFactoryThrowsException() {
+ mThrown.expect(IllegalArgumentException.class);
+ mThrown.expectMessage("Null factory");
+ mCampaign = new MomentAdCampaign(mMoment, null);
+ }
+
+ @Test
+ public void emptyMountName() throws Exception {
+ assertEquals("", mCampaign.getMountName());
+ }
+
+ @Test
+ public void properInterfaceName() throws Exception {
+ assertEquals(
+ MomentAdCampaign.INTERFACE_NAME, mCampaign.getInterfaceName());
+ }
+
+ @Test
+ public void factoryMakesAttributes() throws Exception {
+ assertSame(mAttributes, mCampaign.getAttributes());
+ }
+
+ /**
+ * TODO(jregan): Service needs more coverage.
+ */
+ @Test
+ public void checkService() throws Exception {
+ MomentIfcServer server = (MomentIfcServer) mCampaign.makeService();
+ assertNotNull(server);
+ MomentWireData data = server.getBasics(mCtx, mCall).get();
+ assertEquals(AUTHOR, data.getAuthor());
+ assertEquals(CAPTION, data.getCaption());
+ assertEquals(CREATION_TIME.getMillis(), data.getCreationTime());
+ }
+}
diff --git a/projects/moments/app/src/test/java/io/v/moments/v23/impl/V23ManagerImplTest.java b/projects/moments/app/src/test/java/io/v/moments/v23/impl/V23ManagerImplTest.java
new file mode 100644
index 0000000..a25763e
--- /dev/null
+++ b/projects/moments/app/src/test/java/io/v/moments/v23/impl/V23ManagerImplTest.java
@@ -0,0 +1,65 @@
+// Copyright 2015 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.moments.v23.impl;
+
+import android.app.Activity;
+import android.content.Context;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import io.v.v23.security.Blessings;
+
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class V23ManagerImplTest {
+
+ @Rule
+ public ExpectedException mThrown = ExpectedException.none();
+
+ @Mock
+ Activity mActivity;
+ @Mock
+ Context mContext;
+
+ V23ManagerImpl mManager;
+
+ FutureCallback<Blessings> makeBlessingsCallback() {
+ return new FutureCallback<Blessings>() {
+ @Override
+ public void onSuccess(Blessings blessings) {
+ }
+
+ @Override
+ public void onFailure(final Throwable t) {
+ }
+ };
+ }
+
+ @Before
+ public void setup() throws Exception {
+ mManager = new V23ManagerImpl();
+ when(mActivity.getApplicationContext()).thenReturn(mContext);
+ }
+
+ // Disabled @Test
+ public void initialization() throws Exception {
+ mManager.init(mActivity, makeBlessingsCallback());
+ mManager.shutdown();
+ }
+
+ @Test
+ public void placeholder() {
+ // TODO(jregan): Add tests. Tricky because must start v23 runtime.
+ }
+}