Merge branch 'master' into ble
diff --git a/android-lib/build.gradle b/android-lib/build.gradle
index a091e99..f2d5260 100644
--- a/android-lib/build.gradle
+++ b/android-lib/build.gradle
@@ -15,11 +15,11 @@
 apply plugin: 'wrapper'
 
 android {
-    buildToolsVersion '21.1.2'
-    compileSdkVersion 19
+    buildToolsVersion '22.0.1'
+    compileSdkVersion 22
 
     defaultConfig {
-        minSdkVersion 19
+        minSdkVersion 21
     }
 
     lintOptions {
@@ -41,7 +41,7 @@
     androidTestCompile 'com.google.truth:truth:0.25'
     // This dependency exists only to work around an issue in the sdkmanager plugin v0.12.0. This
     // should be fixed when http://git.io/vIXec is checked in and a release is made.
-    androidTestCompile('com.android.support:support-v4:21.0.0')
+    androidTestCompile 'com.android.support:support-v4:21.0.0'
     androidTestCompile('com.android.support.test:runner:0.3') {
         exclude group: 'junit'  // junit:junit-dep conflicts with junit:unit
     }
diff --git a/android-lib/src/main/java/io/v/android/libs/discovery/ble/BlePlugin.java b/android-lib/src/main/java/io/v/android/libs/discovery/ble/BlePlugin.java
new file mode 100644
index 0000000..ddff860
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/libs/discovery/ble/BlePlugin.java
@@ -0,0 +1,348 @@
+// 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.android.libs.discovery.ble;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanSettings;
+import android.content.Context;
+import android.util.Log;
+
+import org.joda.time.Duration;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.UUID;
+
+import io.v.impl.google.lib.discovery.DeviceCache;
+import io.v.impl.google.lib.discovery.UUIDUtil;
+import io.v.impl.google.lib.discovery.VScanner;
+import io.v.impl.google.lib.discovery.ble.BleAdvertisementConverter;
+import io.v.v23.context.VContext;
+import io.v.impl.google.lib.discovery.ScanHandler;
+import io.v.x.ref.lib.discovery.Advertisement;
+
+/**
+ * The Discovery Plugin Interface for Bluetooth.
+ */
+public class BlePlugin {
+    // We are using a constant for the MTU because Android and paypal/gatt don't get along
+    // when the paypal gatt client sends a setMTU message.  The Android server seems to send
+    // a malformed L2CAP message.
+    private static final int MTU = 23;
+
+    // Object used to lock advertisement objects.
+    private final Object advertisementLock;
+    // The id to assign to the next advertisment.
+    private int nextAdv;
+    // A map of advertisement ids to the advertisement that corresponds to them.
+    private Map<Integer, BluetoothGattService> advertisements;
+    // A map of advertisement ids to the thread waiting for cancellation of the context.
+    private Map<Integer, Thread> advCancellationThreads;
+
+    // Object used to lock scanner objects
+    private final Object scannerLock;
+    // A map of scanner ids to the thread waiting for cancellation of the context.
+    private Map<Integer, Thread> scanCancellationThreads;
+    private DeviceCache cachedDevices;
+    // Used to track the set of devices we currently talking to.
+    private Set<String> pendingCalls;
+
+    // Set of Ble objects that will be interacted with to perform operations.
+    private BluetoothLeAdvertiser bluetoothLeAdvertise;
+    private BluetoothLeScanner bluetoothLeScanner;
+    private BluetoothGattServer bluetoothGattServer;
+
+    // We need to hold onto the callbacks for scan an advertise because that is what is used
+    // to stop the operation.
+    private ScanCallback scanCallback;
+    private AdvertiseCallback advertiseCallback;
+
+    private boolean isScanning;
+
+    private Context androidContext;
+
+
+    // A thread to wait for the cancellation of a particular advertisement.  VContext.done().await()
+    // is blocking so have to spin up a thread per outstanding advertisement.
+    private class AdvertisementCancellationRunner implements Runnable{
+        private VContext ctx;
+
+        private int id;
+        AdvertisementCancellationRunner(VContext ctx, int id) {
+            this.id = id;
+            this.ctx = ctx;
+        }
+
+        @Override
+        public void run() {
+            Uninterruptibles.awaitUninterruptibly(ctx.done());
+            BlePlugin.this.removeAdvertisement(id);
+        }
+    }
+
+    // Similar to AdvertisementCancellationRunner except for scanning.
+    private class ScannerCancellationRunner implements Runnable{
+        private VContext ctx;
+
+        private int id;
+        ScannerCancellationRunner(VContext ctx, int id) {
+            this.id = id;
+            this.ctx = ctx;
+        }
+
+        @Override
+        public void run() {
+            Uninterruptibles.awaitUninterruptibly(ctx.done());
+            BlePlugin.this.removeScanner(id);
+        }
+    }
+
+    public BlePlugin(Context androidContext) {
+        advertisementLock = new Object();
+        nextAdv = 0;
+        advertisements = new TreeMap<>();
+        advCancellationThreads = new HashMap<>();
+
+        scannerLock = new Object();
+        cachedDevices = new DeviceCache(Duration.standardMinutes(5));
+        ;
+        scanCancellationThreads = new HashMap<>();
+        isScanning = false;
+        pendingCalls = new HashSet<>();
+
+        bluetoothLeAdvertise = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
+        bluetoothLeScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
+        this.androidContext = androidContext;
+        BluetoothManager manager = (BluetoothManager) androidContext.getSystemService(
+                Context.BLUETOOTH_SERVICE);
+        bluetoothGattServer = manager.openGattServer(androidContext,
+                new BluetoothGattServerCallback() {
+            @Override
+            public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
+                super.onConnectionStateChange(device, status, newState);
+            }
+
+            @Override
+            public void onCharacteristicReadRequest(BluetoothDevice device, int requestId,
+                                                    int offset,
+                                                    BluetoothGattCharacteristic characteristic) {
+                super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
+                byte[] total =characteristic.getValue();
+                byte[] res = {};
+                // Only send MTU - 1 bytes. The first byte of all packets is the op code.
+                if (offset < total.length) {
+                    int finalByte = offset + MTU - 1;
+                    if (finalByte > total.length) {
+                        finalByte = total.length;
+                    }
+                    res = Arrays.copyOfRange(total, offset, finalByte);
+                    bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, res);
+                } else {
+                    bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_FAILURE, 0,  res);
+                }
+            }
+        });
+    }
+
+
+    // Converts a Vanadium Advertisement to a Bluetooth gatt service.
+    private BluetoothGattService convertToService(Advertisement adv) throws IOException {
+        Map<UUID, byte[]> attributes = BleAdvertisementConverter.vAdvertismentToBleAttr(adv);
+        BluetoothGattService service = new BluetoothGattService(
+                UUIDUtil.UuidToUUID(adv.getServiceUuid()), BluetoothGattService.SERVICE_TYPE_PRIMARY);
+        for (Map.Entry<UUID, byte[]> entry : attributes.entrySet()) {
+            BluetoothGattCharacteristic ch = new BluetoothGattCharacteristic(entry.getKey(), 0,
+                    BluetoothGattCharacteristic.PERMISSION_READ);
+            ch.setValue(entry.getValue());
+            service.addCharacteristic(ch);
+        }
+        return service;
+    }
+
+    public void addAdvertisement(VContext ctx, Advertisement advertisement) throws IOException {
+        BluetoothGattService service = convertToService(advertisement);
+        synchronized (advertisementLock) {
+            int currentId = nextAdv++;
+            advertisements.put(currentId, service);
+            Thread t = new Thread(new AdvertisementCancellationRunner(ctx, currentId));
+            t.start();
+            advCancellationThreads.put(currentId, t);
+            bluetoothGattServer.addService(service);
+            readvertise();
+        }
+    }
+
+    private void removeAdvertisement(int id) {
+        synchronized (advertisements) {
+            BluetoothGattService s = advertisements.get(id);
+            if (s != null) {
+                bluetoothGattServer.removeService(s);
+            }
+            advertisements.remove(id);
+            advCancellationThreads.remove(id);
+            readvertise();
+        }
+    }
+
+    public void addScanner(VContext ctx, UUID serviceUUID,  ScanHandler handler) {
+        VScanner scanner = new VScanner(serviceUUID, handler);
+        int currentId = cachedDevices.addScanner(scanner);
+        synchronized (scannerLock) {
+            Thread t = new Thread(new ScannerCancellationRunner(ctx, currentId));
+            t.start();
+            scanCancellationThreads.put(currentId, t);
+            updateScanning();
+        }
+    }
+
+    private void removeScanner(int id) {
+        cachedDevices.removeScanner(id);
+        synchronized (scannerLock) {
+            scanCancellationThreads.remove(id);
+            updateScanning();
+        }
+    }
+
+    private void updateScanning() {
+        if (isScanning && scanCancellationThreads.size() == 0) {
+            isScanning = false;
+            bluetoothLeScanner.stopScan(scanCallback);
+            return;
+        }
+
+        if (!isScanning && scanCancellationThreads.size() > 0) {
+            isScanning = true;
+            ScanFilter.Builder builder = new ScanFilter.Builder();
+            byte[] manufacturerData = {};
+            byte[] manufacturerMask = {};
+
+            builder.setManufacturerData(1001, manufacturerData, manufacturerMask);
+            final List<ScanFilter> scanFilter = new ArrayList<>();
+            scanFilter.add(builder.build());
+
+
+            scanCallback = new ScanCallback() {
+                @Override
+                public void onScanResult(int callbackType, ScanResult result) {
+                    // in L the only value for callbackType is CALLBACK_TYPE_ALL_MATCHES, so
+                    // we don't look at its value.
+                    ScanRecord record = result.getScanRecord();
+                    // Use 1001 to denote that this is a Vanadium device.  We picked an id that is
+                    // currently not in use.
+                    byte[] data = record.getManufacturerSpecificData(1001);
+                    ByteBuffer buffer = ByteBuffer.wrap(data);
+                    final long hash = buffer.getLong();
+                    final String deviceId = result.getDevice().getAddress();
+                    if (cachedDevices.haveSeenHash(hash, deviceId)) {
+                        return;
+                    }
+                    synchronized (scannerLock) {
+                        if (pendingCalls.contains(deviceId)) {
+                            Log.d("vanadium", "not connecting to " + deviceId + " because of pending connection");
+                            return;
+                        }
+                        pendingCalls.add(deviceId);
+                    }
+                    BluetoothGattClientCallback.Callback ccb = new BluetoothGattClientCallback.Callback() {
+                        @Override
+                        public void handle(Map<UUID, Map<UUID, byte[]>> services) {
+                            Set<Advertisement> advs = new HashSet<>();
+                            for (Map.Entry<UUID, Map<UUID, byte[]>> entry : services.entrySet()) {
+                                try {
+                                    Advertisement adv =
+                                            BleAdvertisementConverter.
+                                                    bleAttrToVAdvertisement(entry.getValue());
+                                    advs.add(adv);
+                                } catch (IOException e) {
+                                    Log.e("vanadium","Failed to convert advertisement" + e);
+                                }
+                            }
+                            cachedDevices.saveDevice(hash, advs, deviceId);
+                            synchronized (scannerLock) {
+                                pendingCalls.remove(deviceId);
+                            }
+                            bluetoothLeScanner.startScan(scanFilter, new ScanSettings.Builder().
+                                    setScanMode(ScanSettings.SCAN_MODE_BALANCED).build(), scanCallback);
+                        }
+                    };
+                    BluetoothGattClientCallback cb = new BluetoothGattClientCallback(ccb);
+                    bluetoothLeScanner.stopScan(scanCallback);
+                    Log.d("vanadium", "connecting to " + result.getDevice());
+                    result.getDevice().connectGatt(androidContext, false, cb);
+                }
+
+                @Override
+                public void onBatchScanResults(List<ScanResult> results) {
+                }
+
+                @Override
+                public void onScanFailed(int errorCode) {
+                }
+            };
+            bluetoothLeScanner.startScan(scanFilter, new ScanSettings.Builder().
+                    setScanMode(ScanSettings.SCAN_MODE_BALANCED).build(), scanCallback);
+        }
+    }
+
+    private void readvertise() {
+        if (advertiseCallback != null) {
+            bluetoothLeAdvertise.stopAdvertising(advertiseCallback);
+            advertiseCallback = null;
+        }
+        if (advertisements.size() == 0) {
+            return;
+        }
+
+        int hash = advertisements.hashCode();
+
+        AdvertiseData.Builder builder = new AdvertiseData.Builder();
+        ByteBuffer buf = ByteBuffer.allocate(9);
+        buf.put((byte) 8);
+        buf.putLong(hash);
+        builder.addManufacturerData(1001, buf.array());
+        AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder();
+        settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY);
+        settingsBuilder.setConnectable(true);
+        advertiseCallback = new AdvertiseCallback() {
+                    @Override
+                    public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+                        Log.i("vanadium", "Successfully started " + settingsInEffect);
+                    }
+
+                    @Override
+                    public void onStartFailure(int errorCode) {
+                        Log.i("vanadium", "Failed to start advertising " + errorCode);
+                    }
+                };
+        bluetoothLeAdvertise.startAdvertising(settingsBuilder.build(), builder.build(),
+                advertiseCallback);
+    }
+}
\ No newline at end of file
diff --git a/android-lib/src/main/java/io/v/android/libs/discovery/ble/BluetoothGattClientCallback.java b/android-lib/src/main/java/io/v/android/libs/discovery/ble/BluetoothGattClientCallback.java
new file mode 100644
index 0000000..62e3c3e
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/libs/discovery/ble/BluetoothGattClientCallback.java
@@ -0,0 +1,99 @@
+// 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.android.libs.discovery.ble;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A handler for responses from a GattServer.
+ */
+public class BluetoothGattClientCallback extends BluetoothGattCallback {
+    // We want to ignore the GATT and GAP services, which are 1800 and 1801 respectively.
+    static final String GATT_AND_GAP_PREFIX = "0000180";
+    public interface Callback {
+        void handle(Map<UUID, Map<UUID, byte[]>> services);
+    }
+    private Callback callback;
+
+    private Map<UUID, Map<UUID, byte[]>> services;
+
+    private BluetoothGatt gatt;
+
+    private List<BluetoothGattCharacteristic> chars;
+    private int pos;
+
+    BluetoothGattClientCallback(Callback cb) {
+        callback = cb;
+        chars = new ArrayList<>();
+        services = new Hashtable<>();
+    }
+
+    @Override
+    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+        for (BluetoothGattService service : gatt.getServices()) {
+            // Skip the GATT AND GAP Services.
+            if (service.getUuid().toString().startsWith(GATT_AND_GAP_PREFIX)) {
+                continue;
+            }
+            services.put(service.getUuid(), new HashMap<UUID, byte[]>());
+            // We only keep track of the characteristics that can be read.
+            for (BluetoothGattCharacteristic ch : chars) {
+                if ((ch.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) != 0) {
+                    chars.add(ch);
+                }
+            }
+        }
+        pos = 0;
+        maybeReadNextCharacteristic();
+    }
+
+    // Reads the next characteristic if there is one.  Otherwise calls callback and
+    // closes the gatt connection.
+    private void maybeReadNextCharacteristic() {
+        if (pos >= chars.size()) {
+            gatt.disconnect();
+            gatt.close();
+            callback.handle(services);
+            return;
+        }
+        BluetoothGattCharacteristic c = chars.get(pos++);
+        if (!gatt.readCharacteristic(c)) {
+            Log.d("vanadium", "Failed to read characteristic " + c.getUuid());
+            maybeReadNextCharacteristic();
+        }
+    }
+
+    @Override
+    public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+        UUID serviceUUID = characteristic.getService().getUuid();
+        services.get(serviceUUID).put(characteristic.getUuid(), characteristic.getValue());
+        maybeReadNextCharacteristic();
+    }
+
+    @Override
+    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+        Log.d("vanadium", "new connections state is " + newState);
+
+        this.gatt = gatt;
+        if (status != BluetoothGatt.GATT_SUCCESS || newState != BluetoothGatt.STATE_CONNECTED) {
+            Log.d("vanadium", "failed to connect with status " + status + " state" + newState);
+            gatt.close();
+            callback.handle(null);
+            return;
+        }
+        gatt.discoverServices();
+    }
+}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index c938d52..3adbe3f 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Apr 03 13:08:46 PDT 2015
+#Thu Oct 01 14:08:26 PDT 2015
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-all.zip
diff --git a/lib/src/main/java/io/v/impl/google/lib/discovery/DeviceCache.java b/lib/src/main/java/io/v/impl/google/lib/discovery/DeviceCache.java
new file mode 100644
index 0000000..14ecf28
--- /dev/null
+++ b/lib/src/main/java/io/v/impl/google/lib/discovery/DeviceCache.java
@@ -0,0 +1,179 @@
+// 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.impl.google.lib.discovery;
+
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+
+import org.joda.time.Duration;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.v.x.ref.lib.discovery.Advertisement;
+
+/**
+ * A Cache of ble devices that were seen recently.  The current Vanadium BLE protocol requires
+ * connecting to the advertiser to grab the attributes and the addrs.  This can be expensive
+ * so we only refetch the data if it hash changed.
+ */
+public class DeviceCache {
+    private Map<Long, CacheEntry> cachedDevices;
+    private Map<String, CacheEntry> knownIds;
+
+
+    final AtomicInteger nextScanner = new AtomicInteger(0);
+    private SetMultimap<UUID, Advertisement> knownServices;
+    private Map<Integer, VScanner> scannersById;
+    private SetMultimap<UUID, VScanner> scannersByUUID;
+
+    private Duration maxAge;
+
+
+    public DeviceCache(Duration maxAge) {
+        cachedDevices = new HashMap<>();
+        knownIds = new HashMap<>();
+        knownServices = HashMultimap.create();
+        scannersById = new HashMap<>();
+        scannersByUUID = HashMultimap.create();
+        this.maxAge = maxAge;
+    }
+
+    /**
+     * Returns whether this hash has been seen before.
+     *
+     * @param hash the hash of the advertisement
+     * @param deviceId the deviceId of the advertisement (used to handle rotating ids).
+     * @return true iff this hash is in the cache.
+     */
+    public boolean haveSeenHash(long hash, String deviceId) {
+        synchronized (this) {
+            CacheEntry entry = cachedDevices.get(hash);
+            if (entry != null) {
+                entry.lastSeen = new Date();
+                if (!entry.deviceId.equals(deviceId)) {
+                    // This probably happened becuase a device has changed it's ble mac address.
+                    // We need to update the mac address for this entry.
+                    knownIds.remove(entry.deviceId);
+                    entry.deviceId = deviceId;
+                    knownIds.put(deviceId, entry);
+                }
+            }
+            return entry != null;
+        }
+    }
+
+    /**
+     * Saves the set of advertisements for this deviceId and hash
+     *
+     * @param hash the hash provided by the advertisement.
+     * @param advs the advertisements exposed by the device.
+     * @param deviceId the id of the device.
+     */
+    public void saveDevice(long hash, Set<Advertisement> advs, String deviceId) {
+        CacheEntry entry = new CacheEntry(advs, hash, deviceId);
+        synchronized (this) {
+            CacheEntry oldEntry = knownIds.get(deviceId);
+            Set<Advertisement> oldValues = null;
+            if (oldEntry != null) {
+                cachedDevices.remove(oldEntry.hash);
+                knownIds.remove(oldEntry.deviceId);
+                oldValues = oldEntry.advertisements;
+            } else {
+                oldValues = new HashSet<>();
+            }
+            Set<Advertisement> removed = Sets.difference(oldValues, advs);
+            for (Advertisement adv : removed) {
+                UUID uuid = UUIDUtil.UuidToUUID(adv.getServiceUuid());
+                adv.setLost(true);
+                knownServices.remove(uuid, adv);
+                handleUpdate(adv);
+            }
+
+            Set<Advertisement> added = Sets.difference(advs, oldValues);
+            for (Advertisement adv: added) {
+                UUID uuid = UUIDUtil.UuidToUUID(adv.getServiceUuid());
+                knownServices.put(uuid, adv);
+                handleUpdate(adv);
+            }
+            cachedDevices.put(hash, entry);
+            CacheEntry oldDeviceEntry = knownIds.get(deviceId);
+            if (oldDeviceEntry != null) {
+                // Delete the old hash value.
+                cachedDevices.remove(hash);
+            }
+            knownIds.put(deviceId, entry);
+        }
+    }
+
+    private void handleUpdate(Advertisement adv) {
+        UUID uuid = UUIDUtil.UuidToUUID(adv.getServiceUuid());
+        Set<VScanner> scanners = scannersByUUID.get(uuid);
+        if (scanners == null) {
+            return;
+        }
+        for (VScanner scanner : scanners) {
+            scanner.getHandler().handleUpdate(adv);
+        }
+    }
+
+    /**
+     * Adds a scanner that will be notified when advertisements that match its query have changed.
+     *
+     * @return the handle of the scanner that can be used to remove the scanner.
+     */
+    public int addScanner(VScanner scanner) {
+        synchronized (this) {
+            int id = nextScanner.addAndGet(1);
+            scannersById.put(id, scanner);
+            scannersByUUID.put(scanner.getmServiceUUID(), scanner);
+            Set<Advertisement> knownAdvs = knownServices.get(scanner.getmServiceUUID());
+            if (knownAdvs != null) {
+                for (Advertisement adv : knownAdvs) {
+                    scanner.getHandler().handleUpdate(adv);
+                }
+            }
+            return id;
+        }
+    }
+
+    /**
+     * Removes the scanner matching this id.  This scanner will stop getting updates.
+     */
+    public void removeScanner(int id) {
+        synchronized (this) {
+            VScanner scanner = scannersById.get(id);
+            if (scanner != null) {
+                scannersByUUID.remove(scanner.getmServiceUUID(), scanner);
+                scannersById.remove(id);
+            }
+        }
+    }
+
+    private class CacheEntry {
+        Set<Advertisement> advertisements;
+
+        long hash;
+
+        Date lastSeen;
+
+        String deviceId;
+
+        CacheEntry(Set<Advertisement> advs, long hash, String deviceId) {
+            advertisements = advs;
+            this.hash = hash;
+            lastSeen = new Date();
+            this.deviceId = deviceId;
+        }
+    }
+
+}
diff --git a/lib/src/main/java/io/v/impl/google/lib/discovery/EncodingUtil.java b/lib/src/main/java/io/v/impl/google/lib/discovery/EncodingUtil.java
new file mode 100644
index 0000000..0441eb6
--- /dev/null
+++ b/lib/src/main/java/io/v/impl/google/lib/discovery/EncodingUtil.java
@@ -0,0 +1,132 @@
+// 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.impl.google.lib.discovery;
+
+
+import com.google.common.primitives.Bytes;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.v23.vom.BinaryUtil;
+import io.v.x.ref.lib.discovery.EncryptionKey;
+
+/**
+ * A utility to encode and decode fields in io.v.v23.Service fields for use in discovery.
+ */
+public class EncodingUtil {
+    static final Charset UTF8_CHARSET = Charset.forName("UTF-8");
+
+    /**
+     * Encodes the addresses passed in.
+     * @param addrs The list of addresses to encode.
+     * @return The byte representation of the encoded addresses.
+     * @throws IOException
+     */
+    public static byte[] packAddresses(List<String> addrs) throws IOException {
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        for (String addr : addrs) {
+            BinaryUtil.encodeUint(stream, addr.length());
+            stream.write(addr.getBytes(UTF8_CHARSET));
+        }
+        return stream.toByteArray();
+    }
+
+    /**
+     * Decodes addresses from a byte array that was encoded by packAddresses
+     * @param input The byte array toe decode
+     * @return The list of addresses.
+     * @throws IOException
+     */
+    public static List<String> unpackAddresses(byte[] input) throws IOException {
+        ByteArrayInputStream stream = new ByteArrayInputStream(input);
+        List<String> output = new ArrayList<>();
+        while (stream.available() > 0) {
+            int stringSize = (int)BinaryUtil.decodeUint(stream);
+            byte[] data = new byte[stringSize];
+            int read = stream.read(data);
+            if (read != stringSize) {
+                throw new IOException("Unexpected end of stream while reading address");
+            }
+            output.add(new String(data, UTF8_CHARSET));
+        }
+        return output;
+    }
+
+    /**
+     * Encode the encryption keys and algorithm passed in.
+     * @param encryptionAlgorithm The encryption algorithm to use.
+     *                            See io.v.x.ref.lib.discovery.Constants for valid values.
+     * @param keys The keys to encode
+     * @return The byte array that is the encoded form.
+     * @throws IOException
+     */
+    public static byte[] packEncryptionKeys(int encryptionAlgorithm, List<EncryptionKey> keys)
+            throws IOException {
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        BinaryUtil.encodeUint(stream, encryptionAlgorithm);
+        for (EncryptionKey key : keys) {
+            byte[] byteKey = Bytes.toArray(key);
+            BinaryUtil.encodeUint(stream, byteKey.length);
+            stream.write(byteKey);
+        }
+        return stream.toByteArray();
+    }
+
+
+    /**
+     * Decodes the encryption algorithm and keys that was encoded by packEncryptionKeys.
+     * @param input The byte array containg the keys.
+     * @return The keys and the encryption algorithm in input.
+     * @throws IOException
+     */
+    public static KeysAndAlgorithm unpackEncryptionKeys(byte[] input) throws IOException {
+        ByteArrayInputStream stream = new ByteArrayInputStream(input);
+        int algo = (int) BinaryUtil.decodeUint(stream);
+        List<EncryptionKey> keys = new ArrayList<>();
+        while (stream.available() > 0) {
+            int size = (int) BinaryUtil.decodeUint(stream);
+            byte[] key = new byte[size];
+            int read = stream.read(key);
+            if (read != size) {
+                throw new IOException("Unexpected end of file reading keys");
+            }
+            keys.add(new EncryptionKey(Bytes.asList(key)));
+        }
+        return new KeysAndAlgorithm(algo, keys);
+    }
+
+    /**
+     * An object with EncryptionKeys and the encryption algorithm.
+     */
+    public static class KeysAndAlgorithm {
+        int encryptionAlgorithm;
+        List<EncryptionKey> keys;
+
+        /**
+         * Gets the stored encryption algorithm.
+         */
+        public int getEncryptionAlgorithm() {
+            return encryptionAlgorithm;
+        }
+
+        /**
+         * Gets the stored keys.
+         */
+        public List<EncryptionKey> getKeys() {
+            return keys;
+        }
+
+        KeysAndAlgorithm(int encryptionAlgo, List<EncryptionKey> keys) {
+            encryptionAlgorithm = encryptionAlgo;
+            this.keys = keys;
+        }
+    }
+
+}
diff --git a/lib/src/main/java/io/v/impl/google/lib/discovery/ScanHandler.java b/lib/src/main/java/io/v/impl/google/lib/discovery/ScanHandler.java
new file mode 100644
index 0000000..20414b7
--- /dev/null
+++ b/lib/src/main/java/io/v/impl/google/lib/discovery/ScanHandler.java
@@ -0,0 +1,20 @@
+// 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.impl.google.lib.discovery;
+
+import io.v.x.ref.lib.discovery.Advertisement;
+
+/**
+ * An interface that is passed into a Vanadium Discovery Scan operation that will handle updates.
+ */
+public interface ScanHandler {
+    /**
+     * Called when there is a new advertisement or an update
+     * to and old advertisement
+     */
+    void handleUpdate(Advertisement advertisement);
+}
+
+
diff --git a/lib/src/main/java/io/v/impl/google/lib/discovery/UUIDUtil.java b/lib/src/main/java/io/v/impl/google/lib/discovery/UUIDUtil.java
new file mode 100644
index 0000000..9413ce9
--- /dev/null
+++ b/lib/src/main/java/io/v/impl/google/lib/discovery/UUIDUtil.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.impl.google.lib.discovery;
+
+import com.google.common.primitives.Bytes;
+
+import java.nio.ByteBuffer;
+import java.util.UUID;
+
+import io.v.x.ref.lib.discovery.Uuid;
+
+/**
+ * A class for generating v5 UUIDs and converting from {@link java.util.UUID} and
+ * {@link io.v.x.ref.lib.discovery.Uuid}.  T{@link java.util.UUID} does not provide
+ * any mechanism to compute v5 UUIDs which are used to convert InterfaceNames to service
+ * UUIDs.  Conversion from {@link Uuid} and {@link UUID} is necessary because the VDL type
+ * and the native type are annoying to convert to.
+ */
+public class UUIDUtil {
+    public static native UUID UUIDForInterfaceName(String name);
+
+    public static native UUID UUIDForAttributeKey(String key);
+
+    /**
+     * Converts from {@link java.util.UUID} to {@link Uuid}.
+     */
+    public static Uuid UUIDToUuid(UUID id) {
+        ByteBuffer b = ByteBuffer.allocate(16);
+        b.putLong(id.getMostSignificantBits());
+        b.putLong(id.getLeastSignificantBits());
+        return new Uuid(Bytes.asList(b.array()));
+    }
+
+    /**
+     * Converts from {@link Uuid} to {@link UUID}
+     */
+    public static UUID UuidToUUID(Uuid id) {
+        ByteBuffer b = ByteBuffer.wrap(Bytes.toArray(id));
+        return new UUID(b.getLong(), b.getLong());
+    }
+}
+
diff --git a/lib/src/main/java/io/v/impl/google/lib/discovery/VScanner.java b/lib/src/main/java/io/v/impl/google/lib/discovery/VScanner.java
new file mode 100644
index 0000000..7b6e411
--- /dev/null
+++ b/lib/src/main/java/io/v/impl/google/lib/discovery/VScanner.java
@@ -0,0 +1,32 @@
+// 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.impl.google.lib.discovery;
+
+import java.util.UUID;
+
+import io.v.impl.google.lib.discovery.ScanHandler;
+
+/**
+ * Wraps a {@link UUID} and a {@link ScanHandler}.
+ */
+public class VScanner {
+    private UUID serviceUUID;
+
+    private ScanHandler handler;
+
+    public VScanner(UUID serviceUUID, ScanHandler handler) {
+        this.serviceUUID = serviceUUID;
+        this.handler = handler;
+    }
+
+    public UUID getmServiceUUID() {
+        return serviceUUID;
+    }
+
+    public ScanHandler getHandler() {
+        return handler;
+    }
+
+}
diff --git a/lib/src/main/java/io/v/impl/google/lib/discovery/ble/BleAdvertisementConverter.java b/lib/src/main/java/io/v/impl/google/lib/discovery/ble/BleAdvertisementConverter.java
new file mode 100644
index 0000000..fc1de67
--- /dev/null
+++ b/lib/src/main/java/io/v/impl/google/lib/discovery/ble/BleAdvertisementConverter.java
@@ -0,0 +1,119 @@
+// 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.impl.google.lib.discovery.ble;
+
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import io.v.impl.google.lib.discovery.EncodingUtil;
+import io.v.impl.google.lib.discovery.UUIDUtil;
+import io.v.x.ref.lib.discovery.Advertisement;
+import io.v.v23.discovery.Attributes;
+import io.v.v23.discovery.Service;
+import io.v.x.ref.lib.discovery.EncryptionAlgorithm;
+import io.v.x.ref.lib.discovery.EncryptionKey;
+import io.v.x.ref.lib.discovery.plugins.ble.Constants;
+
+/**
+ * BleAdvertisementConverter converts from {@link Advertisement} to
+ * the gatt Services and vice-versa.
+ */
+public class BleAdvertisementConverter {
+    private static Charset utf8 = Charset.forName("UTF-8");
+
+    /**
+     * Converts from {@link Advertisement} to the ble representation.
+     *
+     * @return map of Characteristic UUIDs to their values.
+     * @throws IOException
+     */
+    public static Map<UUID, byte[]> vAdvertismentToBleAttr(Advertisement adv)
+            throws IOException {
+        Map<UUID, byte[]> attr = new HashMap<>();
+        Service service = adv.getService();
+        attr.put(UUID.fromString(Constants.INTERFACE_NAME_UUID),
+                service.getInterfaceName().getBytes(utf8));
+        attr.put(UUID.fromString(Constants.INSTANCE_UUID), service.getInstanceUuid());
+        attr.put(UUID.fromString(Constants.ADDRS_UUID),
+                EncodingUtil.packAddresses(service.getAddrs()));
+
+        String instanceName = service.getInstanceName();
+        if (instanceName != null && !instanceName.equals("")) {
+            attr.put(UUID.fromString(Constants.INSTANCE_NAME_UUID), instanceName.getBytes(utf8));
+        }
+
+        if (adv.getEncryptionAlgorithm().getValue() != 0) {
+            attr.put(UUID.fromString(Constants.ENCRYPTION_UUID),
+                    EncodingUtil.packEncryptionKeys(adv.getEncryptionAlgorithm().getValue(),
+                            adv.getEncryptionKeys()));
+        }
+
+        for (Map.Entry<String, String> keyAndValue : service.getAttrs().entrySet()) {
+            String key = keyAndValue.getKey();
+            UUID attrKey = UUIDUtil.UUIDForAttributeKey(key);
+            String attrValue = key + "=" + keyAndValue.getValue();
+            attr.put(attrKey, attrValue.getBytes(utf8));
+        }
+        return attr;
+    }
+
+    /**
+     * Converts from Map of Characteristic UUIDs -> values to a
+     * {@link Advertisement}
+     *
+     * @param attr the map of characteristic uuids to their values
+     * @return the Vanadium Advertisement based on characteristics.
+     * @throws IOException
+     */
+    public static Advertisement bleAttrToVAdvertisement(Map<UUID, byte[]> attr)
+            throws IOException {
+        Map<String, String> cleanAttrs = new HashMap<String, String>();
+        byte[] instanceId = null;
+        String interfaceName = null;
+        String instanceName = null;
+        List<String> addrs = null;
+        int encryptionAlgo = 0;
+        List<EncryptionKey> encryptionKeys = null;
+
+        for (Map.Entry<UUID, byte[]> entry : attr.entrySet()) {
+            String uuidKey = entry.getKey().toString();
+            System.out.println("key is " + uuidKey);
+            byte[] value = entry.getValue();
+            if (uuidKey.equals(Constants.INSTANCE_UUID)) {
+                instanceId = value;
+            } else if (uuidKey.equals(Constants.INSTANCE_NAME_UUID)) {
+                instanceName = new String(value, utf8);
+            } else if (uuidKey.equals(Constants.INTERFACE_NAME_UUID)) {
+                interfaceName = new String(value, utf8);
+            } else if (uuidKey.equals(Constants.ADDRS_UUID)) {
+                addrs = EncodingUtil.unpackAddresses(value);
+            } else if (uuidKey.equals(Constants.ENCRYPTION_UUID)) {
+                EncodingUtil.KeysAndAlgorithm res = EncodingUtil.unpackEncryptionKeys(value);
+                encryptionAlgo = res.getEncryptionAlgorithm();
+                encryptionKeys = res.getKeys();
+            } else {
+                String keyAndValue = new String(value, utf8);
+                String[] parts = keyAndValue.split("=", 2);
+                if (parts.length != 2) {
+                    System.err.println("Failed to parse key and value" + keyAndValue);
+                    continue;
+                }
+                cleanAttrs.put(parts[0], parts[1]);
+            }
+        }
+        return new Advertisement(
+                new Service(instanceId, instanceName, interfaceName, new Attributes(cleanAttrs),
+                        addrs),
+                UUIDUtil.UUIDToUuid(UUIDUtil.UUIDForInterfaceName(interfaceName)),
+                new EncryptionAlgorithm(encryptionAlgo),
+                encryptionKeys,
+                false);
+    }
+}
diff --git a/lib/src/main/java/io/v/v23/vom/BinaryUtil.java b/lib/src/main/java/io/v/v23/vom/BinaryUtil.java
index 1a73d90..e739dbb 100644
--- a/lib/src/main/java/io/v/v23/vom/BinaryUtil.java
+++ b/lib/src/main/java/io/v/v23/vom/BinaryUtil.java
@@ -15,7 +15,7 @@
 /**
  * Binary encoding and decoding routines.
  */
