blob: ddff8603f71fc315b5f4fedc15daf79acadb6107 [file] [log] [blame]
// 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);
}
}