blob: 1a8a74181426a2a74f67c04f45c81f8260cf283a [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.android.impl.google.discovery.plugins.ble;
import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattCharacteristic;
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.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.ParcelUuid;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import io.v.android.v23.V;
import io.v.v23.context.VContext;
/**
* A BLE Driver for Android.
*
* This Driver also support discovery over Bluetooth classic by
* - Each peripheral makes the device discoverable for a specified duration.
* - A central device discovers near-by devices through Bluetooth classic,
* tries to connect and check each device whether the device has any
* services that the central is looking for. A central device will fetch
* services through Gatt over BR/EDR.
*/
public class Driver implements BluetoothScanner.Handler, GattReader.Handler {
static final String TAG = "BleDriver";
/**
* An interface for passing scanned advertisements.
*/
public interface ScanHandler {
/**
* Called with each discovery update.
*/
void onDiscovered(String uuid, Map<String, byte[]> characteristics, int rssi);
}
private final Context mContext;
private final BluetoothAdapter mBluetoothAdapter;
private BluetoothAdvertiser mClassicAdvertiser;
private static int sClassicDiscoverableDurationInSec;
private BluetoothLeAdvertiser mLeAdvertiser;
private Map<UUID, AdvertiseCallback> mLeAdvertiseCallbacks;
private GattServer mGattServer;
private final Map<String, BluetoothGattService> mServices;
private BluetoothScanner mClassicScanner;
private static boolean sClassicScanEnabled;
private BluetoothLeScanner mLeScanner;
private ScanCallback mLeScanCallback;
private GattReader mGattReader;
private Set<UUID> mScanUuids;
private ParcelUuid mScanBaseUuid, mScanMaskUuid;
private ScanHandler mScanHandler;
private Map<BluetoothDevice, Integer> mScanSeens;
private boolean mEnabled;
private int mOnServiceReadCallbacks;
private final class BluetoothAdapterStatusReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!intent.getAction().equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
return;
}
switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
case BluetoothAdapter.STATE_ON:
resume();
break;
case BluetoothAdapter.STATE_OFF:
pause();
break;
}
}
}
/**
* Create a new BLE driver for Android.
*
* @param vContext Vanadium context.
*/
public Driver(VContext vContext) {
mContext = V.getAndroidContext(vContext);
if (mContext == null) {
throw new IllegalStateException("AndroidContext not available");
}
mServices = new HashMap<>();
BluetoothManager manager =
((BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE));
mBluetoothAdapter = manager.getAdapter();
if (mBluetoothAdapter == null) {
Log.w(TAG, "BluetoothAdapter not available");
return;
}
if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(
mContext, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
Log.w(
TAG,
"ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION not granted, "
+ "Bluetooth discovery will not be happening");
return;
}
mContext.registerReceiver(
new BluetoothAdapterStatusReceiver(),
new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
if (!mBluetoothAdapter.isEnabled()) {
// Prompt user to turn on Bluetooth.
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
enableBtIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(enableBtIntent);
return;
}
resume();
}
private synchronized void resume() {
if (mEnabled) {
return;
}
if (!mBluetoothAdapter.isEnabled()) {
return;
}
mEnabled = true;
resumeAdvertising();
resumeScanning();
Log.i(TAG, "started");
}
private synchronized void pause() {
if (!mEnabled) {
return;
}
mEnabled = false;
pauseAdvertising();
pauseScanning();
Log.i(TAG, "stopped");
}
public void addService(final String uuid, Map<String, byte[]> characteristics) {
BluetoothGattService service =
new BluetoothGattService(
UUID.fromString(uuid), BluetoothGattService.SERVICE_TYPE_PRIMARY);
for (Map.Entry<String, byte[]> entry : characteristics.entrySet()) {
BluetoothGattCharacteristic characteristic =
new BluetoothGattCharacteristic(
UUID.fromString(entry.getKey()),
BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_READ);
characteristic.setValue(entry.getValue());
service.addCharacteristic(characteristic);
}
synchronized (this) {
if (mServices.put(uuid, service) != null) {
throw new IllegalStateException("already being advertised: " + uuid);
}
if (mEnabled) {
startAdvertising(service);
}
}
}
public synchronized void removeService(String uuid) {
BluetoothGattService service = mServices.remove(uuid);
if (service == null) {
return;
}
if (mEnabled) {
stopAdvertising(service);
}
}
private synchronized void startAdvertising(BluetoothGattService service) {
mGattServer.addService(service);
synchronized (Driver.class) {
mClassicAdvertiser.addService(service.getUuid(), sClassicDiscoverableDurationInSec);
sClassicDiscoverableDurationInSec = 0;
}
if (mLeAdvertiser != null) {
final UUID uuid = service.getUuid();
AdvertiseSettings settings =
new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
.setConnectable(true)
.build();
AdvertiseData data =
new AdvertiseData.Builder()
.addServiceUuid(new ParcelUuid(uuid))
.setIncludeTxPowerLevel(true)
.build();
AdvertiseCallback callback =
new AdvertiseCallback() {
@Override
public void onStartFailure(int errorCode) {
Log.e(
TAG,
"startAdvertising failed: "
+ uuid
+ ", errorCode:"
+ errorCode);
}
};
// TODO(jhahn): The maximum number of simultaneous advertisements is limited by the chipset.
// Rotate active advertisements periodically if the total number of advertisement exceeds
// the limit.
mLeAdvertiser.startAdvertising(settings, data, callback);
mLeAdvertiseCallbacks.put(uuid, callback);
}
}
private synchronized void stopAdvertising(BluetoothGattService service) {
mGattServer.removeService(service);
mClassicAdvertiser.removeService(service.getUuid());
if (mLeAdvertiser != null) {
AdvertiseCallback callback = mLeAdvertiseCallbacks.remove(service.getUuid());
mLeAdvertiser.stopAdvertising(callback);
}
}
private synchronized void resumeAdvertising() {
mGattServer = new GattServer(mContext);
mClassicAdvertiser = new BluetoothAdvertiser(mContext, mBluetoothAdapter);
if (mBluetoothAdapter.isMultipleAdvertisementSupported()) {
mLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
mLeAdvertiseCallbacks = new HashMap<>();
}
for (BluetoothGattService service : mServices.values()) {
startAdvertising(service);
}
}
private synchronized void pauseAdvertising() {
mGattServer.close();
mGattServer = null;
mClassicAdvertiser.close();
mClassicAdvertiser = null;
// mLeAdvertiser is invalidated when BluetoothAdapter is turned off.
// We don't need to stop any active advertising.
mLeAdvertiser = null;
mLeAdvertiseCallbacks = null;
}
public synchronized void startScan(
String[] uuids, String baseUuid, String maskUuid, ScanHandler handler) {
if (mScanHandler != null) {
throw new IllegalStateException("scan already started");
}
ImmutableSet.Builder<UUID> builder = ImmutableSet.builder();
if (uuids != null) {
for (String uuid : uuids) {
builder.add(UUID.fromString(uuid));
}
}
mScanUuids = builder.build();
mScanBaseUuid = ParcelUuid.fromString(baseUuid);
mScanMaskUuid = ParcelUuid.fromString(maskUuid);
mScanHandler = handler;
if (mEnabled) {
startScanning();
}
}
public synchronized void stopScan() {
if (mScanHandler == null) {
return;
}
if (mEnabled) {
stopScanning();
}
mScanUuids = null;
mScanBaseUuid = null;
mScanMaskUuid = null;
mScanHandler = null;
}
private synchronized void startScanning() {
mScanSeens = new HashMap<>();
mGattReader = new GattReader(mContext, mScanUuids, this);
synchronized (Driver.class) {
if (sClassicScanEnabled) {
// Note that BluetoothLeScan will be started when BluetoothScan finishes.
mClassicScanner.startScan(mScanUuids);
sClassicScanEnabled = false;
} else {
startBluetoothLeScanner();
}
}
}
private synchronized void startBluetoothLeScanner() {
if (mLeScanner == null) {
return;
}
ImmutableList.Builder<ScanFilter> builder = new ImmutableList.Builder();
for (UUID uuid : mScanUuids) {
builder.add(new ScanFilter.Builder().setServiceUuid(new ParcelUuid(uuid)).build());
}
List<ScanFilter> filters = builder.build();
ScanSettings settings =
new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_BALANCED).build();
// ScanFilter doesn't work with startScan() if there are too many - more than 63bits - ignore
// bits. So we call startScan() without a scan filter for base/mask uuids and match scan results
// against it.
final ScanFilter matcher =
new ScanFilter.Builder().setServiceUuid(mScanBaseUuid, mScanMaskUuid).build();
mLeScanCallback =
new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
// This callback will never be called with this callback type, since the
// scan setting is for CALLBACK_TYPE_ALL_MATCHES. But just for safety.
return;
}
if (!matcher.matches(result)) {
return;
}
BluetoothDevice device = result.getDevice();
synchronized (Driver.this) {
if (mScanSeens != null
&& mScanSeens.put(device, result.getRssi()) == null) {
mGattReader.readDevice(device);
}
}
}
@Override
public void onScanFailed(int errorCode) {
Log.e(TAG, "startScan failed: " + errorCode);
}
};
mLeScanner.startScan(filters, settings, mLeScanCallback);
}
private synchronized void stopScanning() {
mClassicScanner.stopScan();
if (mLeScanCallback != null) {
mLeScanner.stopScan(mLeScanCallback);
mLeScanCallback = null;
}
mGattReader.close();
mGattReader = null;
mScanSeens = null;
}
private synchronized void resumeScanning() {
mClassicScanner = new BluetoothScanner(mContext, mBluetoothAdapter, this);
mLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
if (mScanHandler != null) {
startScanning();
}
}
private synchronized void pauseScanning() {
mClassicScanner.close();
mClassicScanner = null;
if (mScanHandler != null) {
mGattReader.close();
mGattReader = null;
// mLeScanner is invalidated when BluetoothAdapter is turned off.
// We don't need to stop any active scan.
mLeScanner = null;
mLeScanCallback = null;
mScanSeens = null;
}
}
public synchronized void onBluetoothDiscoveryFinished(Map<BluetoothDevice, Integer> found) {
if (mScanSeens == null) {
return;
}
// Start to read services through Gatt.
//
// TODO(jhahn): Do we need to retry when Gatt read fails?
for (Map.Entry<BluetoothDevice, Integer> e : found.entrySet()) {
mScanSeens.put(e.getKey(), e.getValue());
mGattReader.readDevice(e.getKey());
}
// Now start BluetoothLeScan.
startBluetoothLeScanner();
}
public void onGattRead(BluetoothDevice device, BluetoothGattService service) {
Map<String, byte[]> characteristics;
ImmutableMap.Builder<String, byte[]> builder = new ImmutableMap.Builder();
for (BluetoothGattCharacteristic c : service.getCharacteristics()) {
builder.put(c.getUuid().toString(), c.getValue());
}
characteristics = builder.build();
synchronized (this) {
if (mScanSeens == null) {
return;
}
Integer rssi = mScanSeens.get(device);
if (rssi == null) {
return;
}
mScanHandler.onDiscovered(service.getUuid().toString(), characteristics, rssi);
mOnServiceReadCallbacks++;
}
}
public synchronized void onGattReadFailed(BluetoothDevice device) {
if (mScanSeens == null) {
return;
}
// Remove the seen record to retry to read the device.
mScanSeens.remove(device);
}
/**
* Set the Duration of Bluetooth discoverability in seconds. This will be applied for
* the next addService() only one time.
*
* TODO(jhahn): Find a better API to set Bluetooth discovery options.
*/
public static synchronized void setBluetoothDiscoverableDuration(int durationInSec) {
sClassicDiscoverableDurationInSec = durationInSec;
}
/**
* Enable Bluetooth scan. This will be applied for the next startScan() only one time.
*
* TODO(jhahn): Find a better API to set Bluetooth discovery options.
*/
public static synchronized void setBluetoothScanEnabled(boolean enabled) {
sClassicScanEnabled = enabled;
}
public synchronized String debugString() {
if (mBluetoothAdapter == null) {
return "Not available";
}
StringBuilder b = new StringBuilder().append("BluetoothAdapter: ");
switch (mBluetoothAdapter.getState()) {
case BluetoothAdapter.STATE_ON:
b.append("ON");
break;
case BluetoothAdapter.STATE_TURNING_ON:
b.append("Turning on");
break;
case BluetoothAdapter.STATE_OFF:
b.append("OFF");
break;
case BluetoothAdapter.STATE_TURNING_OFF:
b.append("Turning off");
break;
default:
b.append("Unknown state");
break;
}
b.append("\n");
b.append("ENABLED: ").append(mEnabled).append("\n");
if (mServices.size() > 0) {
b.append("ADVERTISING ").append(mServices.size()).append(" services\n");
}
if (mLeScanCallback != null) {
b.append("SCANNING\n");
}
b.append("OnServiceReadCallbacks: ").append(mOnServiceReadCallbacks).append("\n");
return b.toString();
}
}