discovery: android ble driver
* Updated GattServer/Reader to handle Gatt over BR/EDR so that
we can do Gatt operations while having Bluetooth socket.
* Updated GattReader to read all requested services in one Gatt
connection instead of having a separate Gatt connection per service.
* Added Bluetooth classic discovery
- This doesn't work well yet.
Change-Id: I18e96fe45acfd914925c918f847cb73b37200561
diff --git a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/BluetoothAdvertiser.java b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/BluetoothAdvertiser.java
new file mode 100644
index 0000000..4ec1b24
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/BluetoothAdvertiser.java
@@ -0,0 +1,141 @@
+// 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.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * An advertiser that broadcasts Vanadium services through Bluetooth.
+ */
+class BluetoothAdvertiser {
+ private static final String TAG = Driver.TAG;
+
+ private final Context mContext;
+
+ private final BluetoothAdapter mBluetoothAdapter;
+ private RfcommListener mRfcommListener;
+
+ private final Set<UUID> mServices;
+
+ private class RfcommListener extends Thread {
+ private BluetoothServerSocket mServerSocket;
+
+ public void run() {
+ try {
+ mServerSocket =
+ mBluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord(
+ Constants.SDP_NAME, Constants.SDP_UUID);
+ } catch (IOException e) {
+ Log.e(TAG, "rfcomm listen failed", e);
+ return;
+ }
+
+ for (; ; ) {
+ try (BluetoothSocket socket = mServerSocket.accept();
+ ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
+ ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream())) {
+ Set<UUID> uuids = (Set<UUID>) in.readObject();
+
+ boolean found;
+ synchronized (BluetoothAdvertiser.this) {
+ if (uuids.isEmpty()) {
+ found = !mServices.isEmpty();
+ } else {
+ found = !Collections.disjoint(mServices, uuids);
+ }
+ }
+
+ out.writeBoolean(found);
+ out.flush();
+ } catch (Exception e) {
+ Log.e(TAG, "rfcomm accept failed", e);
+ break;
+ }
+ }
+ }
+
+ void close() {
+ if (mServerSocket == null) {
+ return;
+ }
+
+ try {
+ mServerSocket.close();
+ } catch (IOException e) {
+ Log.e(TAG, "close failed", e);
+ }
+ }
+ }
+
+ BluetoothAdvertiser(Context context, BluetoothAdapter bluetoothAdapter) {
+ mContext = context;
+ mBluetoothAdapter = bluetoothAdapter;
+ mServices = new HashSet<>();
+ }
+
+ /**
+ * Add a service to the Bluetooth advertiser.
+ */
+ synchronized void addService(UUID uuid, int discoverableDurationInSec) {
+ mServices.add(uuid);
+
+ if (discoverableDurationInSec <= 0) {
+ return;
+ }
+
+ if (mRfcommListener == null) {
+ mRfcommListener = new RfcommListener();
+ mRfcommListener.start();
+ }
+
+ // Make it discoverable if not already in discoverable mode.
+ if (mBluetoothAdapter.getScanMode()
+ == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+ Log.w(TAG, "already in discoverable mode");
+ return;
+ }
+
+ Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
+ discoverableIntent.putExtra(
+ BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, discoverableDurationInSec);
+ discoverableIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(discoverableIntent);
+ }
+
+ /**
+ * Removes a service from the Bluetooth advertiser.
+ */
+ synchronized void removeService(UUID uuid) {
+ mServices.remove(uuid);
+ if (mServices.isEmpty() && mRfcommListener != null) {
+ mRfcommListener.close();
+ mRfcommListener = null;
+ }
+ }
+
+ /**
+ * Closes the BT advertiser.
+ */
+ synchronized void close() {
+ mServices.clear();
+ if (mRfcommListener != null) {
+ mRfcommListener.close();
+ mRfcommListener = null;
+ }
+ }
+}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/BluetoothScanner.java b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/BluetoothScanner.java
new file mode 100644
index 0000000..764738c
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/BluetoothScanner.java
@@ -0,0 +1,156 @@
+// 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.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * An advertiser that broadcasts Vanadium services through Bluetooth.
+ */
+class BluetoothScanner {
+ private static final String TAG = Driver.TAG;
+
+ // A handler that will get called when a GATT service is read.
+ interface Handler {
+ void onBluetoothDiscoveryFinished(Map<BluetoothDevice, Integer> found);
+ }
+
+ private final Context mContext;
+
+ private final BluetoothAdapter mBluetoothAdapter;
+ private BluetoothScanReceiver mBluetoothScanReceiver;
+
+ private Set<UUID> mScanUuids;
+ private Handler mHandler;
+
+ private final class BluetoothScanReceiver extends BroadcastReceiver {
+ // A map of devices that have been discovered with RSSI.
+ private final Map<BluetoothDevice, Integer> mScanSeens = new HashMap<>();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // We try to connect each discovered device once discovery finishes,
+ // since discovery will slow down connections and unstable.
+ switch (intent.getAction()) {
+ case BluetoothDevice.ACTION_FOUND:
+ BluetoothDevice device =
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (device.getType() == BluetoothDevice.DEVICE_TYPE_CLASSIC) {
+ mScanSeens.put(
+ device,
+ (int) intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, (short) 0));
+ }
+ break;
+ case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
+ // Note that discovery can be finished when bluetooth is turned off.
+ if (mBluetoothAdapter.isEnabled()) {
+ new RfcommConnector(mScanSeens).start();
+ }
+ break;
+ }
+ }
+ }
+
+ private class RfcommConnector extends Thread {
+ private final Map<BluetoothDevice, Integer> mScanSeens;
+
+ RfcommConnector(Map<BluetoothDevice, Integer> scanSeens) {
+ mScanSeens = scanSeens;
+ }
+
+ public void run() {
+ Map<BluetoothDevice, Integer> found = new HashMap<>();
+
+ for (Map.Entry<BluetoothDevice, Integer> seen : mScanSeens.entrySet()) {
+ BluetoothDevice device = seen.getKey();
+
+ try (BluetoothSocket socket =
+ device.createInsecureRfcommSocketToServiceRecord(Constants.SDP_UUID)) {
+ socket.connect();
+
+ try (ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
+ ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
+ synchronized (BluetoothScanner.this) {
+ if (mScanUuids == null) {
+ // Scan already finished.
+ return;
+ }
+ out.writeObject(mScanUuids);
+ }
+ out.flush();
+
+ if (in.readBoolean()) {
+ found.put(device, seen.getValue());
+ }
+ }
+ } catch (Exception e) {
+ }
+ }
+
+ synchronized (BluetoothScanner.this) {
+ if (mHandler != null) {
+ mHandler.onBluetoothDiscoveryFinished(found);
+ }
+ }
+ }
+ }
+
+ BluetoothScanner(Context context, BluetoothAdapter bluetoothAdapter, Handler handler) {
+ mContext = context;
+ mBluetoothAdapter = bluetoothAdapter;
+ mHandler = handler;
+ }
+
+ synchronized void startScan(Set<UUID> uuids) {
+ mScanUuids = uuids;
+
+ if (mBluetoothAdapter.isDiscovering()) {
+ mBluetoothAdapter.cancelDiscovery();
+ }
+
+ mBluetoothScanReceiver = new BluetoothScanReceiver();
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
+ intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
+ mContext.registerReceiver(mBluetoothScanReceiver, intentFilter);
+
+ if (!mBluetoothAdapter.startDiscovery()) {
+ mHandler.onBluetoothDiscoveryFinished(Collections.<BluetoothDevice, Integer>emptyMap());
+ }
+ }
+
+ synchronized void stopScan() {
+ if (mScanUuids == null) {
+ return;
+ }
+
+ if (mBluetoothAdapter.isDiscovering()) {
+ mBluetoothAdapter.cancelDiscovery();
+ }
+
+ mContext.unregisterReceiver(mBluetoothScanReceiver);
+ mBluetoothScanReceiver = null;
+ mScanUuids = null;
+ }
+
+ synchronized void close() {
+ stopScan();
+ mHandler = null;
+ }
+}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/Constants.java b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/Constants.java
new file mode 100644
index 0000000..cba1b8e
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/Constants.java
@@ -0,0 +1,16 @@
+// 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 java.util.UUID;
+
+/**
+ * Constants for use in the Bluetooth Advertisements.
+ */
+class Constants {
+ // Name and UUID for SDP record.
+ static final String SDP_NAME = "v23";
+ static final UUID SDP_UUID = UUID.fromString("62e59f86-22b8-572b-82bf-2ee0ea877259");
+}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/Driver.java b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/Driver.java
index 83073b4..1a8a741 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/Driver.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/Driver.java
@@ -4,6 +4,7 @@
package io.v.android.impl.google.discovery.plugins.ble;
+import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattCharacteristic;
@@ -16,23 +17,26 @@
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanRecord;
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 android.util.Pair;
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;
@@ -40,8 +44,15 @@
/**
* 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 GattReader.Handler {
+public class Driver implements BluetoothScanner.Handler, GattReader.Handler {
static final String TAG = "BleDriver";
/**
@@ -54,24 +65,28 @@
void onDiscovered(String uuid, Map<String, byte[]> characteristics, int rssi);
}
- private Context mContext;
+ private final Context mContext;
private final BluetoothAdapter mBluetoothAdapter;
- private BluetoothLeAdvertiser mAdvertiser;
- private Map<UUID, AdvertiseCallback> mAdvertiseCallbacks;
+ private BluetoothAdvertiser mClassicAdvertiser;
+ private static int sClassicDiscoverableDurationInSec;
+ private BluetoothLeAdvertiser mLeAdvertiser;
+ private Map<UUID, AdvertiseCallback> mLeAdvertiseCallbacks;
private GattServer mGattServer;
- private final Map<String, BluetoothGattService> mGattServices;
+ private final Map<String, BluetoothGattService> mServices;
- private BluetoothLeScanner mScanner;
- private ScanCallback mScanCallback;
+ private BluetoothScanner mClassicScanner;
+ private static boolean sClassicScanEnabled;
+ private BluetoothLeScanner mLeScanner;
+ private ScanCallback mLeScanCallback;
private GattReader mGattReader;
- private String[] mScanUuids;
- private String mScanBaseUuid, mScanMaskUuid;
+ private Set<UUID> mScanUuids;
+ private ParcelUuid mScanBaseUuid, mScanMaskUuid;
private ScanHandler mScanHandler;
- private Map<Pair<BluetoothDevice, UUID>, Integer> mScanSeens;
+ private Map<BluetoothDevice, Integer> mScanSeens;
private boolean mEnabled;
private int mOnServiceReadCallbacks;
@@ -103,7 +118,7 @@
if (mContext == null) {
throw new IllegalStateException("AndroidContext not available");
}
- mGattServices = new HashMap<>();
+ mServices = new HashMap<>();
BluetoothManager manager =
((BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE));
@@ -113,6 +128,18 @@
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));
@@ -135,8 +162,8 @@
}
mEnabled = true;
- resumeAdvertisingSynchronized();
- resumeScanningSynchronized();
+ resumeAdvertising();
+ resumeScanning();
Log.i(TAG, "started");
}
@@ -147,8 +174,8 @@
}
mEnabled = false;
- pauseAdvertisingSynchronized();
- pauseScanningSynchronized();
+ pauseAdvertising();
+ pauseScanning();
Log.i(TAG, "stopped");
}
@@ -168,84 +195,95 @@
}
synchronized (this) {
- if (mGattServices.put(uuid, service) != null) {
+ if (mServices.put(uuid, service) != null) {
throw new IllegalStateException("already being advertised: " + uuid);
}
- if (mEnabled && mBluetoothAdapter.isMultipleAdvertisementSupported()) {
- startAdvertisingSynchronized(service);
+ if (mEnabled) {
+ startAdvertising(service);
}
}
}
public synchronized void removeService(String uuid) {
- BluetoothGattService service = mGattServices.remove(uuid);
+ BluetoothGattService service = mServices.remove(uuid);
if (service == null) {
return;
}
- if (mEnabled && mBluetoothAdapter.isMultipleAdvertisementSupported()) {
- stopAdvertisingSynchronized(service);
+ if (mEnabled) {
+ stopAdvertising(service);
}
}
- private void startAdvertisingSynchronized(BluetoothGattService service) {
+ private synchronized void startAdvertising(BluetoothGattService service) {
mGattServer.addService(service);
-
- 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.
- mAdvertiser.startAdvertising(settings, data, callback);
- mAdvertiseCallbacks.put(uuid, callback);
+ 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 void stopAdvertisingSynchronized(BluetoothGattService service) {
+ private synchronized void stopAdvertising(BluetoothGattService service) {
mGattServer.removeService(service);
-
- AdvertiseCallback callback = mAdvertiseCallbacks.remove(service.getUuid());
- mAdvertiser.stopAdvertising(callback);
+ mClassicAdvertiser.removeService(service.getUuid());
+ if (mLeAdvertiser != null) {
+ AdvertiseCallback callback = mLeAdvertiseCallbacks.remove(service.getUuid());
+ mLeAdvertiser.stopAdvertising(callback);
+ }
}
- private void resumeAdvertisingSynchronized() {
- if (!mBluetoothAdapter.isMultipleAdvertisementSupported()) {
- Log.w(TAG, "advertisement is not supported by this device");
- return;
- }
-
- mAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
- mAdvertiseCallbacks = new HashMap<>();
+ private synchronized void resumeAdvertising() {
mGattServer = new GattServer(mContext);
- for (BluetoothGattService service : mGattServices.values()) {
- startAdvertisingSynchronized(service);
+ mClassicAdvertiser = new BluetoothAdvertiser(mContext, mBluetoothAdapter);
+ if (mBluetoothAdapter.isMultipleAdvertisementSupported()) {
+ mLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
+ mLeAdvertiseCallbacks = new HashMap<>();
+ }
+
+ for (BluetoothGattService service : mServices.values()) {
+ startAdvertising(service);
}
}
- private void pauseAdvertisingSynchronized() {
- if (mGattServer != null) {
- mGattServer.close();
- mGattServer = null;
- }
+ private synchronized void pauseAdvertising() {
+ mGattServer.close();
+ mGattServer = null;
+ mClassicAdvertiser.close();
+ mClassicAdvertiser = null;
- // mAdvertiser is invalidated when BluetoothAdapter is turned off.
+ // mLeAdvertiser is invalidated when BluetoothAdapter is turned off.
// We don't need to stop any active advertising.
- mAdvertiser = null;
- mAdvertiseCallbacks = null;
+ mLeAdvertiser = null;
+ mLeAdvertiseCallbacks = null;
}
public synchronized void startScan(
@@ -254,12 +292,18 @@
throw new IllegalStateException("scan already started");
}
- mScanUuids = uuids;
- mScanBaseUuid = baseUuid;
- mScanMaskUuid = maskUuid;
+ 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) {
- startScanningSynchronized();
+ startScanning();
}
}
@@ -268,30 +312,40 @@
return;
}
+ if (mEnabled) {
+ stopScanning();
+ }
mScanUuids = null;
mScanBaseUuid = null;
mScanMaskUuid = null;
mScanHandler = null;
- if (mEnabled) {
- stopScanningSynchronized();
+ }
+
+ 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 void startScanningSynchronized() {
- mGattReader = new GattReader(mContext, this);
- mScanSeens = new HashMap();
-
- List<ScanFilter> filters = null;
- if (mScanUuids != null) {
- ImmutableList.Builder<ScanFilter> builder = new ImmutableList.Builder();
- for (String uuid : mScanUuids) {
- builder.add(
- new ScanFilter.Builder()
- .setServiceUuid(ParcelUuid.fromString(uuid))
- .build());
- }
- filters = builder.build();
+ 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();
@@ -299,13 +353,9 @@
// 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(
- ParcelUuid.fromString(mScanBaseUuid),
- ParcelUuid.fromString(mScanMaskUuid))
- .build();
+ new ScanFilter.Builder().setServiceUuid(mScanBaseUuid, mScanMaskUuid).build();
- mScanCallback =
+ mLeScanCallback =
new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
@@ -317,16 +367,11 @@
if (!matcher.matches(result)) {
return;
}
- ScanRecord scanRecord = result.getScanRecord();
- if (scanRecord.getServiceUuids().size() != 1) {
- // This shouldn't happen since we advertise only one uuid in each advertisement.
- return;
- }
- UUID uuid = scanRecord.getServiceUuids().get(0).getUuid();
- Pair<BluetoothDevice, UUID> seen = Pair.create(result.getDevice(), uuid);
+ BluetoothDevice device = result.getDevice();
synchronized (Driver.this) {
- if (mEnabled && mScanSeens.put(seen, result.getRssi()) == null) {
- mGattReader.readService(result.getDevice(), uuid);
+ if (mScanSeens != null
+ && mScanSeens.put(device, result.getRssi()) == null) {
+ mGattReader.readDevice(device);
}
}
}
@@ -336,39 +381,63 @@
Log.e(TAG, "startScan failed: " + errorCode);
}
};
- mScanner.startScan(filters, settings, mScanCallback);
+
+ mLeScanner.startScan(filters, settings, mLeScanCallback);
}
- private void stopScanningSynchronized() {
- mScanner.stopScan(mScanCallback);
- mScanCallback = null;
- mScanSeens = null;
-
+ private synchronized void stopScanning() {
+ mClassicScanner.stopScan();
+ if (mLeScanCallback != null) {
+ mLeScanner.stopScan(mLeScanCallback);
+ mLeScanCallback = null;
+ }
mGattReader.close();
mGattReader = null;
+ mScanSeens = null;
}
- private void resumeScanningSynchronized() {
- mScanner = mBluetoothAdapter.getBluetoothLeScanner();
+ private synchronized void resumeScanning() {
+ mClassicScanner = new BluetoothScanner(mContext, mBluetoothAdapter, this);
+ mLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
if (mScanHandler != null) {
- startScanningSynchronized();
+ startScanning();
}
}
- private void pauseScanningSynchronized() {
+ private synchronized void pauseScanning() {
+ mClassicScanner.close();
+ mClassicScanner = null;
if (mScanHandler != null) {
mGattReader.close();
mGattReader = null;
- // mScanner is invalidated when BluetoothAdapter is turned off.
+ // mLeScanner is invalidated when BluetoothAdapter is turned off.
// We don't need to stop any active scan.
- mScanner = null;
- mScanCallback = null;
+ mLeScanner = null;
+ mLeScanCallback = null;
mScanSeens = null;
}
}
- public void onServiceRead(BluetoothDevice device, BluetoothGattService service) {
+ 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()) {
@@ -377,22 +446,44 @@
characteristics = builder.build();
synchronized (this) {
- mOnServiceReadCallbacks++;
- if (mScanHandler == null) {
+ if (mScanSeens == null) {
return;
}
- Integer rssi = mScanSeens.get(Pair.create(device, service.getUuid()));
+ Integer rssi = mScanSeens.get(device);
if (rssi == null) {
return;
}
- mScanHandler.onDiscovered(
- service.getUuid().toString(), characteristics, rssi);
+ mScanHandler.onDiscovered(service.getUuid().toString(), characteristics, rssi);
+ mOnServiceReadCallbacks++;
}
}
- public synchronized void onServiceReadFailed(BluetoothDevice device, UUID uuid) {
- // Remove the seen record to retry to read the service.
- mScanSeens.remove(Pair.create(device, uuid));
+ 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() {
@@ -419,10 +510,10 @@
}
b.append("\n");
b.append("ENABLED: ").append(mEnabled).append("\n");
- if (mGattServices.size() > 0) {
- b.append("ADVERTISING ").append(mGattServices.size()).append(" services\n");
+ if (mServices.size() > 0) {
+ b.append("ADVERTISING ").append(mServices.size()).append(" services\n");
}
- if (mScanCallback != null) {
+ if (mLeScanCallback != null) {
b.append("SCANNING\n");
}
b.append("OnServiceReadCallbacks: ").append(mOnServiceReadCallbacks).append("\n");
diff --git a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattReader.java b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattReader.java
index 800ba35..0099475 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattReader.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattReader.java
@@ -11,17 +11,16 @@
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import android.util.Log;
-import android.util.Pair;
import com.google.common.collect.Queues;
import java.util.ArrayDeque;
import java.util.Iterator;
-import java.util.Timer;
-import java.util.TimerTask;
+import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
/**
* A reader that reads Vanadium Gatt services from remote Gatt servers.
@@ -31,9 +30,9 @@
// A handler that will get called when a GATT service is read.
interface Handler {
- void onServiceRead(BluetoothDevice device, BluetoothGattService service);
+ void onGattRead(BluetoothDevice device, BluetoothGattService service);
- void onServiceReadFailed(BluetoothDevice device, UUID uuid);
+ void onGattReadFailed(BluetoothDevice device);
}
// TODO(jhahn): What's the maximum MTU size in Android?
@@ -48,36 +47,48 @@
private final Context mContext;
- private final ExecutorService mExecutor;
- private final Timer mTimer;
+ private final ScheduledThreadPoolExecutor mExecutor;
+
+ private final Set<UUID> mScanUuids;
private final Handler mHandler;
- private final ArrayDeque<Pair<BluetoothDevice, UUID>> mPendingReads;
+ private final ArrayDeque<BluetoothDevice> mPendingReads;
- private Pair<BluetoothDevice, UUID> mCurrentRead;
+ private final Runnable mCancelTask;
+
+ private BluetoothDevice mCurrentDevice;
private BluetoothGatt mCurrentGatt;
- private TimerTask mCurrentGattTimeout;
+ private ScheduledFuture mCurrentGattTimeout;
private BluetoothGattService mCurrentService;
+ private Iterator<BluetoothGattService> mCurrentServiceIterator;
private Iterator<BluetoothGattCharacteristic> mCurrentCharacteristicIterator;
- GattReader(Context context, Handler handler) {
+ GattReader(Context context, Set<UUID> scanUuids, Handler handler) {
mContext = context;
- mExecutor = Executors.newSingleThreadExecutor();
- mTimer = new Timer();
+ mExecutor = new ScheduledThreadPoolExecutor(1);
+ mScanUuids = scanUuids;
mHandler = handler;
mPendingReads = Queues.newArrayDeque();
+ mCancelTask =
+ new Runnable() {
+ @Override
+ public void run() {
+ Log.e(TAG, "gatt operation timed out: " + mCurrentDevice);
+ cancelAndMaybeReadNextDevice();
+ }
+ };
}
/**
* Reads a specified service from a remote device as well as their characteristics.
* <p/>
- * This is an asynchronous operation. Once service read is completed, the onServiceRead() or
+ * This is an asynchronous operation. Once service read is completed, the onGattRead() or
* onServiceReadFailed() callback is triggered.
*/
- synchronized void readService(BluetoothDevice device, UUID uuid) {
- mPendingReads.add(Pair.create(device, uuid));
- if (mCurrentRead == null) {
- maybeReadNextService();
+ synchronized void readDevice(BluetoothDevice device) {
+ mPendingReads.add(device);
+ if (mCurrentDevice == null) {
+ maybeReadNextDevice();
}
}
@@ -86,69 +97,54 @@
*/
synchronized void close() {
if (mCurrentGatt != null) {
- mCurrentGatt.disconnect();
+ mCurrentGatt.close();
}
- mTimer.cancel();
+ mExecutor.shutdown();
mPendingReads.clear();
}
- private synchronized void maybeReadNextService() {
+ private synchronized void maybeReadNextDevice() {
mCurrentGatt = null;
mCurrentGattTimeout = null;
mCurrentService = null;
+ mCurrentServiceIterator = null;
mCurrentCharacteristicIterator = null;
- mCurrentRead = mPendingReads.poll();
- if (mCurrentRead == null) {
+ mCurrentDevice = mPendingReads.poll();
+ if (mCurrentDevice == null) {
return;
}
- mCurrentGatt = mCurrentRead.first.connectGatt(mContext, false, this);
+
+ mCurrentGatt = mCurrentDevice.connectGatt(mContext, false, this);
mCurrentGattTimeout =
- new TimerTask() {
- @Override
- public void run() {
- Log.e(TAG, "gatt operation timed out: " + mCurrentRead.first);
- cancelAndMaybeReadNextService();
- }
- };
- mTimer.schedule(mCurrentGattTimeout, GATT_TIMEOUT_MS);
+ mExecutor.schedule(mCancelTask, GATT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
- private synchronized void finishAndMaybeReadNextService() {
- mCurrentGattTimeout.cancel();
+ private synchronized void finishAndMaybeReadNextDevice() {
+ mCurrentGattTimeout.cancel(false);
mCurrentGatt.disconnect();
- final BluetoothDevice currentDevice = mCurrentRead.first;
- final BluetoothGattService currentService = mCurrentService;
+ maybeReadNextDevice();
+ }
+
+ private synchronized void cancelAndMaybeReadNextDevice() {
+ mCurrentGattTimeout.cancel(false);
+ mCurrentGatt.close();
+
+ final BluetoothDevice device = mCurrentDevice;
mExecutor.submit(
new Runnable() {
@Override
public void run() {
- mHandler.onServiceRead(currentDevice, currentService);
+ mHandler.onGattReadFailed(device);
}
});
- maybeReadNextService();
- }
-
- private synchronized void cancelAndMaybeReadNextService() {
- mCurrentGattTimeout.cancel();
- mCurrentGatt.disconnect();
-
- final Pair<BluetoothDevice, UUID> currentRead = mCurrentRead;
- mExecutor.submit(
- new Runnable() {
- @Override
- public void run() {
- mHandler.onServiceReadFailed(currentRead.first, currentRead.second);
- }
- });
- maybeReadNextService();
+ maybeReadNextDevice();
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
-
if (newState != BluetoothGatt.STATE_CONNECTED) {
// Connection is disconnected. Release it.
gatt.close();
@@ -156,84 +152,118 @@
}
if (status != BluetoothGatt.GATT_SUCCESS) {
- Log.e(TAG, "connectGatt failed: " + mCurrentRead.first + " , status: " + status);
- cancelAndMaybeReadNextService();
+ Log.e(TAG, "connectGatt failed: " + mCurrentDevice + " , status: " + status);
+ cancelAndMaybeReadNextDevice();
return;
}
- if (!gatt.requestMtu(MTU)) {
- Log.e(TAG, "requestMtu failed: " + mCurrentRead.first);
-
- // Try to discover services although requesting MTU fails.
+ // MTU exchange is not allowed on a BR/EDR physical link.
+ // (Bluetooth Core Specification Volume 3, Part G, 4.3.1)
+ //
+ // There is no way to get the actual link type. So we use the device type for it.
+ // It is not clear whether DEVICE_TYPE_DUAL is on a BR/EDR physical link, but
+ // it is safe to not exchange MTU for that type too.
+ int deviceType = mCurrentDevice.getType();
+ if (deviceType != BluetoothDevice.DEVICE_TYPE_CLASSIC
+ && deviceType != BluetoothDevice.DEVICE_TYPE_DUAL) {
+ if (!gatt.requestMtu(MTU)) {
+ Log.e(TAG, "requestMtu failed: " + mCurrentDevice);
+ cancelAndMaybeReadNextDevice();
+ }
+ } else {
if (!gatt.discoverServices()) {
- Log.e(TAG, "discoverServices failed: " + mCurrentRead.first);
- cancelAndMaybeReadNextService();
+ Log.e(TAG, "discoverServices failed: " + mCurrentDevice);
+ cancelAndMaybeReadNextDevice();
}
}
}
@Override
- public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
+ public synchronized void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
if (status != BluetoothGatt.GATT_SUCCESS) {
- Log.w(TAG, "requestMtu failed: " + mCurrentRead.first + ", status: " + status);
+ Log.w(TAG, "requestMtu failed: " + mCurrentDevice + ", status: " + status);
+ cancelAndMaybeReadNextDevice();
+ return;
}
if (!gatt.discoverServices()) {
- Log.e(TAG, "discoverServices failed: " + mCurrentRead.first);
- cancelAndMaybeReadNextService();
+ Log.e(TAG, "discoverServices failed: " + mCurrentDevice);
+ cancelAndMaybeReadNextDevice();
}
}
@Override
- public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ public synchronized void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status != BluetoothGatt.GATT_SUCCESS) {
- Log.e(TAG, "discoverServices failed: " + mCurrentRead.first + ", status: " + status);
- cancelAndMaybeReadNextService();
+ Log.e(TAG, "discoverServices failed: " + mCurrentDevice + ", status: " + status);
+ cancelAndMaybeReadNextDevice();
return;
}
- mCurrentService = gatt.getService(mCurrentRead.second);
- if (mCurrentService == null) {
- Log.e(
- TAG,
- "service not found: " + mCurrentRead.first + ", uuid: " + mCurrentRead.second);
- cancelAndMaybeReadNextService();
+ mCurrentServiceIterator = gatt.getServices().iterator();
+ maybeReadNextService();
+ }
+
+ private void maybeReadNextService() {
+ while (mCurrentServiceIterator.hasNext()) {
+ mCurrentService = mCurrentServiceIterator.next();
+ if (mScanUuids.isEmpty() || mScanUuids.contains(mCurrentService.getUuid())) {
+ // Reset the timer.
+ if (!mCurrentGattTimeout.cancel(false)) {
+ // Already cancelled.
+ return;
+ }
+ mCurrentGattTimeout =
+ mExecutor.schedule(mCancelTask, GATT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+ mCurrentCharacteristicIterator = mCurrentService.getCharacteristics().iterator();
+ maybeReadNextCharacteristic();
+ return;
+ }
+ }
+
+ // All services have been read. Finish the current device read.
+ finishAndMaybeReadNextDevice();
+ }
+
+ @Override
+ public synchronized void onCharacteristicRead(
+ BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ super.onCharacteristicRead(gatt, characteristic, status);
+
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.e(TAG, "readCharacteristic failed: " + mCurrentDevice + ", status: " + status);
+ cancelAndMaybeReadNextDevice();
return;
}
- mCurrentCharacteristicIterator = mCurrentService.getCharacteristics().iterator();
maybeReadNextCharacteristic();
}
private void maybeReadNextCharacteristic() {
if (!mCurrentCharacteristicIterator.hasNext()) {
- // All characteristics have been read. Finish the current read.
- finishAndMaybeReadNextService();
+ // All characteristics have been read. Finish the current service read.
+ final BluetoothDevice device = mCurrentDevice;
+ final BluetoothGattService service = mCurrentService;
+ mExecutor.submit(
+ new Runnable() {
+ @Override
+ public void run() {
+ mHandler.onGattRead(device, service);
+ }
+ });
+ maybeReadNextService();
return;
}
BluetoothGattCharacteristic characteristic = mCurrentCharacteristicIterator.next();
if (!mCurrentGatt.readCharacteristic(characteristic)) {
- Log.e(TAG, "readCharacteristic failed: " + mCurrentRead.first);
- cancelAndMaybeReadNextService();
+ Log.e(TAG, "readCharacteristic failed: " + mCurrentDevice);
+ cancelAndMaybeReadNextDevice();
}
}
-
- @Override
- public void onCharacteristicRead(
- BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
- super.onCharacteristicRead(gatt, characteristic, status);
-
- if (status != BluetoothGatt.GATT_SUCCESS) {
- Log.e(TAG, "readCharacteristic failed: " + mCurrentRead.first + ", status: " + status);
- cancelAndMaybeReadNextService();
- return;
- }
-
- maybeReadNextCharacteristic();
- }
}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattServer.java b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattServer.java
index 45683a8..7195102 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattServer.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattServer.java
@@ -71,7 +71,7 @@
public void onMtuChanged(BluetoothDevice device, int mtu) {
super.onMtuChanged(device, mtu);
- mMtus.put(device, new Integer(mtu));
+ mMtus.put(device, mtu);
}
@Override
@@ -86,16 +86,27 @@
if (offset >= data.length) {
data = null;
} else {
- int mtu = DEFAULT_MTU;
- if (mMtus.containsKey(device)) {
- mtu = mMtus.get(device).intValue();
+ // MTU exchange is not allowed on a BR/EDR physical link. Send all requested data in that link.
+ int deviceType = device.getType();
+ if (deviceType == BluetoothDevice.DEVICE_TYPE_CLASSIC
+ || deviceType == BluetoothDevice.DEVICE_TYPE_DUAL) {
+ if (offset > 0) {
+ data = Arrays.copyOfRange(data, offset, data.length);
+ }
+ } else {
+ int mtu = DEFAULT_MTU;
+ if (mMtus.containsKey(device)) {
+ mtu = mMtus.get(device);
+ }
+ // We can send data up to MTU - 1 bytes.
+ int to = offset + mtu - 1;
+ if (to > data.length) {
+ to = data.length;
+ }
+ if (offset > 0 || to < data.length) {
+ data = Arrays.copyOfRange(data, offset, to);
+ }
}
- // We can send data up to MTU - 1 bytes.
- int to = offset + mtu - 1;
- if (to > data.length) {
- to = data.length;
- }
- data = Arrays.copyOfRange(data, offset, to);
}
mBluetoothGattServer.sendResponse(
device, requestId, BluetoothGatt.GATT_SUCCESS, offset, data);