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);
+    }
+}