BluetoothManager is a class responsible for interaction with a ble device.
It is injected as a singelton with RxBleClient and starts scanning on fragment/service's start method.
Sometimes scanning is stopped automatically and sometimes after few seconds and I can't find the reason of it.
The thing is that the device is still broadcasting its packages, but my application is not able to receive it as BluetoothAdapter has already stopped the scan.
Here are logs from scanning in a service:
03-09 11:17:58.526 10196-10196/com.turboegg.storm D/BluetoothManager *** 212: Clear scan
03-09 11:17:58.527 10196-10196/com.turboegg.storm D/BluetoothManager *** 72: Start scanning
03-09 11:17:58.579 10196-10196/com.turboegg.storm I/AppCompatViewInflater: app:theme is now deprecated. Please move to using android:theme instead.
03-09 11:17:58.589 10196-10196/com.turboegg.storm I/AppCompatViewInflater: app:theme is now deprecated. Please move to using android:theme instead.
03-09 11:17:58.602 10196-10196/com.turboegg.storm I/AppCompatViewInflater: app:theme is now deprecated. Please move to using android:theme instead.
03-09 11:17:58.604 10196-10196/com.turboegg.storm I/AppCompatViewInflater: app:theme is now deprecated. Please move to using android:theme instead.
03-09 11:18:01.592 10196-10196/com.turboegg.storm D/BluetoothAdapter: startLeScan(): null
03-09 11:18:01.596 10196-10196/com.turboegg.storm D/BluetoothAdapter: STATE_ON
03-09 11:18:01.600 10196-10207/com.turboegg.storm D/BluetoothLeScanner: onClientRegistered() - status=0 clientIf=8
03-09 11:18:01.656 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device C6:86:B6:35:FF:00
03-09 11:18:01.784 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device EE:AC:C7:32:9A:F6
03-09 11:18:02.643 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device C6:86:B6:35:FF:00
03-09 11:18:02.835 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device EE:AC:C7:32:9A:F6
03-09 11:18:03.659 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device C6:86:B6:35:FF:00
03-09 11:18:03.796 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device EE:AC:C7:32:9A:F6
03-09 11:18:04.663 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device C6:86:B6:35:FF:00
03-09 11:18:04.785 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device EE:AC:C7:32:9A:F6
03-09 11:18:05.648 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device C6:86:B6:35:FF:00
03-09 11:18:05.809 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device EE:AC:C7:32:9A:F6
03-09 11:18:06.817 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device EE:AC:C7:32:9A:F6
03-09 11:18:07.672 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device C6:86:B6:35:FF:00
03-09 11:18:07.827 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device EE:AC:C7:32:9A:F6
03-09 11:18:08.682 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device C6:86:B6:35:FF:00
03-09 11:18:08.826 10196-10196/com.turboegg.storm D/BluetoothManager *** 79: Device EE:AC:C7:32:9A:F6
03-09 11:18:09.009 10196-10196/com.turboegg.storm D/BluetoothAdapter: stopLeScan()
03-09 11:18:09.011 10196-10196/com.turboegg.storm D/BluetoothAdapter: STATE_ON
Here is BluetoohManager:
public class BluetoothManager {
private Context context;
private RxBleClient rxBleClient;
private StorageManager storageManager;
private Subscription scanSubscription, connectionSubscription, notificationSubscription, writeSubscription, keepAwakenSubscription;
private RxBleDevice connectedDevice;
private RxBleConnection activeConnection;
public BluetoothManager(Context context, RxBleClient rxBleClient, StorageManager storageManager) {
this.context = context;
this.rxBleClient = rxBleClient;
this.storageManager = storageManager;
}
public void onDestroy() {
stop();
storageManager.setAllSensorsDisconnectible();
storageManager.clearResources();
}
public void stop() {
clearScan();
disconnect();
}
public void stopScan() {
clearScan();
}
public void disconnect() {
clearConnection();
clearNotification();
clearWrite();
clearKeepAwaken();
}
public void startScan() {
clearScan();
Timber.d("Start scanning");
scanSubscription = Observable.timer(3, TimeUnit.SECONDS)
.flatMap(ignored -> rxBleClient.scanBleDevices())
.filter(rxBleScanResult -> CommonUtils.isStormSensor(rxBleScanResult.getBleDevice()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(rxBleScanResult -> {
RxBleDevice device = rxBleScanResult.getBleDevice();
Timber.d("Device %s", device.getMacAddress());
if (device.getMacAddress() != null && !storageManager.containsDiscoveredDevice(device.getMacAddress())) {
Timber.d("Found device, %s %s", device.getName(), device.getMacAddress());
storageManager.addDiscoveredSensor(device.getMacAddress(), device.getName());
EventBus.getDefault().post(new DeviceDiscovered(device.getMacAddress(), device.getName()));
}
storageManager.setSensorConnectible(device.getMacAddress(), Calendar.getInstance().getTime());
}, this::onBleFailure);
}
public void connectDevice(String macAddress, String outInDoor) {
disconnect();
connectedDevice = rxBleClient.getBleDevice(macAddress);
if (connectedDevice != null) {
Timber.d("Start connecting to %s", macAddress);
connectionSubscription = connectedDevice.establishConnection(context, false)
.subscribe(connection -> {
// Need to delay write operations due to sensor processor limitations
writeCharacteristic(connection, CommonUtils.getBleTime())
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
writeCharacteristic(connection, AppConstants.BLUETOOTH_SET_BUZZER_OFF)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
writeCharacteristic(connection, AppConstants.BLUETOOTH_SET_MASK)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
writeCharacteristic(connection, AppConstants.BLUETOOTH_CLEAR_STATS)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
writeCharacteristic(connection, AppConstants.BLUETOOTH_CLEAR_MEM)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
writeCharacteristic(connection, outInDoor)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
writeCharacteristic(connection, AppConstants.BLUETOOTH_CLEAR_MASK)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
writeCharacteristic(connection, AppConstants.BLUETOOTH_SET_MASK)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS);
activeConnection = connection;
getBasicParams(connection, macAddress);
subscribeForNotifications(connection, macAddress);
}, throwable -> {
Timber.e("Error while connecting " + throwable.toString());
handleConnectionError(macAddress);
disconnect();
});
}
}
public void writeOutInDoor(String outInDoor) {
clearWrite();
writeSubscription = Observable.combineLatest(
writeCharacteristic(activeConnection, AppConstants.BLUETOOTH_CLEAR_STATS)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS),
writeCharacteristic(activeConnection, AppConstants.BLUETOOTH_CLEAR_MEM)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS),
writeCharacteristic(activeConnection, outInDoor)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS),
writeCharacteristic(activeConnection, AppConstants.BLUETOOTH_CLEAR_MASK)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS),
writeCharacteristic(activeConnection, AppConstants.BLUETOOTH_SET_MASK)
.delay(AppConstants.BLUETOOTH_WRITE_DELAY_MILIS, TimeUnit.MILLISECONDS),
(bytes, bytes2, value, bytes4, bytes5) -> value)
.subscribe(value -> Timber.d("Written value %s", new String(value)),
throwable -> Timber.e("Error writing outInDoor: %s", throwable.toString())
);
}
public void handleDeviceStatus(boolean checked) {
if (checked) {
keepDeviceAwaken();
} else {
clearKeepAwaken();
}
}
public void handleDeviceDisconnection(String macAddress) {
if (connectedDevice != null && connectedDevice.getMacAddress().equals(macAddress)) {
disconnect();
}
}
/**
* Private methods
*/
private void getBasicParams(RxBleConnection connection, String macAddress) {
Observable.combineLatest(
connection.readCharacteristic(AppConstants.BLUETOOTH_UUID_BATTERY),
connection.readCharacteristic(AppConstants.BLUETOOTH_UUID_FIRMWARE),
(bytes, bytes2) -> new DeviceConnected(bytes, bytes2, macAddress))
.take(1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(deviceConnected -> {
Timber.d("Bat %s", deviceConnected.getBattery());
Timber.d("Fmw %s", deviceConnected.getFirmware());
storageManager.updateSensorInfo(macAddress, deviceConnected.getBattery(), deviceConnected.getFirmware());
storageManager.setSensorConnected(macAddress);
handleDeviceStatus(storageManager.getStormSensor(macAddress).isStatusChecked());
EventBus.getDefault().post(deviceConnected);
}, throwable -> Timber.e("Sensor basic info error %s", throwable.getMessage()));
}
private void subscribeForNotifications(RxBleConnection connection, String macAddress) {
notificationSubscription = Observable.combineLatest(
connection.setupNotification(AppConstants.BLUETOOTH_UUID_EVENT)
.<byte[]>flatMap(observable -> observable),
connection.setupNotification(AppConstants.BLUETOOTH_UUID_DISTANCE)
.<byte[]>flatMap(observable -> observable),
NotificationStormEvent::new)
.filter(CommonUtils::isStormEvent)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(notificationStormEvent -> {
Timber.d("New storm, event: %s, distance %s km", notificationStormEvent.getEvent(), notificationStormEvent.getDistance());
storageManager.addStormEvent(Calendar.getInstance().getTime(), notificationStormEvent.getDistance());
EventBus.getDefault().post(notificationStormEvent);
}, throwable -> Timber.e("Notification error %s", throwable.toString()));
}
private void keepDeviceAwaken() {
clearKeepAwaken();
keepAwakenSubscription = Observable.interval(
AppConstants.BLUETOOTH_KEEP_AWAKE_MINS, AppConstants.BLUETOOTH_KEEP_AWAKE_MINS, TimeUnit.MINUTES)
.flatMap(aLong -> writeCharacteristic(activeConnection, CommonUtils.getBleTime()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(bytes -> Timber.d("Written value %s to keep device awaken", new String(bytes)),
throwable -> Timber.e("Error keeping awake %s", throwable.toString()));
}
private void onBleFailure(Throwable throwable) {
if (throwable instanceof BleScanException) {
handleBleScanException((BleScanException) throwable);
}
}
private void clearScan() {
Timber.d("Clear scan");
if (scanSubscription != null) {
scanSubscription.unsubscribe();
Timber.d("Clear unsubscribed");
}
scanSubscription = null;
}
private void clearConnection() {
if (connectionSubscription != null) connectionSubscription.unsubscribe();
connectionSubscription = null;
connectedDevice = null;
activeConnection = null;
}
private void clearNotification() {
if (notificationSubscription != null) notificationSubscription.unsubscribe();
notificationSubscription = null;
}
private void clearWrite() {
if (writeSubscription != null) writeSubscription.unsubscribe();
writeSubscription = null;
}
private void clearKeepAwaken() {
if (keepAwakenSubscription != null) keepAwakenSubscription.unsubscribe();
keepAwakenSubscription = null;
}
private Observable<byte[]> writeCharacteristic(RxBleConnection connection, String value) {
byte[] bytesToWrite = value.getBytes(StandardCharsets.UTF_8);
return connection.writeCharacteristic(AppConstants.BLUETOOTH_UUID_UART_WRITE, bytesToWrite);
}
private void handleBleScanException(BleScanException bleScanException) {
switch (bleScanException.getReason()) {
case BleScanException.BLUETOOTH_NOT_AVAILABLE:
Timber.e("Bluetooth is not available");
break;
case BleScanException.BLUETOOTH_DISABLED:
Timber.e("Enable bluetooth and try again");
break;
case BleScanException.LOCATION_PERMISSION_MISSING:
Timber.e("On Android 6.0 location permission is required. Implement Runtime Permissions ");
break;
case BleScanException.LOCATION_SERVICES_DISABLED:
Timber.e("Location services needs to be enabled on Android 6.0");
break;
case BleScanException.BLUETOOTH_CANNOT_START:
default:
Timber.e("Unable to onStart scanning");
break;
}
}
private void handleConnectionError(String macAddress) {
// This is the case where e.g. connection was lost or the device was turned off
// Need to call for realm to modify the sensor object in different thread
Realm realm = Realm.getDefaultInstance();
realm.executeTransaction(realm1 -> {
StormSensor sensor = realm1.where(StormSensor.class).equalTo(AppConstants.STORM_SENSOR_MAC_ADDRESS, macAddress).findFirst();
sensor.setConnected(false);
});
realm.close();
}
}