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'