Merge "android-lib: Add VBeam class for sharing over NFC."
diff --git a/android-lib/build.gradle b/android-lib/build.gradle
index 1f0511b..6eac8e0 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,6 +17,7 @@
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.
@@ -56,6 +58,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..b9b89a3 100644
--- a/android-lib/src/main/AndroidManifest.xml
+++ b/android-lib/src/main/AndroidManifest.xml
@@ -14,10 +14,21 @@
<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"/>
+ <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..e1950ca
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/services/beam/BeamActivity.java
@@ -0,0 +1,108 @@
+// 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(VBeam.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/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);
+ }
+}