-final class BinaryUtil {
+public final class BinaryUtil {
     /**
      * Every binary stream starts with this magic byte, to distinguish the binary encoding from
      * the JSON encoding. Note that every valid JSON encoding must start with an ASCII character,or
diff --git a/lib/src/test/java/io/v/impl/google/lib/discovery/DeviceCacheTest.java b/lib/src/test/java/io/v/impl/google/lib/discovery/DeviceCacheTest.java
new file mode 100644
index 0000000..5cf666e
--- /dev/null
+++ b/lib/src/test/java/io/v/impl/google/lib/discovery/DeviceCacheTest.java
@@ -0,0 +1,209 @@
+// 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.impl.google.lib.discovery;
+
+import junit.framework.TestCase;
+import org.joda.time.Duration;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+import io.v.v23.discovery.Service;
+import io.v.x.ref.lib.discovery.Advertisement;
+import io.v.x.ref.lib.discovery.EncryptionAlgorithm;
+import io.v.x.ref.lib.discovery.Uuid;
+
+/**
+ * Test for {@link DeviceCache}
+ */
+public class DeviceCacheTest extends TestCase {
+    private abstract class CountingHandler implements ScanHandler {
+        protected int mNumCalls = 0;
+
+        @Override
+        public void handleUpdate(Advertisement adv) {}
+    }
+
+    public void testSaveDeveice() {
+        DeviceCache cache = new DeviceCache(new Duration(1000 * 60 * 60));
+        // The advertisements here are not relevant since we are just checking
+        // the seen hash function.
+        Set<Advertisement> advs = new HashSet<>();
+        long hash = 10001;
+        assertFalse(cache.haveSeenHash(hash, "newDevice"));
+        cache.saveDevice(hash, advs, "newDevice");
+        assertTrue(cache.haveSeenHash(hash, "newDevice"));
+    }
+
+    public void testSaveDeviceWithDifferentHashCode() {
+        DeviceCache cache = new DeviceCache(new Duration(1000 * 60 * 60));
+        // The advertisements here are not relevant since we are just checking
+        // the seen hash function.
+        Set<Advertisement> advs = new HashSet<>();
+        long hash = 10001;
+        assertFalse(cache.haveSeenHash(hash, "newDevice"));
+        cache.saveDevice(hash, advs, "newDevice");
+        assertTrue(cache.haveSeenHash(hash, "newDevice"));
+        cache.saveDevice(hash + 1, advs, "newDevice");
+        assertTrue(cache.haveSeenHash(hash + 1, "newDevice"));
+        assertFalse(cache.haveSeenHash(hash, "newDevice"));
+    }
+
+    public void testAddingScannerBeforeSavingDevice() {
+        DeviceCache cache = new DeviceCache(new Duration(1000 * 60 * 60));
+        Set<Advertisement> advs = new HashSet<>();
+        long hash = 10001;
+        assertFalse(cache.haveSeenHash(hash, "newDevice"));
+
+        Service service1 = new Service();
+        service1.setInterfaceName("randomInterface");
+        Uuid uuid1 = UUIDUtil.UUIDToUuid(UUID.randomUUID());
+        final Advertisement adv1 = new Advertisement(service1, uuid1, new EncryptionAlgorithm(0), null, false);
+        advs.add(adv1);
+
+        CountingHandler handler = new CountingHandler() {
+            @Override
+            public void handleUpdate(Advertisement advertisement) {
+                assertEquals(adv1, advertisement);
+                assertEquals(mNumCalls, 0);
+                mNumCalls++;
+            }
+        };
+
+        cache.addScanner(new VScanner(UUIDUtil.UuidToUUID(uuid1), handler));
+
+        Service service2 = new Service();
+        service1.setInterfaceName("randomInterface2");
+        Uuid uuid2 = UUIDUtil.UUIDToUuid(UUID.randomUUID());
+        Advertisement adv2 = new Advertisement(service2, uuid2, new EncryptionAlgorithm(0), null, false);
+        advs.add(adv2);
+
+        cache.saveDevice(hash, advs, "newDevice");
+
+        // Make sure that the handler is called;
+        assertEquals(1, handler.mNumCalls);
+    }
+
+    public void testAddingScannerAfterSavingDevice() {
+        DeviceCache cache = new DeviceCache(new Duration(1000 * 60 * 60));
+        Set<Advertisement> advs = new HashSet<>();
+        long hash = 10001;
+        assertFalse(cache.haveSeenHash(hash, "newDevice"));
+
+        Service service1 = new Service();
+        service1.setInterfaceName("randomInterface");
+        Uuid uuid1 = UUIDUtil.UUIDToUuid(UUID.randomUUID());
+        final Advertisement adv1 = new Advertisement(service1, uuid1, new EncryptionAlgorithm(0), null, false);
+
+        advs.add(adv1);
+
+        CountingHandler handler = new CountingHandler() {
+            @Override
+            public void handleUpdate(Advertisement advertisement) {
+                assertEquals(adv1, advertisement);
+                assertEquals(mNumCalls, 0);
+                mNumCalls++;
+            }
+        };
+
+        Service service2 = new Service();
+        service1.setInterfaceName("randomInterface2");
+        Uuid uuid2 = UUIDUtil.UUIDToUuid(UUID.randomUUID());
+        Advertisement adv2 = new Advertisement(service2, uuid2, new EncryptionAlgorithm(0), null, false);
+        advs.add(adv2);
+        cache.saveDevice(hash, advs, "newDevice");
+
+        cache.addScanner(new VScanner(UUIDUtil.UuidToUUID(uuid1), handler));
+
+        // Make sure that the handler is called;
+        assertEquals(1, handler.mNumCalls);
+    }
+
+    public void testRemovingAnAdvertisementCallsHandler() {
+        DeviceCache cache = new DeviceCache(new Duration(1000 * 60 * 60));
+        Set<Advertisement> advs = new HashSet<>();
+        long hash = 10001;
+        assertFalse(cache.haveSeenHash(hash, "newDevice"));
+
+        Service service1 = new Service();
+        service1.setInterfaceName("randomInterface");
+        Uuid uuid1 = UUIDUtil.UUIDToUuid(UUID.randomUUID());
+        final Advertisement adv1 = new Advertisement(service1, uuid1, new EncryptionAlgorithm(0), null, false);
+        advs.add(adv1);
+
+        CountingHandler handler = new CountingHandler() {
+            @Override
+            public void handleUpdate(Advertisement advertisement) {
+                // The first call should be an add and the second call should be
+                // a remove.
+                if (mNumCalls == 0) {
+                    assertEquals(adv1, advertisement);
+                } else {
+                    Advertisement removed = new Advertisement(
+                            adv1.getService(), adv1.getServiceUuid(),
+                            adv1.getEncryptionAlgorithm(), adv1.getEncryptionKeys(), true);
+                    assertEquals(removed, advertisement);
+                }
+                mNumCalls++;
+            }
+        };
+
+        cache.addScanner(new VScanner(UUIDUtil.UuidToUUID(uuid1), handler));
+
+        Service service2 = new Service();
+        service2.setInterfaceName("randomInterface2");
+        Uuid uuid2 = UUIDUtil.UUIDToUuid(UUID.randomUUID());
+        Advertisement adv2 = new Advertisement(service2, uuid2, new EncryptionAlgorithm(0), null, false);
+        advs.add(adv2);
+
+        cache.saveDevice(hash, advs, "newDevice");
+
+        Set<Advertisement> newAdvs = new HashSet<>();
+        newAdvs.add(adv2);
+
+        cache.saveDevice(10002, newAdvs, "newDevice");
+
+        // Make sure that the handler is called;
+        assertEquals(2, handler.mNumCalls);
+    }
+
+    public void testAddingtheSameAdvertisementDoesNotCallsHandler() {
+        DeviceCache cache = new DeviceCache(new Duration(1000 * 60 * 60));
+        Set<Advertisement> advs = new HashSet<>();
+        long hash = 10001;
+        assertFalse(cache.haveSeenHash(hash, "newDevice"));
+
+        Service service1 = new Service();
+        service1.setInterfaceName("randomInterface");
+        Uuid uuid1 = UUIDUtil.UUIDToUuid(UUID.randomUUID());
+        final Advertisement adv1 = new Advertisement(service1, uuid1, new EncryptionAlgorithm(0), null, false);
+        advs.add(adv1);
+
+        CountingHandler handler = new CountingHandler() {
+            @Override
+            public void handleUpdate(Advertisement advertisement) {
+                assertEquals(adv1, advertisement);
+                mNumCalls++;
+            }
+        };
+
+        cache.addScanner(new VScanner(UUIDUtil.UuidToUUID(uuid1), handler));
+
+        Service service2 = new Service();
+        service2.setInterfaceName("randomInterface2");
+        Uuid uuid2 = UUIDUtil.UUIDToUuid(UUID.randomUUID());
+        Advertisement adv2 = new Advertisement(service2, uuid2, new EncryptionAlgorithm(0), null, false);
+        advs.add(adv2);
+
+        cache.saveDevice(hash, advs, "newDevice");
+
+        Set<Advertisement> advs2 = new HashSet<>(advs);
+        cache.saveDevice(10002, advs2, "newDevice");
+
+        // Make sure that the handler is called;
+        assertEquals(1, handler.mNumCalls);
+    }
+}
diff --git a/lib/src/test/java/io/v/impl/google/lib/discovery/EncodingUtilTest.java b/lib/src/test/java/io/v/impl/google/lib/discovery/EncodingUtilTest.java
new file mode 100644
index 0000000..55a9d35
--- /dev/null
+++ b/lib/src/test/java/io/v/impl/google/lib/discovery/EncodingUtilTest.java
@@ -0,0 +1,56 @@
+// 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.impl.google.lib.discovery;
+
+import com.google.common.primitives.Bytes;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import io.v.x.ref.lib.discovery.EncryptionAlgorithm;
+import io.v.x.ref.lib.discovery.EncryptionKey;
+import io.v.x.ref.lib.discovery.testdata.PackAddressTest;
+import io.v.x.ref.lib.discovery.testdata.Constants;
+import io.v.x.ref.lib.discovery.testdata.PackEncryptionKeysTest;
+
+/**
+ * Test for {@link EncodingUtil}
+ */
+public class EncodingUtilTest extends TestCase {
+    public void testPackAddresses() throws IOException {
+        for (PackAddressTest test : Constants.PACK_ADDRESS_TEST_DATA) {
+            assertEquals(Arrays.toString(test.getPacked()),
+                    Arrays.toString(EncodingUtil.packAddresses(test.getIn())));
+        }
+    }
+
+    public void testUnpackAddresses() throws IOException {
+         for (PackAddressTest test : Constants.PACK_ADDRESS_TEST_DATA) {
+            assertEquals(test.getIn(),
+                    EncodingUtil.unpackAddresses(test.getPacked()));
+        }
+    }
+
+    public void testPackEncryptionKeys() throws IOException {
+        for (PackEncryptionKeysTest test : Constants.PACK_ENCRYPTION_KEYS_TEST_DATA) {
+            byte[] res = EncodingUtil.packEncryptionKeys(test.getAlgo().getValue(),
+                    test.getKeys());
+            assertEquals(Arrays.toString(test.getPacked()), Arrays.toString(res));
+        }
+    }
+
+    public void testUnpackEncryptionKeys() throws IOException {
+         for (PackEncryptionKeysTest test : Constants.PACK_ENCRYPTION_KEYS_TEST_DATA) {
+             EncodingUtil.KeysAndAlgorithm res = EncodingUtil.unpackEncryptionKeys(
+                     test.getPacked());
+             assertEquals(test.getAlgo(), new EncryptionAlgorithm(res.getEncryptionAlgorithm()));
+             assertEquals(test.getKeys(), res.getKeys());
+        }
+    }
+}
diff --git a/lib/src/test/java/io/v/impl/google/lib/discovery/UUIDUtilTest.java b/lib/src/test/java/io/v/impl/google/lib/discovery/UUIDUtilTest.java
new file mode 100644
index 0000000..f9e5db9
--- /dev/null
+++ b/lib/src/test/java/io/v/impl/google/lib/discovery/UUIDUtilTest.java
@@ -0,0 +1,27 @@
+// 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.impl.google.lib.discovery;
+
+import junit.framework.TestCase;
+
+import java.util.UUID;
+
+import io.v.v23.V;
+import io.v.v23.context.VContext;
+import io.v.x.ref.lib.discovery.testdata.Constants;
+import io.v.x.ref.lib.discovery.testdata.UuidTestData;
+
+/**
+ * Tests for {@link UUIDUtil}
+ */
+public class UUIDUtilTest extends TestCase {
+    public void testInterfaceNameUUID() {
+        VContext ctx = V.init();
+        for (UuidTestData test: Constants.INTERFACE_NAME_TEST) {
+            UUID id = UUIDUtil.UUIDForInterfaceName(test.getIn());
+            assertEquals(test.getWant(), id.toString());
+        }
+    }
+}
diff --git a/lib/src/test/java/io/v/impl/google/lib/discovery/ble/BleAdvertisementConverterTest.java b/lib/src/test/java/io/v/impl/google/lib/discovery/ble/BleAdvertisementConverterTest.java
new file mode 100644
index 0000000..90809d4
--- /dev/null
+++ b/lib/src/test/java/io/v/impl/google/lib/discovery/ble/BleAdvertisementConverterTest.java
@@ -0,0 +1,55 @@
+// 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.impl.google.lib.discovery.ble;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import io.v.v23.V;
+import io.v.x.ref.lib.discovery.Advertisement;
+import io.v.x.ref.lib.discovery.plugins.ble.testdata.AdvertisementConversionTestCase;
+import io.v.x.ref.lib.discovery.plugins.ble.testdata.Constants;
+
+/**
+ * Tests for {@link BleAdvertisementConverter}
+ */
+public class BleAdvertisementConverterTest extends TestCase {
+    private static Map<UUID, byte[]> convertBAdv(Map<String, byte[]> in) {
+        Map<UUID, byte[]> res = new HashMap<>(in.size());
+        for (Map.Entry<String, byte[]> entry : in.entrySet()) {
+            res.put(UUID.fromString(entry.getKey()), entry.getValue());
+        }
+        return res;
+    }
+
+    public void testConversionToBle() throws IOException {
+        // V.init() sets up the jni bindings.
+        V.init();
+        for (AdvertisementConversionTestCase test : Constants.CONVERSION_TEST_DATA) {
+            Map<UUID, byte[]> res = BleAdvertisementConverter.vAdvertismentToBleAttr(
+                    test.getVAdvertisement());
+            Map<String, byte[]> want = test.getBleAdvertisement();
+            assertEquals(want.size(), res.size());
+            for (Map.Entry<UUID, byte[]> entry : res.entrySet()) {
+                String stringKey = entry.getKey().toString();
+                assertTrue(Arrays.equals(want.get(stringKey), entry.getValue()));
+            }
+        }
+    }
+    public void testConversionFromBle() throws IOException {
+        // V.init() sets up the jni bindings.
+        V.init();
+        for (AdvertisementConversionTestCase test : Constants.CONVERSION_TEST_DATA) {
+            Map<UUID, byte[]> bleAdv = convertBAdv(test.getBleAdvertisement());
+            Advertisement res = BleAdvertisementConverter.bleAttrToVAdvertisement(bleAdv);
+            assertEquals(test.getVAdvertisement(), res);
+        }
+    }
+}
diff --git a/projects/build.gradle b/projects/build.gradle
index e69de29..571b417 100644
--- a/projects/build.gradle
+++ b/projects/build.gradle
@@ -0,0 +1,2 @@
+dependencies {
+}
\ No newline at end of file
diff --git a/projects/discovery_sample/.gitignore b/projects/discovery_sample/.gitignore
new file mode 100644
index 0000000..9c4de58
--- /dev/null
+++ b/projects/discovery_sample/.gitignore
@@ -0,0 +1,7 @@
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
diff --git a/projects/discovery_sample/app/.gitignore b/projects/discovery_sample/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/projects/discovery_sample/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/projects/discovery_sample/app/build.gradle b/projects/discovery_sample/app/build.gradle
new file mode 100644
index 0000000..02fe43b
--- /dev/null
+++ b/projects/discovery_sample/app/build.gradle
@@ -0,0 +1,73 @@
+buildscript {
+    repositories {
+        mavenCentral()
+        maven {
+            url 'https://maven.v.io'
+        }
+    }
+    dependencies {
+        // This introduces the Android plugin to make building Android
+        // applications easier.
+        classpath 'com.android.tools.build:gradle:1.3.0'
+        // We are going to define a custom VDL service. The Vanadium
+        // Gradle plugin makes that easier, so let's use that.
+        classpath 'io.v:gradle-plugin:0.1-SNAPSHOT'
+        // Use the Android SDK manager, which will automatically download
+        // the required Android SDK.
+        classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0'
+    }
+}
+
+// Make our lives easier by automatically downloading the appropriate Android
+// SDK.
+apply plugin: 'android-sdk-manager'
+// It's an Android application.
+apply plugin: 'com.android.application'
+// It's going to use VDL.
+apply plugin: 'io.v.vdl'
+
+repositories {
+    mavenCentral()
+    maven {
+        url 'https://maven.v.io/'
+    }
+}
+
+android {
+    compileSdkVersion 23
+    buildToolsVersion "23.0.1"
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_7
+        targetCompatibility JavaVersion.VERSION_1_7
+    }
+    packagingOptions {
+        exclude 'META-INF/LICENSE.txt'
+        exclude 'META-INF/NOTICE.txt'
+    }
+
+    defaultConfig {
+        applicationId "io.v.discovery_sample"
+        minSdkVersion 22
+        targetSdkVersion 22
+        versionCode 1
+        versionName "1.0"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    compile fileTree(dir: 'libs', include: ['*.jar'])
+    compile project(':lib')
+    compile project(':android-lib')
+}
+
+vdl {
+    // This is where the VDL tool will look for VDL definitions.
+    inputPaths += 'src/main/java'
+}
diff --git a/projects/discovery_sample/app/proguard-rules.pro b/projects/discovery_sample/app/proguard-rules.pro
new file mode 100644
index 0000000..75fe971
--- /dev/null
+++ b/projects/discovery_sample/app/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/local/google/home/bjornick/Android/Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
diff --git a/projects/discovery_sample/app/src/androidTest/java/io/v/discoverysample/ApplicationTest.java b/projects/discovery_sample/app/src/androidTest/java/io/v/discoverysample/ApplicationTest.java
new file mode 100644
index 0000000..05649f9
--- /dev/null
+++ b/projects/discovery_sample/app/src/androidTest/java/io/v/discoverysample/ApplicationTest.java
@@ -0,0 +1,17 @@
+// 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.discoverysample;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+    public ApplicationTest() {
+        super(Application.class);
+    }
+}
\ No newline at end of file
diff --git a/projects/discovery_sample/app/src/main/AndroidManifest.xml b/projects/discovery_sample/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..523e8c4
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.v.discoverysample" >
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
+    <uses-permission android:name="android.permission.BLUETOOTH"/>
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:theme="@style/AppTheme" >
+        <activity
+            android:name=".MainActivity"
+            android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/projects/discovery_sample/app/src/main/java/io/v/discoverysample/AttrAdapter.java b/projects/discovery_sample/app/src/main/java/io/v/discoverysample/AttrAdapter.java
new file mode 100644
index 0000000..23c0947
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/java/io/v/discoverysample/AttrAdapter.java
@@ -0,0 +1,82 @@
+// 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.discoverysample;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import org.w3c.dom.Text;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import io.v.v23.discovery.Attributes;
+
+/**
+ * Created by bjornick on 10/14/15.
+ */
+public class AttrAdapter extends BaseAdapter implements ListAdapter {
+    private class Entry implements Comparable<Entry> {
+        String key;
+        String value;
+
+        Entry(Map.Entry<String, String> entry) {
+            key = entry.getKey();
+            value = entry.getValue();
+        }
+
+        @Override
+        public int compareTo(Entry entry) {
+            return key.compareTo(entry.key);
+        }
+    }
+
+    List<Entry> entries;
+    Context ctx;;
+    LayoutInflater inflater;
+    public AttrAdapter(LayoutInflater inflater, Attributes attrs) {
+        this.inflater = inflater;
+        entries = new ArrayList<>(attrs.size());
+        for (Map.Entry<String, String> entry : attrs.entrySet()) {
+            entries.add(new Entry(entry));
+        }
+        Collections.sort(entries);
+    }
+
+    @Override
+    public int getCount() {
+        return entries.size();
+    }
+
+    @Override
+    public Object getItem(int i) {
+        return entries.get(i);
+    }
+
+    @Override
+    public long getItemId(int i) {
+        return i;
+    }
+
+    @Override
+    public View getView(int i, View view, ViewGroup viewGroup) {
+        if (view == null) {
+            view = inflater.inflate(R.layout.attribute, null);
+        }
+        Entry e = entries.get(i);
+        TextView key = (TextView) view.findViewById(R.id.key);
+        key.setText(e.key);
+        TextView value = (TextView) view.findViewById(R.id.value);
+        value.setText(e.value);
+        return view;
+    }
+}
diff --git a/projects/discovery_sample/app/src/main/java/io/v/discoverysample/MainActivity.java b/projects/discovery_sample/app/src/main/java/io/v/discoverysample/MainActivity.java
new file mode 100644
index 0000000..affb56a
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/java/io/v/discoverysample/MainActivity.java
@@ -0,0 +1,203 @@
+// 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.discoverysample;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.android.libs.discovery.ble.BlePlugin;
+import io.v.android.libs.security.BlessingsManager;
+import io.v.android.v23.V;
+import io.v.android.v23.services.blessing.BlessingCreationException;
+import io.v.android.v23.services.blessing.BlessingService;
+import io.v.impl.google.lib.discovery.UUIDUtil;
+import io.v.v23.context.CancelableVContext;
+import io.v.v23.context.VContext;
+import io.v.v23.discovery.Attributes;
+import io.v.v23.discovery.Service;
+import io.v.v23.security.Blessings;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+import io.v.x.ref.lib.discovery.Advertisement;
+import io.v.x.ref.lib.discovery.EncryptionAlgorithm;
+
+
+public class MainActivity extends Activity {
+
+    private static final int BLESSINGS_REQUEST = 1;
+    private Button chooseBlessingsButton;
+    private Button scanButton;
+    private Button advertiseButton;
+    private Blessings blessings;
+    private BlePlugin plugin;
+
+    private VContext rootCtx;
+    private CancelableVContext scanCtx;
+    private CancelableVContext advCtx;
+
+    private boolean isScanning;
+    private boolean isAdvertising;
+
+    private ScanHandlerAdapter adapter;
+
+    private Advertisement advertisement;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        rootCtx = V.init(this);
+        isScanning = false;
+        isAdvertising = false;
+
+        setContentView(R.layout.activity_main);
+        chooseBlessingsButton = (Button)findViewById(R.id.blessingsButton);
+        chooseBlessingsButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                fetchBlessings(false);
+            }
+        });
+
+        scanButton = (Button)findViewById(R.id.scanForService);
+        scanButton.setEnabled(false);
+        scanButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                flipScan();
+            }
+        });
+
+        advertiseButton = (Button) findViewById(R.id.advertiseButton);
+        advertiseButton.setEnabled(false);
+        advertiseButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                flipAdvertise();
+            }
+        });
+
+        byte[] instanceId = {0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15};
+        Attributes attrs = new Attributes();
+        attrs.put("foo", "bar");
+        List<String> addrs = new ArrayList<>();
+        addrs.add("localhost:2000");
+        String interfaceName = "v.io/x/ref.Interface";
+        advertisement = new Advertisement(
+                new Service(instanceId, "Android instance",
+                        interfaceName, attrs, addrs),
+                UUIDUtil.UUIDToUuid(UUIDUtil.UUIDForInterfaceName(interfaceName)),
+                new EncryptionAlgorithm(0),
+                null,
+                false);
+        adapter = new ScanHandlerAdapter(this);
+        ListView devices = (ListView) findViewById(R.id.list_view);
+        devices.setAdapter(adapter);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // Inflate the menu; this adds items to the action bar if it is present.
+        getMenuInflater().inflate(R.menu.menu_main, menu);
+        return true;
+    }
+
+    private void flipAdvertise() {
+        if (plugin == null) {
+            plugin = new BlePlugin(this);
+        }
+
+        if (!isAdvertising) {
+            isAdvertising = true;
+
+            advCtx = rootCtx.withCancel();
+            try {
+                plugin.addAdvertisement(advCtx, advertisement);
+                advertiseButton.setText("Stop Advertisement");
+            } catch (IOException e) {
+                advCtx.cancel();
+                isAdvertising = false;
+            }
+        } else {
+            isAdvertising = false;
+            advCtx.cancel();
+            advertiseButton.setText("Advertise");
+        }
+    }
+    private void flipScan() {
+        if (plugin == null) {
+            plugin = new BlePlugin(this);
+        }
+        if (!isScanning) {
+            scanCtx = rootCtx.withCancel();
+            plugin.addScanner(scanCtx,
+                    UUIDUtil.UUIDForInterfaceName("v.io/x/ref.Interface"),
+                    adapter);
+            isScanning = true;
+            scanButton.setText("Stop scanning");
+        } else {
+            isScanning = false;
+            scanCtx.cancel();
+            scanButton.setText("Start Scan");
+        }
+    }
+
+    private void fetchBlessings(boolean startScan) {
+        Intent intent = BlessingService.newBlessingIntent(this);
+        startActivityForResult(intent, BLESSINGS_REQUEST);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case BLESSINGS_REQUEST:
+                try {
+                    // The Account Manager will pass us the blessings to use as
+                    // an array of bytes. Use VomUtil to decode them...
+                    byte[] blessingsVom =
+                        BlessingService.extractBlessingReply(resultCode, data);
+                    blessings = (Blessings) VomUtil.decode(blessingsVom, Blessings.class);
+                    BlessingsManager.addBlessings(this, blessings);
+                    Toast.makeText(this, "Success, ready to listen!",
+                            Toast.LENGTH_SHORT).show();
+
+                    // Enable the "start service" button.
+                    scanButton.setEnabled(true);
+                    advertiseButton.setEnabled(true);
+                } catch (BlessingCreationException e) {
+                    String msg = "Couldn't create blessing: " + e.getMessage();
+                    Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
+                } catch (VException e) {
+                    String msg = "Couldn't store blessing: " + e.getMessage();
+                    Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
+                }
+                return;
+        }
+    }
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle action bar item clicks here. The action bar will
+        // automatically handle clicks on the Home/Up button, so long
+        // as you specify a parent activity in AndroidManifest.xml.
+        int id = item.getItemId();
+
+        //noinspection SimplifiableIfStatement
+        if (id == R.id.action_settings) {
+            return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+}
diff --git a/projects/discovery_sample/app/src/main/java/io/v/discoverysample/ScanHandlerAdapter.java b/projects/discovery_sample/app/src/main/java/io/v/discoverysample/ScanHandlerAdapter.java
new file mode 100644
index 0000000..c6bb1cd
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/java/io/v/discoverysample/ScanHandlerAdapter.java
@@ -0,0 +1,117 @@
+// 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.discoverysample;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.google.common.base.Joiner;
+
+import org.w3c.dom.Text;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.v.impl.google.lib.discovery.ScanHandler;
+import io.v.x.ref.lib.discovery.Advertisement;
+
+/**
+ * Created by bjornick on 10/14/15.
+ */
+public class ScanHandlerAdapter extends BaseAdapter implements ScanHandler{
+
+    List<Advertisement> knownAdvertisements;
+
+    List<DataSetObserver> observers;
+
+    LayoutInflater inflater;
+
+    Activity activity;
+
+    ScanHandlerAdapter(Activity activity) {
+        knownAdvertisements = new ArrayList<>();
+        observers = new ArrayList<>();
+        this.activity = activity;
+        inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    }
+
+    @Override
+    public int getCount() {
+        return knownAdvertisements.size();
+    }
+
+    @Override
+    public Object getItem(int i) {
+        if (i < knownAdvertisements.size()) {
+            return knownAdvertisements.get(i);
+        }
+        return null;
+    }
+
+    @Override
+    public long getItemId(int i) {
+        if (i < knownAdvertisements.size()) {
+            return knownAdvertisements.get(i).hashCode();
+        }
+        return 0;
+    }
+
+    @Override
+    public View getView(int i, View view, ViewGroup viewGroup) {
+        if (view == null) {
+            view = inflater.inflate(R.layout.item, null);
+        }
+        Advertisement adv = knownAdvertisements.get(i);
+        TextView displayName = (TextView)view.findViewById(R.id.display_name);
+        displayName.setText(adv.getService().getInstanceName());
+        TextView interfaceName = (TextView)view.findViewById(R.id.interface_name);
+        interfaceName.setText(adv.getService().getInterfaceName());
+        TextView addrs = (TextView)view.findViewById(R.id.addrs);
+        addrs.setText(Joiner.on(",").join(adv.getService().getAddrs()));
+        ListView attrs = (ListView)view.findViewById(R.id.attributes);
+        attrs.setAdapter(new AttrAdapter(inflater, adv.getService().getAttrs()));
+        return view;
+    }
+
+    @Override
+    public int getItemViewType(int i) {
+        return 0;
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        return 1;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return knownAdvertisements.isEmpty();
+    }
+
+    @Override
+    public void handleUpdate(Advertisement advertisement) {
+        if (!advertisement.getLost()) {
+            knownAdvertisements.add(advertisement);
+        } else {
+            advertisement.setLost(false);
+            knownAdvertisements.remove(advertisement);
+        }
+        activity.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                notifyDataSetChanged();
+            }
+        });
+    }
+}
diff --git a/projects/discovery_sample/app/src/main/res/layout/activity_main.xml b/projects/discovery_sample/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..67646aa
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,33 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
+    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
+
+    <Button
+        android:id="@+id/blessingsButton"
+        android:text="Get Blessings"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentTop="true"/>
+    <Button
+        android:id="@+id/scanForService"
+        android:text="Start Scan"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentTop="true"
+        android:layout_toRightOf="@id/blessingsButton"/>
+    <Button
+        android:id="@+id/advertiseButton"
+        android:text="Advertise"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentTop="true"
+        android:layout_toRightOf="@id/scanForService"/>
+    <ListView
+        android:id="@+id/list_view"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/blessingsButton"/>
+</RelativeLayout>
diff --git a/projects/discovery_sample/app/src/main/res/layout/attribute.xml b/projects/discovery_sample/app/src/main/res/layout/attribute.xml
new file mode 100644
index 0000000..933b071
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/layout/attribute.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal" android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <TextView
+        android:id="@+id/key"
+        android:layout_width="wrap_content"
+        android:layout_height="fill_parent" />
+    <TextView
+        android:id="@+id/value"
+        android:layout_width="wrap_content"
+        android:layout_height="fill_parent" />
+</LinearLayout>
\ No newline at end of file
diff --git a/projects/discovery_sample/app/src/main/res/layout/item.xml b/projects/discovery_sample/app/src/main/res/layout/item.xml
new file mode 100644
index 0000000..434fdae
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/layout/item.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <TextView
+        android:id="@+id/display_name"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content" />
+    <TextView
+        android:id="@+id/interface_name"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content" />
+    <TextView
+        android:id="@+id/addrs"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content" />
+    <ListView
+        android:id="@+id/attributes"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/projects/discovery_sample/app/src/main/res/menu/menu_main.xml b/projects/discovery_sample/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..87a750e
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,5 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
+    <item android:id="@+id/action_settings" android:title="@string/action_settings"
+        android:orderInCategory="100" android:showAsAction="never" />
+</menu>
diff --git a/projects/discovery_sample/app/src/main/res/mipmap-hdpi/ic_launcher.png b/projects/discovery_sample/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/projects/discovery_sample/app/src/main/res/mipmap-mdpi/ic_launcher.png b/projects/discovery_sample/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/projects/discovery_sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/projects/discovery_sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/projects/discovery_sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/projects/discovery_sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/projects/discovery_sample/app/src/main/res/values-v21/styles.xml b/projects/discovery_sample/app/src/main/res/values-v21/styles.xml
new file mode 100644
index 0000000..dba3c41
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/values-v21/styles.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="AppTheme" parent="android:Theme.Material.Light">
+    </style>
+</resources>
diff --git a/projects/discovery_sample/app/src/main/res/values-w820dp/dimens.xml b/projects/discovery_sample/app/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+    <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+         (such as screen margins) for screens with more than 820dp of available width. This
+         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+    <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/projects/discovery_sample/app/src/main/res/values/dimens.xml b/projects/discovery_sample/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..47c8224
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/projects/discovery_sample/app/src/main/res/values/strings.xml b/projects/discovery_sample/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..78a6bfc
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/values/strings.xml
@@ -0,0 +1,6 @@
+<resources>
+    <string name="app_name">DiscoverySample</string>
+
+    <string name="hello_world">Hello world!</string>
+    <string name="action_settings">Settings</string>
+</resources>
diff --git a/projects/discovery_sample/app/src/main/res/values/styles.xml b/projects/discovery_sample/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..ff6c9d2
--- /dev/null
+++ b/projects/discovery_sample/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+    </style>
+
+</resources>
diff --git a/projects/discovery_sample/build.gradle b/projects/discovery_sample/build.gradle
new file mode 100644
index 0000000..1b7886d
--- /dev/null
+++ b/projects/discovery_sample/build.gradle
@@ -0,0 +1,19 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:1.3.0'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+    }
+}
diff --git a/projects/discovery_sample/gradle.properties b/projects/discovery_sample/gradle.properties
new file mode 100644
index 0000000..1d3591c
--- /dev/null
+++ b/projects/discovery_sample/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
\ No newline at end of file
diff --git a/projects/discovery_sample/gradle/wrapper/gradle-wrapper.jar b/projects/discovery_sample/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
--- /dev/null
+++ b/projects/discovery_sample/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/projects/discovery_sample/gradle/wrapper/gradle-wrapper.properties b/projects/discovery_sample/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..0c71e76
--- /dev/null
+++ b/projects/discovery_sample/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
diff --git a/projects/discovery_sample/gradlew b/projects/discovery_sample/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/projects/discovery_sample/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/projects/discovery_sample/gradlew.bat b/projects/discovery_sample/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/projects/discovery_sample/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windowz variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+if "%@eval[2+2]" == "4" goto 4NT_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+goto execute

+

+:4NT_args

+@rem Get arguments from the 4NT Shell from JP Software

+set CMD_LINE_ARGS=%$

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/projects/discovery_sample/settings.gradle b/projects/discovery_sample/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/projects/discovery_sample/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/settings.gradle b/settings.gradle
index d9d2af5..f626ccc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include 'lib', 'tests', 'android-lib', 'benchmarks:syncbench', 'projects:syncslides', 'projects:syncslides:app', 'projects:syncslidepresenter'
+include 'lib', 'tests', 'android-lib', 'benchmarks:syncbench', 'projects:syncslides', 'projects:syncslides:app', 'projects:syncslidepresenter', 'projects:discovery_sample', 'projects:discovery_sample:app'