From 6022a39f40d6813598956ff5e5ce42a82e3e5775 Mon Sep 17 00:00:00 2001 From: Grzegorz Miszewski Date: Mon, 9 Oct 2023 12:29:29 +0200 Subject: [PATCH] refactor: move everything from MultiplatformAdapter library into android library folder --- android/build.gradle | 4 +- android/src/main/AndroidManifest.xml | 19 +- .../main/java/com/bleplx/BlePlxModule.java | 30 +- .../com/bleplx/adapter/AdvertisementData.java | 250 +++ .../java/com/bleplx/adapter/BleAdapter.java | 250 +++ .../com/bleplx/adapter/BleAdapterCreator.java | 7 + .../com/bleplx/adapter/BleAdapterFactory.java | 21 + .../java/com/bleplx/adapter/BleException.java | 4 + .../java/com/bleplx/adapter/BleModule.java | 1565 +++++++++++++++++ .../com/bleplx/adapter/Characteristic.java | 151 ++ .../com/bleplx/adapter/ConnectionOptions.java | 72 + .../com/bleplx/adapter/ConnectionState.java | 12 + .../java/com/bleplx/adapter/Descriptor.java | 115 ++ .../main/java/com/bleplx/adapter/Device.java | 81 + .../com/bleplx/adapter/OnErrorCallback.java | 8 + .../com/bleplx/adapter/OnEventCallback.java | 6 + .../com/bleplx/adapter/OnSuccessCallback.java | 6 + .../com/bleplx/adapter/RefreshGattMoment.java | 20 + .../java/com/bleplx/adapter/ScanResult.java | 132 ++ .../main/java/com/bleplx/adapter/Service.java | 56 + .../com/bleplx/adapter/errors/BleError.java | 32 + .../bleplx/adapter/errors/BleErrorCode.java | 57 + .../bleplx/adapter/errors/BleErrorUtils.java | 86 + .../bleplx/adapter/errors/ErrorConverter.java | 219 +++ .../CannotMonitorCharacteristicException.java | 15 + .../bleplx/adapter/utils/Base64Converter.java | 12 + .../com/bleplx/adapter/utils/ByteUtils.java | 15 + .../com/bleplx/adapter/utils/Constants.java | 66 + .../bleplx/adapter/utils/DisposableMap.java | 39 + .../com/bleplx/adapter/utils/IdGenerator.java | 22 + .../bleplx/adapter/utils/IdGeneratorKey.java | 37 + .../com/bleplx/adapter/utils/LogLevel.java | 47 + .../utils/RefreshGattCustomOperation.java | 48 + .../bleplx/adapter/utils/SafeExecutor.java | 33 + .../bleplx/adapter/utils/ServiceFactory.java | 16 + .../bleplx/adapter/utils/UUIDConverter.java | 51 + .../mapper/RxBleDeviceToDeviceMapper.java | 19 + .../RxScanResultToScanResultMapper.java | 20 + .../BleErrorToJsObjectConverter.java | 4 +- .../CharacteristicToJsObjectConverter.java | 6 +- .../DescriptorToJsObjectConverter.java | 8 +- .../converter/DeviceToJsObjectConverter.java | 4 +- .../ScanResultToJsObjectConverter.java | 8 +- .../converter/ServiceToJsObjectConverter.java | 4 +- 44 files changed, 3642 insertions(+), 35 deletions(-) create mode 100644 android/src/main/java/com/bleplx/adapter/AdvertisementData.java create mode 100644 android/src/main/java/com/bleplx/adapter/BleAdapter.java create mode 100644 android/src/main/java/com/bleplx/adapter/BleAdapterCreator.java create mode 100644 android/src/main/java/com/bleplx/adapter/BleAdapterFactory.java create mode 100644 android/src/main/java/com/bleplx/adapter/BleException.java create mode 100755 android/src/main/java/com/bleplx/adapter/BleModule.java create mode 100755 android/src/main/java/com/bleplx/adapter/Characteristic.java create mode 100644 android/src/main/java/com/bleplx/adapter/ConnectionOptions.java create mode 100644 android/src/main/java/com/bleplx/adapter/ConnectionState.java create mode 100644 android/src/main/java/com/bleplx/adapter/Descriptor.java create mode 100755 android/src/main/java/com/bleplx/adapter/Device.java create mode 100644 android/src/main/java/com/bleplx/adapter/OnErrorCallback.java create mode 100644 android/src/main/java/com/bleplx/adapter/OnEventCallback.java create mode 100644 android/src/main/java/com/bleplx/adapter/OnSuccessCallback.java create mode 100755 android/src/main/java/com/bleplx/adapter/RefreshGattMoment.java create mode 100644 android/src/main/java/com/bleplx/adapter/ScanResult.java create mode 100755 android/src/main/java/com/bleplx/adapter/Service.java create mode 100755 android/src/main/java/com/bleplx/adapter/errors/BleError.java create mode 100755 android/src/main/java/com/bleplx/adapter/errors/BleErrorCode.java create mode 100755 android/src/main/java/com/bleplx/adapter/errors/BleErrorUtils.java create mode 100755 android/src/main/java/com/bleplx/adapter/errors/ErrorConverter.java create mode 100755 android/src/main/java/com/bleplx/adapter/exceptions/CannotMonitorCharacteristicException.java create mode 100755 android/src/main/java/com/bleplx/adapter/utils/Base64Converter.java create mode 100644 android/src/main/java/com/bleplx/adapter/utils/ByteUtils.java create mode 100755 android/src/main/java/com/bleplx/adapter/utils/Constants.java create mode 100755 android/src/main/java/com/bleplx/adapter/utils/DisposableMap.java create mode 100755 android/src/main/java/com/bleplx/adapter/utils/IdGenerator.java create mode 100755 android/src/main/java/com/bleplx/adapter/utils/IdGeneratorKey.java create mode 100755 android/src/main/java/com/bleplx/adapter/utils/LogLevel.java create mode 100755 android/src/main/java/com/bleplx/adapter/utils/RefreshGattCustomOperation.java create mode 100644 android/src/main/java/com/bleplx/adapter/utils/SafeExecutor.java create mode 100644 android/src/main/java/com/bleplx/adapter/utils/ServiceFactory.java create mode 100755 android/src/main/java/com/bleplx/adapter/utils/UUIDConverter.java create mode 100644 android/src/main/java/com/bleplx/adapter/utils/mapper/RxBleDeviceToDeviceMapper.java create mode 100644 android/src/main/java/com/bleplx/adapter/utils/mapper/RxScanResultToScanResultMapper.java diff --git a/android/build.gradle b/android/build.gradle index c0b30534..54b42751 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -81,7 +81,6 @@ android { repositories { mavenCentral() google() - maven { url 'https://jitpack.io' } } @@ -90,7 +89,8 @@ dependencies { // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" - implementation 'com.github.dotintent:MultiPlatformBleAdapter:a136c3f4ac' + implementation 'io.reactivex.rxjava2:rxjava:2.2.17' + implementation "com.polidea.rxandroidble2:rxandroidble:1.17.2" } if (isNewArchitectureEnabled()) { diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index ab962717..8854d00e 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,20 @@ + package="com.bleplx" + xmlns:tools="http://schemas.android.com/tools"> + + + + + + + diff --git a/android/src/main/java/com/bleplx/BlePlxModule.java b/android/src/main/java/com/bleplx/BlePlxModule.java index 229a7023..1464bd27 100644 --- a/android/src/main/java/com/bleplx/BlePlxModule.java +++ b/android/src/main/java/com/bleplx/BlePlxModule.java @@ -14,20 +14,20 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.modules.core.DeviceEventManagerModule; -import com.polidea.multiplatformbleadapter.BleAdapter; -import com.polidea.multiplatformbleadapter.BleAdapterFactory; -import com.polidea.multiplatformbleadapter.Characteristic; -import com.polidea.multiplatformbleadapter.ConnectionOptions; -import com.polidea.multiplatformbleadapter.ConnectionState; -import com.polidea.multiplatformbleadapter.Descriptor; -import com.polidea.multiplatformbleadapter.Device; -import com.polidea.multiplatformbleadapter.OnErrorCallback; -import com.polidea.multiplatformbleadapter.OnEventCallback; -import com.polidea.multiplatformbleadapter.OnSuccessCallback; -import com.polidea.multiplatformbleadapter.RefreshGattMoment; -import com.polidea.multiplatformbleadapter.ScanResult; -import com.polidea.multiplatformbleadapter.Service; -import com.polidea.multiplatformbleadapter.errors.BleError; +import com.bleplx.adapter.BleAdapter; +import com.bleplx.adapter.BleAdapterFactory; +import com.bleplx.adapter.Characteristic; +import com.bleplx.adapter.ConnectionOptions; +import com.bleplx.adapter.ConnectionState; +import com.bleplx.adapter.Descriptor; +import com.bleplx.adapter.Device; +import com.bleplx.adapter.OnErrorCallback; +import com.bleplx.adapter.OnEventCallback; +import com.bleplx.adapter.OnSuccessCallback; +import com.bleplx.adapter.RefreshGattMoment; +import com.bleplx.adapter.ScanResult; +import com.bleplx.adapter.Service; +import com.bleplx.adapter.errors.BleError; import com.bleplx.converter.BleErrorToJsObjectConverter; import com.bleplx.converter.CharacteristicToJsObjectConverter; import com.bleplx.converter.DescriptorToJsObjectConverter; @@ -936,7 +936,7 @@ public void onError(BleError bleError) { @ReactMethod public void addListener(String eventName) { - // Keep: Required for RN built in Event Emitter Calls. + // Keep: Required for RN built in Event Emitter Calls. } @ReactMethod diff --git a/android/src/main/java/com/bleplx/adapter/AdvertisementData.java b/android/src/main/java/com/bleplx/adapter/AdvertisementData.java new file mode 100644 index 00000000..35fbb3b1 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/AdvertisementData.java @@ -0,0 +1,250 @@ +package com.bleplx.adapter; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +public class AdvertisementData { + + private byte[] manufacturerData; + private Map serviceData; + private List serviceUUIDs; + private String localName; + private Integer txPowerLevel; + private List solicitedServiceUUIDs; + private byte[] rawScanRecord; + + private static final long BLUETOOTH_BASE_UUID_LSB = 0x800000805F9B34FBL; + private static final int BLUETOOTH_BASE_UUID_MSB = 0x00001000; + + public String getLocalName() { + return localName; + } + + public byte[] getManufacturerData() { + return manufacturerData; + } + + public Map getServiceData() { + return serviceData; + } + + public List getServiceUUIDs() { + return serviceUUIDs; + } + + public Integer getTxPowerLevel() { + return txPowerLevel; + } + + public List getSolicitedServiceUUIDs() { + return solicitedServiceUUIDs; + } + + public byte[] getRawScanRecord() { + return rawScanRecord; + } + + private AdvertisementData() {} + + public AdvertisementData(byte[] manufacturerData, + Map serviceData, + List serviceUUIDs, + String localName, + Integer txPowerLevel, + List solicitedServiceUUIDs) { + this.manufacturerData = manufacturerData; + this.serviceData = serviceData; + this.serviceUUIDs = serviceUUIDs; + this.localName = localName; + this.txPowerLevel = txPowerLevel; + this.solicitedServiceUUIDs = solicitedServiceUUIDs; + } + + public static AdvertisementData parseScanResponseData(byte[] advertisement) { + AdvertisementData advData = new AdvertisementData(); + advData.rawScanRecord = advertisement; + + ByteBuffer rawData = ByteBuffer.wrap(advertisement).order(ByteOrder.LITTLE_ENDIAN); + while (rawData.remaining() >= 2) { + int adLength = rawData.get() & 0xFF; + if (adLength == 0) break; + adLength -= 1; + int adType = rawData.get() & 0xFF; + if (rawData.remaining() < adLength) break; + parseAdvertisementData(advData, adType, adLength, rawData.slice().order(ByteOrder.LITTLE_ENDIAN)); + rawData.position(rawData.position() + adLength); + } + return advData; + } + + private static void parseAdvertisementData(AdvertisementData advData, int adType, int adLength, ByteBuffer data) { + switch (adType) { + case 0xFF: + parseManufacturerData(advData, adLength, data); + break; + + case 0x02: + case 0x03: + parseServiceUUIDs(advData, adLength, data, 2); + break; + case 0x04: + case 0x05: + parseServiceUUIDs(advData, adLength, data, 4); + break; + case 0x06: + case 0x07: + parseServiceUUIDs(advData, adLength, data, 16); + break; + + case 0x08: + case 0x09: + parseLocalName(advData, adType, adLength, data); + break; + + case 0x0A: + parseTxPowerLevel(advData, adLength, data); + break; + + case 0x14: + parseSolicitedServiceUUIDs(advData, adLength, data, 2); + break; + case 0x1F: + parseSolicitedServiceUUIDs(advData, adLength, data, 4); + break; + case 0x15: + parseSolicitedServiceUUIDs(advData, adLength, data, 16); + break; + + case 0x16: + parseServiceData(advData, adLength, data, 2); + break; + case 0x20: + parseServiceData(advData, adLength, data, 4); + break; + case 0x21: + parseServiceData(advData, adLength, data, 16); + break; + } + } + + private static void parseLocalName(AdvertisementData advData, int adType, int adLength, ByteBuffer data) { + // Complete local name is preferred over short local name. + if (advData.localName == null || adType == 0x09) { + byte[] bytes = new byte[adLength]; + data.get(bytes, 0, adLength); + advData.localName = new String(bytes, Charset.forName("UTF-8")); + } + } + + private static UUID parseUUID(ByteBuffer data, int uuidLength) { + long lsb; + long msb; + switch (uuidLength) { + case 2: + msb = (((long) data.getShort() & 0xFFFF) << 32) + BLUETOOTH_BASE_UUID_MSB; + lsb = BLUETOOTH_BASE_UUID_LSB; + break; + case 4: + msb = ((long) data.getInt() << 32) + BLUETOOTH_BASE_UUID_MSB; + lsb = BLUETOOTH_BASE_UUID_LSB; + break; + case 16: + lsb = data.getLong(); + msb = data.getLong(); + break; + default: + data.position(data.position() + uuidLength); + return null; + } + return new UUID(msb, lsb); + } + + private static void parseSolicitedServiceUUIDs(AdvertisementData advData, int adLength, ByteBuffer data, int uuidLength) { + if (advData.solicitedServiceUUIDs == null) advData.solicitedServiceUUIDs = new ArrayList<>(); + while (data.remaining() >= uuidLength && data.position() < adLength) { + advData.solicitedServiceUUIDs.add(parseUUID(data, uuidLength)); + } + } + + private static void parseServiceUUIDs(AdvertisementData advData, int adLength, ByteBuffer data, int uuidLength) { + if (advData.serviceUUIDs == null) advData.serviceUUIDs = new ArrayList<>(); + while (data.remaining() >= uuidLength && data.position() < adLength) { + advData.serviceUUIDs.add(parseUUID(data, uuidLength)); + } + } + + private static void parseServiceData(AdvertisementData advData, int adLength, ByteBuffer data, int uuidLength) { + if (adLength < uuidLength) return; + if (advData.serviceData == null) advData.serviceData = new HashMap<>(); + UUID serviceUUID = parseUUID(data, uuidLength); + int serviceDataLength = adLength - uuidLength; + byte[] serviceData = new byte[serviceDataLength]; + data.get(serviceData, 0, serviceDataLength); + advData.serviceData.put(serviceUUID, serviceData); + } + + private static void parseTxPowerLevel(AdvertisementData advData, int adLength, ByteBuffer data) { + if (adLength != 1) return; + advData.txPowerLevel = (int) data.get(); + } + + private static void parseManufacturerData(AdvertisementData advData, int adLength, ByteBuffer data) { + if (adLength < 2) return; + advData.manufacturerData = new byte[adLength]; + data.get(advData.manufacturerData, 0, adLength); + } + + @Override + public String toString() { + return "AdvertisementData{" + + "manufacturerData=" + Arrays.toString(manufacturerData) + + ", serviceData=" + serviceData + + ", serviceUUIDs=" + serviceUUIDs + + ", localName='" + localName + '\'' + + ", txPowerLevel=" + txPowerLevel + + ", solicitedServiceUUIDs=" + solicitedServiceUUIDs + + ", rawScanRecord=" + Arrays.toString(rawScanRecord) + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AdvertisementData that = (AdvertisementData) o; + + if (!Arrays.equals(manufacturerData, that.manufacturerData)) return false; + if (!Objects.equals(serviceData, that.serviceData)) + return false; + if (!Objects.equals(serviceUUIDs, that.serviceUUIDs)) + return false; + if (!Objects.equals(localName, that.localName)) + return false; + if (!Objects.equals(txPowerLevel, that.txPowerLevel)) + return false; + if (!Objects.equals(solicitedServiceUUIDs, that.solicitedServiceUUIDs)) + return false; + return Arrays.equals(rawScanRecord, that.rawScanRecord); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(manufacturerData); + result = 31 * result + (serviceData != null ? serviceData.hashCode() : 0); + result = 31 * result + (serviceUUIDs != null ? serviceUUIDs.hashCode() : 0); + result = 31 * result + (localName != null ? localName.hashCode() : 0); + result = 31 * result + (txPowerLevel != null ? txPowerLevel.hashCode() : 0); + result = 31 * result + (solicitedServiceUUIDs != null ? solicitedServiceUUIDs.hashCode() : 0); + result = 31 * result + Arrays.hashCode(rawScanRecord); + return result; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/BleAdapter.java b/android/src/main/java/com/bleplx/adapter/BleAdapter.java new file mode 100644 index 00000000..9d63f3bd --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/BleAdapter.java @@ -0,0 +1,250 @@ +package com.bleplx.adapter; + +import com.bleplx.adapter.errors.BleError; + +import java.util.List; + +public interface BleAdapter { + + void createClient(String restoreStateIdentifier, + OnEventCallback onAdapterStateChangeCallback, + OnEventCallback onStateRestored); + + void destroyClient(); + + void enable( + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void disable( + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + String getCurrentState(); + + void startDeviceScan( + String[] filteredUUIDs, + int scanMode, + int callbackType, + OnEventCallback onEventCallback, + OnErrorCallback onErrorCallback); + + void stopDeviceScan(); + + void requestConnectionPriorityForDevice( + String deviceIdentifier, + int connectionPriority, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void readRSSIForDevice( + String deviceIdentifier, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void requestMTUForDevice( + String deviceIdentifier, + int mtu, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void getKnownDevices( + String[] deviceIdentifiers, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void getConnectedDevices( + String[] serviceUUIDs, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void connectToDevice( + String deviceIdentifier, + ConnectionOptions connectionOptions, + OnSuccessCallback onSuccessCallback, + OnEventCallback onConnectionStateChangedCallback, + OnErrorCallback onErrorCallback); + + void cancelDeviceConnection( + String deviceIdentifier, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void isDeviceConnected( + String deviceIdentifier, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void discoverAllServicesAndCharacteristicsForDevice( + String deviceIdentifier, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + List getServicesForDevice( + String deviceIdentifier) throws BleError; + + List getCharacteristicsForDevice( + String deviceIdentifier, + String serviceUUID) throws BleError; + + List getCharacteristicsForService( + int serviceIdentifier) throws BleError; + + List descriptorsForDevice( + String deviceIdentifier, + String serviceUUID, + String characteristicUUID) throws BleError; + + List descriptorsForService( + int serviceIdentifier, + String characteristicUUID) throws BleError; + + List descriptorsForCharacteristic( + int characteristicIdentifier) throws BleError; + + + void readCharacteristicForDevice( + String deviceIdentifier, + String serviceUUID, + String characteristicUUID, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void readCharacteristicForService( + int serviceIdentifier, + String characteristicUUID, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void readCharacteristic( + int characteristicIdentifer, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void writeCharacteristicForDevice( + String deviceIdentifier, + String serviceUUID, + String characteristicUUID, + String valueBase64, + boolean withResponse, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void writeCharacteristicForService( + int serviceIdentifier, + String characteristicUUID, + String valueBase64, + boolean withResponse, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void writeCharacteristic( + int characteristicIdentifier, + String valueBase64, + boolean withResponse, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void monitorCharacteristicForDevice( + String deviceIdentifier, + String serviceUUID, + String characteristicUUID, + String transactionId, + OnEventCallback onEventCallback, + OnErrorCallback onErrorCallback); + + void monitorCharacteristicForService( + int serviceIdentifier, + String characteristicUUID, + String transactionId, + OnEventCallback onEventCallback, + OnErrorCallback onErrorCallback); + + void monitorCharacteristic( + int characteristicIdentifier, + String transactionId, + OnEventCallback onEventCallback, + OnErrorCallback onErrorCallback); + + void readDescriptorForDevice( + final String deviceId, + final String serviceUUID, + final String characteristicUUID, + final String descriptorUUID, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback); + + void readDescriptorForService( + final int serviceIdentifier, + final String characteristicUUID, + final String descriptorUUID, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback); + + void readDescriptorForCharacteristic( + final int characteristicIdentifier, + final String descriptorUUID, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback); + + void readDescriptor( + final int descriptorIdentifier, + final String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback); + + void writeDescriptorForDevice( + final String deviceId, + final String serviceUUID, + final String characteristicUUID, + final String descriptorUUID, + final String valueBase64, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback); + + void writeDescriptorForService( + final int serviceIdentifier, + final String characteristicUUID, + final String descriptorUUID, + final String valueBase64, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback); + + void writeDescriptorForCharacteristic( + final int characteristicIdentifier, + final String descriptorUUID, + final String valueBase64, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback); + + void writeDescriptor( + final int descriptorIdentifier, + final String valueBase64, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback); + + void cancelTransaction(String transactionId); + + void setLogLevel(String logLevel); + + String getLogLevel(); +} diff --git a/android/src/main/java/com/bleplx/adapter/BleAdapterCreator.java b/android/src/main/java/com/bleplx/adapter/BleAdapterCreator.java new file mode 100644 index 00000000..1d9fb361 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/BleAdapterCreator.java @@ -0,0 +1,7 @@ +package com.bleplx.adapter; + +import android.content.Context; + +public interface BleAdapterCreator { + BleAdapter createAdapter(Context context); +} diff --git a/android/src/main/java/com/bleplx/adapter/BleAdapterFactory.java b/android/src/main/java/com/bleplx/adapter/BleAdapterFactory.java new file mode 100644 index 00000000..b2143895 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/BleAdapterFactory.java @@ -0,0 +1,21 @@ +package com.bleplx.adapter; + +import android.content.Context; + +public class BleAdapterFactory { + + private static BleAdapterCreator bleAdapterCreator = new BleAdapterCreator() { + @Override + public BleAdapter createAdapter(Context context) { + return new BleModule(context); + } + }; + + public static BleAdapter getNewAdapter(Context context) { + return bleAdapterCreator.createAdapter(context); + } + + public static void setBleAdapterCreator(BleAdapterCreator bleAdapterCreator) { + BleAdapterFactory.bleAdapterCreator = bleAdapterCreator; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/BleException.java b/android/src/main/java/com/bleplx/adapter/BleException.java new file mode 100644 index 00000000..3f470b32 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/BleException.java @@ -0,0 +1,4 @@ +package com.bleplx.adapter; + +public interface BleException { +} diff --git a/android/src/main/java/com/bleplx/adapter/BleModule.java b/android/src/main/java/com/bleplx/adapter/BleModule.java new file mode 100755 index 00000000..4c2cf8f5 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/BleModule.java @@ -0,0 +1,1565 @@ +package com.bleplx.adapter; + +import static com.bleplx.adapter.utils.Constants.BluetoothState; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.ParcelUuid; +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bleplx.adapter.errors.BleError; +import com.bleplx.adapter.errors.BleErrorCode; +import com.bleplx.adapter.errors.BleErrorUtils; +import com.bleplx.adapter.errors.ErrorConverter; +import com.bleplx.adapter.exceptions.CannotMonitorCharacteristicException; +import com.bleplx.adapter.utils.Base64Converter; +import com.bleplx.adapter.utils.Constants; +import com.bleplx.adapter.utils.DisposableMap; +import com.bleplx.adapter.utils.IdGenerator; +import com.bleplx.adapter.utils.LogLevel; +import com.bleplx.adapter.utils.RefreshGattCustomOperation; +import com.bleplx.adapter.utils.SafeExecutor; +import com.bleplx.adapter.utils.ServiceFactory; +import com.bleplx.adapter.utils.UUIDConverter; +import com.bleplx.adapter.utils.mapper.RxBleDeviceToDeviceMapper; +import com.bleplx.adapter.utils.mapper.RxScanResultToScanResultMapper; +import com.polidea.rxandroidble2.NotificationSetupMode; +import com.polidea.rxandroidble2.RxBleAdapterStateObservable; +import com.polidea.rxandroidble2.RxBleClient; +import com.polidea.rxandroidble2.RxBleConnection; +import com.polidea.rxandroidble2.RxBleDevice; +import com.polidea.rxandroidble2.internal.RxBleLog; +import com.polidea.rxandroidble2.scan.ScanFilter; +import com.polidea.rxandroidble2.scan.ScanSettings; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import io.reactivex.BackpressureStrategy; +import io.reactivex.Observable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Action; +import io.reactivex.schedulers.Schedulers; + +public class BleModule implements BleAdapter { + + private final ErrorConverter errorConverter = new ErrorConverter(); + + @Nullable + private RxBleClient rxBleClient; + + private final HashMap discoveredDevices = new HashMap<>(); + + private final HashMap connectedDevices = new HashMap<>(); + + private final HashMap activeConnections = new HashMap<>(); + + private final SparseArray discoveredServices = new SparseArray<>(); + + private final SparseArray discoveredCharacteristics = new SparseArray<>(); + + private final SparseArray discoveredDescriptors = new SparseArray<>(); + + private final DisposableMap pendingTransactions = new DisposableMap(); + + private final DisposableMap connectingDevices = new DisposableMap(); + + private final BluetoothManager bluetoothManager; + + private final BluetoothAdapter bluetoothAdapter; + + private final Context context; + + @Nullable + private Disposable scanSubscription; + + @Nullable + private Disposable adapterStateChangesSubscription; + + private final RxBleDeviceToDeviceMapper rxBleDeviceToDeviceMapper = new RxBleDeviceToDeviceMapper(); + + private final RxScanResultToScanResultMapper rxScanResultToScanResultMapper = new RxScanResultToScanResultMapper(); + + private final ServiceFactory serviceFactory = new ServiceFactory(); + + private int currentLogLevel = RxBleLog.NONE; + + public BleModule(Context context) { + this.context = context; + bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); + bluetoothAdapter = bluetoothManager.getAdapter(); + } + + @Override + public void createClient(String restoreStateIdentifier, + OnEventCallback onAdapterStateChangeCallback, + OnEventCallback onStateRestored) { + rxBleClient = RxBleClient.create(context); + adapterStateChangesSubscription = monitorAdapterStateChanges(context, onAdapterStateChangeCallback); + + // We need to send signal that BLE Module starts without restored state + if (restoreStateIdentifier != null) { + onStateRestored.onEvent(null); + } + } + + @Override + public void destroyClient() { + if (adapterStateChangesSubscription != null) { + adapterStateChangesSubscription.dispose(); + adapterStateChangesSubscription = null; + } + if (scanSubscription != null && !scanSubscription.isDisposed()) { + scanSubscription.dispose(); + scanSubscription = null; + } + pendingTransactions.removeAllSubscriptions(); + connectingDevices.removeAllSubscriptions(); + + discoveredServices.clear(); + discoveredCharacteristics.clear(); + discoveredDescriptors.clear(); + connectedDevices.clear(); + activeConnections.clear(); + discoveredDevices.clear(); + + rxBleClient = null; + IdGenerator.clear(); + } + + + @Override + public void enable(final String transactionId, + final OnSuccessCallback onSuccessCallback, + final OnErrorCallback onErrorCallback) { + changeAdapterState( + RxBleAdapterStateObservable.BleAdapterState.STATE_ON, + transactionId, + onSuccessCallback, + onErrorCallback); + } + + @Override + public void disable(final String transactionId, + final OnSuccessCallback onSuccessCallback, + final OnErrorCallback onErrorCallback) { + changeAdapterState( + RxBleAdapterStateObservable.BleAdapterState.STATE_OFF, + transactionId, + onSuccessCallback, + onErrorCallback); + } + + @BluetoothState + @Override + public String getCurrentState() { + if (!supportsBluetoothLowEnergy()) return BluetoothState.UNSUPPORTED; + if (bluetoothManager == null) return BluetoothState.POWERED_OFF; + return mapNativeAdapterStateToLocalBluetoothState(bluetoothAdapter.getState()); + } + + @Override + public void startDeviceScan(String[] filteredUUIDs, + int scanMode, + int callbackType, + OnEventCallback onEventCallback, + OnErrorCallback onErrorCallback) { + UUID[] uuids = null; + + if (filteredUUIDs != null) { + uuids = UUIDConverter.convert(filteredUUIDs); + if (uuids == null) { + onErrorCallback.onError(BleErrorUtils.invalidIdentifiers(filteredUUIDs)); + return; + } + } + + safeStartDeviceScan(uuids, scanMode, callbackType, onEventCallback, onErrorCallback); + } + + @Override + public void stopDeviceScan() { + if (scanSubscription != null) { + scanSubscription.dispose(); + scanSubscription = null; + } + } + + @Override + public void requestConnectionPriorityForDevice(String deviceIdentifier, + int connectionPriority, + final String transactionId, + final OnSuccessCallback onSuccessCallback, + final OnErrorCallback onErrorCallback) { + final Device device; + try { + device = getDeviceById(deviceIdentifier); + } catch (BleError error) { + onErrorCallback.onError(error); + return; + } + + final RxBleConnection connection = getConnectionOrEmitError(device.getId(), onErrorCallback); + if (connection == null) { + return; + } + + final SafeExecutor safeExecutor = new SafeExecutor<>(onSuccessCallback, onErrorCallback); + + final Disposable subscription = connection + .requestConnectionPriority(connectionPriority, 1, TimeUnit.MILLISECONDS) + .doOnDispose(() -> { + safeExecutor.error(BleErrorUtils.cancelled()); + pendingTransactions.removeSubscription(transactionId); + }).subscribe((Action) () -> { + safeExecutor.success(device); + pendingTransactions.removeSubscription(transactionId); + }, throwable -> { + safeExecutor.error(errorConverter.toError(throwable)); + pendingTransactions.removeSubscription(transactionId); + }); + + pendingTransactions.replaceSubscription(transactionId, subscription); + } + + @Override + public void readRSSIForDevice(String deviceIdentifier, + final String transactionId, + final OnSuccessCallback onSuccessCallback, + final OnErrorCallback onErrorCallback) { + final Device device; + try { + device = getDeviceById(deviceIdentifier); + } catch (BleError error) { + onErrorCallback.onError(error); + return; + } + final RxBleConnection connection = getConnectionOrEmitError(device.getId(), onErrorCallback); + if (connection == null) { + return; + } + + final SafeExecutor safeExecutor = new SafeExecutor<>(onSuccessCallback, onErrorCallback); + + final Disposable subscription = connection + .readRssi() + .doOnDispose(() -> { + safeExecutor.error(BleErrorUtils.cancelled()); + pendingTransactions.removeSubscription(transactionId); + }) + .subscribe(rssi -> { + device.setRssi(rssi); + safeExecutor.success(device); + pendingTransactions.removeSubscription(transactionId); + }, error -> { + safeExecutor.error(errorConverter.toError(error)); + pendingTransactions.removeSubscription(transactionId); + }); + + pendingTransactions.replaceSubscription(transactionId, subscription); + } + + @Override + public void requestMTUForDevice(String deviceIdentifier, int mtu, + final String transactionId, + final OnSuccessCallback onSuccessCallback, + final OnErrorCallback onErrorCallback) { + final Device device; + try { + device = getDeviceById(deviceIdentifier); + } catch (BleError error) { + onErrorCallback.onError(error); + return; + } + + final RxBleConnection connection = getConnectionOrEmitError(device.getId(), onErrorCallback); + if (connection == null) { + return; + } + + final SafeExecutor safeExecutor = new SafeExecutor<>(onSuccessCallback, onErrorCallback); + + final Disposable subscription = connection + .requestMtu(mtu) + .doOnDispose(() -> { + safeExecutor.error(BleErrorUtils.cancelled()); + pendingTransactions.removeSubscription(transactionId); + }).subscribe(outputMtu -> { + device.setMtu(outputMtu); + safeExecutor.success(device); + pendingTransactions.removeSubscription(transactionId); + }, error -> { + safeExecutor.error(errorConverter.toError(error)); + pendingTransactions.removeSubscription(transactionId); + }); + + pendingTransactions.replaceSubscription(transactionId, subscription); + } + + @Override + public void getKnownDevices(String[] deviceIdentifiers, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + if (rxBleClient == null) { + onErrorCallback.onError(new BleError(BleErrorCode.BluetoothManagerDestroyed, "BleManager not created when tried to get known devices", null)); + return; + } + + List knownDevices = new ArrayList<>(); + for (final String deviceId : deviceIdentifiers) { + if (deviceId == null) { + onErrorCallback.onError(BleErrorUtils.invalidIdentifiers(deviceIdentifiers)); + return; + } + + final Device device = discoveredDevices.get(deviceId); + if (device != null) { + knownDevices.add(device); + } + } + + onSuccessCallback.onSuccess(knownDevices.toArray(new Device[knownDevices.size()])); + } + + @Override + public void getConnectedDevices(String[] serviceUUIDs, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + if (rxBleClient == null) { + onErrorCallback.onError(new BleError(BleErrorCode.BluetoothManagerDestroyed, "BleManager not created when tried to get connected devices", null)); + return; + } + + if (serviceUUIDs.length == 0) { + onSuccessCallback.onSuccess(new Device[0]); + return; + } + + UUID[] uuids = new UUID[serviceUUIDs.length]; + for (int i = 0; i < serviceUUIDs.length; i++) { + UUID uuid = UUIDConverter.convert(serviceUUIDs[i]); + + if (uuid == null) { + onErrorCallback.onError(BleErrorUtils.invalidIdentifiers(serviceUUIDs)); + return; + } + + uuids[i] = uuid; + } + + List localConnectedDevices = new ArrayList<>(); + for (Device device : connectedDevices.values()) { + for (UUID uuid : uuids) { + if (device.getServiceByUUID(uuid) != null) { + localConnectedDevices.add(device); + break; + } + } + } + + onSuccessCallback.onSuccess(localConnectedDevices.toArray(new Device[localConnectedDevices.size()])); + + } + + @Override + public void connectToDevice(String deviceIdentifier, + ConnectionOptions connectionOptions, + OnSuccessCallback onSuccessCallback, + OnEventCallback onConnectionStateChangedCallback, + OnErrorCallback onErrorCallback) { + if (rxBleClient == null) { + onErrorCallback.onError(new BleError(BleErrorCode.BluetoothManagerDestroyed, "BleManager not created when tried to connect to device", null)); + return; + } + + final RxBleDevice device = rxBleClient.getBleDevice(deviceIdentifier); + if (device == null) { + onErrorCallback.onError(BleErrorUtils.deviceNotFound(deviceIdentifier)); + return; + } + + safeConnectToDevice( + device, + connectionOptions.getAutoConnect(), + connectionOptions.getRequestMTU(), + connectionOptions.getRefreshGattMoment(), + connectionOptions.getTimeoutInMillis(), + connectionOptions.getConnectionPriority(), + onSuccessCallback, onConnectionStateChangedCallback, onErrorCallback); + } + + @Override + public void cancelDeviceConnection(String deviceIdentifier, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + if (rxBleClient == null) { + onErrorCallback.onError(new BleError(BleErrorCode.BluetoothManagerDestroyed, "BleManager not created when tried to cancel device connection", null)); + return; + } + + final RxBleDevice device = rxBleClient.getBleDevice(deviceIdentifier); + + if (connectingDevices.removeSubscription(deviceIdentifier) && device != null) { + onSuccessCallback.onSuccess(rxBleDeviceToDeviceMapper.map(device, null)); + } else { + if (device == null) { + onErrorCallback.onError(BleErrorUtils.deviceNotFound(deviceIdentifier)); + } else { + onErrorCallback.onError(BleErrorUtils.deviceNotConnected(deviceIdentifier)); + } + } + } + + @Override + public void isDeviceConnected(String deviceIdentifier, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + if (rxBleClient == null) { + onErrorCallback.onError(new BleError(BleErrorCode.BluetoothManagerDestroyed, "BleManager not created when tried to check if device is connected", null)); + return; + } + + try { + final RxBleDevice device = rxBleClient.getBleDevice(deviceIdentifier); + if (device == null) { + onErrorCallback.onError(BleErrorUtils.deviceNotFound(deviceIdentifier)); + return; + } + + boolean connected = device.getConnectionState() + .equals(RxBleConnection.RxBleConnectionState.CONNECTED); + onSuccessCallback.onSuccess(connected); + } catch (Exception e) { + onErrorCallback.onError(errorConverter.toError(e)); + } + } + + @Override + public void discoverAllServicesAndCharacteristicsForDevice(String deviceIdentifier, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + final Device device; + try { + device = getDeviceById(deviceIdentifier); + } catch (BleError error) { + onErrorCallback.onError(error); + return; + } + + safeDiscoverAllServicesAndCharacteristicsForDevice(device, transactionId, onSuccessCallback, onErrorCallback); + } + + @Override + public List getServicesForDevice(String deviceIdentifier) throws BleError { + final Device device = getDeviceById(deviceIdentifier); + final List services = device.getServices(); + if (services == null) { + throw BleErrorUtils.deviceServicesNotDiscovered(device.getId()); + } + return services; + } + + @Override + public List getCharacteristicsForDevice(String deviceIdentifier, + String serviceUUID) throws BleError { + final UUID convertedServiceUUID = UUIDConverter.convert(serviceUUID); + if (convertedServiceUUID == null) { + throw BleErrorUtils.invalidIdentifiers(serviceUUID); + } + + final Device device = getDeviceById(deviceIdentifier); + + final Service service = device.getServiceByUUID(convertedServiceUUID); + if (service == null) { + throw BleErrorUtils.serviceNotFound(serviceUUID); + } + + return service.getCharacteristics(); + } + + @Override + public List getCharacteristicsForService(int serviceIdentifier) throws BleError { + Service service = discoveredServices.get(serviceIdentifier); + if (service == null) { + throw BleErrorUtils.serviceNotFound(Integer.toString(serviceIdentifier)); + } + return service.getCharacteristics(); + } + + @Override + public List descriptorsForDevice(final String deviceIdentifier, + final String serviceUUID, + final String characteristicUUID) throws BleError { + final UUID[] uuids = UUIDConverter.convert(serviceUUID, characteristicUUID); + if (uuids == null) { + throw BleErrorUtils.invalidIdentifiers(serviceUUID, characteristicUUID); + } + + Device device = getDeviceById(deviceIdentifier); + + final Service service = device.getServiceByUUID(uuids[0]); + if (service == null) { + throw BleErrorUtils.serviceNotFound(serviceUUID); + } + + final Characteristic characteristic = service.getCharacteristicByUUID(uuids[1]); + if (characteristic == null) { + throw BleErrorUtils.characteristicNotFound(characteristicUUID); + } + + return characteristic.getDescriptors(); + } + + @Override + public List descriptorsForService(final int serviceIdentifier, + final String characteristicUUID) throws BleError { + final UUID uuid = UUIDConverter.convert(characteristicUUID); + if (uuid == null) { + throw BleErrorUtils.invalidIdentifiers(characteristicUUID); + } + + Service service = discoveredServices.get(serviceIdentifier); + if (service == null) { + throw BleErrorUtils.serviceNotFound(Integer.toString(serviceIdentifier)); + } + + final Characteristic characteristic = service.getCharacteristicByUUID(uuid); + if (characteristic == null) { + throw BleErrorUtils.characteristicNotFound(characteristicUUID); + } + + return characteristic.getDescriptors(); + } + + @Override + public List descriptorsForCharacteristic(final int characteristicIdentifier) throws BleError { + Characteristic characteristic = discoveredCharacteristics.get(characteristicIdentifier); + if (characteristic == null) { + throw BleErrorUtils.characteristicNotFound(Integer.toString(characteristicIdentifier)); + } + + return characteristic.getDescriptors(); + } + + @Override + public void readCharacteristicForDevice(String deviceIdentifier, + String serviceUUID, + String characteristicUUID, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + final Characteristic characteristic = getCharacteristicOrEmitError( + deviceIdentifier, serviceUUID, characteristicUUID, onErrorCallback); + if (characteristic == null) { + return; + } + + safeReadCharacteristicForDevice(characteristic, transactionId, onSuccessCallback, onErrorCallback); + } + + @Override + public void readCharacteristicForService(int serviceIdentifier, + String characteristicUUID, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + final Characteristic characteristic = getCharacteristicOrEmitError( + serviceIdentifier, characteristicUUID, onErrorCallback); + if (characteristic == null) { + return; + } + + safeReadCharacteristicForDevice(characteristic, transactionId, onSuccessCallback, onErrorCallback); + } + + @Override + public void readCharacteristic(int characteristicIdentifier, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + final Characteristic characteristic = getCharacteristicOrEmitError(characteristicIdentifier, onErrorCallback); + if (characteristic == null) { + return; + } + + safeReadCharacteristicForDevice(characteristic, transactionId, onSuccessCallback, onErrorCallback); + } + + @Override + public void writeCharacteristicForDevice(String deviceIdentifier, + String serviceUUID, + String characteristicUUID, + String valueBase64, + boolean withResponse, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + final Characteristic characteristic = getCharacteristicOrEmitError( + deviceIdentifier, serviceUUID, characteristicUUID, onErrorCallback); + if (characteristic == null) { + return; + } + + writeCharacteristicWithValue( + characteristic, + valueBase64, + withResponse, + transactionId, + onSuccessCallback, + onErrorCallback); + } + + @Override + public void writeCharacteristicForService(int serviceIdentifier, + String characteristicUUID, + String valueBase64, + boolean withResponse, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + final Characteristic characteristic = getCharacteristicOrEmitError( + serviceIdentifier, characteristicUUID, onErrorCallback); + if (characteristic == null) { + return; + } + + writeCharacteristicWithValue( + characteristic, + valueBase64, + withResponse, + transactionId, + onSuccessCallback, + onErrorCallback); + } + + @Override + public void writeCharacteristic(int characteristicIdentifier, + String valueBase64, + boolean withResponse, + String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + final Characteristic characteristic = getCharacteristicOrEmitError(characteristicIdentifier, onErrorCallback); + if (characteristic == null) { + return; + } + + writeCharacteristicWithValue( + characteristic, + valueBase64, + withResponse, + transactionId, + onSuccessCallback, + onErrorCallback + ); + } + + @Override + public void monitorCharacteristicForDevice(String deviceIdentifier, + String serviceUUID, + String characteristicUUID, + String transactionId, + OnEventCallback onEventCallback, + OnErrorCallback onErrorCallback) { + final Characteristic characteristic = getCharacteristicOrEmitError( + deviceIdentifier, serviceUUID, characteristicUUID, onErrorCallback); + if (characteristic == null) { + return; + } + + safeMonitorCharacteristicForDevice(characteristic, transactionId, onEventCallback, onErrorCallback); + } + + @Override + public void monitorCharacteristicForService(int serviceIdentifier, + String characteristicUUID, + String transactionId, + OnEventCallback onEventCallback, + OnErrorCallback onErrorCallback) { + final Characteristic characteristic = getCharacteristicOrEmitError( + serviceIdentifier, characteristicUUID, onErrorCallback); + if (characteristic == null) { + return; + } + + safeMonitorCharacteristicForDevice(characteristic, transactionId, onEventCallback, onErrorCallback); + } + + @Override + public void monitorCharacteristic(int characteristicIdentifier, String transactionId, + OnEventCallback onEventCallback, + OnErrorCallback onErrorCallback) { + final Characteristic characteristic = getCharacteristicOrEmitError(characteristicIdentifier, onErrorCallback); + if (characteristic == null) { + return; + } + + safeMonitorCharacteristicForDevice(characteristic, transactionId, onEventCallback, onErrorCallback); + } + + @Override + public void readDescriptorForDevice(final String deviceId, + final String serviceUUID, + final String characteristicUUID, + final String descriptorUUID, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback) { + + try { + Descriptor descriptor = getDescriptor(deviceId, serviceUUID, characteristicUUID, descriptorUUID); + safeReadDescriptorForDevice(descriptor, transactionId, successCallback, errorCallback); + } catch (BleError error) { + errorCallback.onError(error); + } + } + + @Override + public void readDescriptorForService(final int serviceIdentifier, + final String characteristicUUID, + final String descriptorUUID, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback) { + try { + Descriptor descriptor = getDescriptor(serviceIdentifier, characteristicUUID, descriptorUUID); + safeReadDescriptorForDevice(descriptor, transactionId, successCallback, errorCallback); + } catch (BleError error) { + errorCallback.onError(error); + } + } + + @Override + public void readDescriptorForCharacteristic(final int characteristicIdentifier, + final String descriptorUUID, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback) { + + try { + Descriptor descriptor = getDescriptor(characteristicIdentifier, descriptorUUID); + safeReadDescriptorForDevice(descriptor, transactionId, successCallback, errorCallback); + } catch (BleError error) { + errorCallback.onError(error); + } + } + + @Override + public void readDescriptor(final int descriptorIdentifier, + final String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + try { + Descriptor descriptor = getDescriptor(descriptorIdentifier); + safeReadDescriptorForDevice(descriptor, transactionId, onSuccessCallback, onErrorCallback); + } catch (BleError error) { + onErrorCallback.onError(error); + } + } + + private void safeReadDescriptorForDevice(final Descriptor descriptor, + final String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + final RxBleConnection connection = getConnectionOrEmitError(descriptor.getDeviceId(), onErrorCallback); + if (connection == null) { + return; + } + + final SafeExecutor safeExecutor = new SafeExecutor<>(onSuccessCallback, onErrorCallback); + + final Disposable subscription = connection + .readDescriptor(descriptor.getNativeDescriptor()) + .doOnDispose(() -> { + safeExecutor.error(BleErrorUtils.cancelled()); + pendingTransactions.removeSubscription(transactionId); + }) + .subscribe(bytes -> { + descriptor.logValue("Read from", bytes); + descriptor.setValue(bytes); + safeExecutor.success(new Descriptor(descriptor)); + pendingTransactions.removeSubscription(transactionId); + }, error -> { + safeExecutor.error(errorConverter.toError(error)); + pendingTransactions.removeSubscription(transactionId); + }); + + pendingTransactions.replaceSubscription(transactionId, subscription); + } + + @Override + public void writeDescriptorForDevice(final String deviceId, + final String serviceUUID, + final String characteristicUUID, + final String descriptorUUID, + final String valueBase64, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback) { + try { + Descriptor descriptor = getDescriptor(deviceId, serviceUUID, characteristicUUID, descriptorUUID); + safeWriteDescriptorForDevice( + descriptor, + valueBase64, + transactionId, + successCallback, + errorCallback); + } catch (BleError error) { + errorCallback.onError(error); + } + } + + @Override + public void writeDescriptorForService(final int serviceIdentifier, + final String characteristicUUID, + final String descriptorUUID, + final String valueBase64, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback) { + try { + Descriptor descriptor = getDescriptor(serviceIdentifier, characteristicUUID, descriptorUUID); + safeWriteDescriptorForDevice( + descriptor, + valueBase64, + transactionId, + successCallback, + errorCallback); + } catch (BleError error) { + errorCallback.onError(error); + } + } + + @Override + public void writeDescriptorForCharacteristic(final int characteristicIdentifier, + final String descriptorUUID, + final String valueBase64, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback) { + try { + Descriptor descriptor = getDescriptor(characteristicIdentifier, descriptorUUID); + safeWriteDescriptorForDevice( + descriptor, + valueBase64, + transactionId, + successCallback, + errorCallback); + } catch (BleError error) { + errorCallback.onError(error); + } + } + + @Override + public void writeDescriptor(final int descriptorIdentifier, + final String valueBase64, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback) { + try { + Descriptor descriptor = getDescriptor(descriptorIdentifier); + safeWriteDescriptorForDevice( + descriptor, + valueBase64, + transactionId, + successCallback, + errorCallback); + } catch (BleError error) { + errorCallback.onError(error); + } + + } + + private void safeWriteDescriptorForDevice(final Descriptor descriptor, + final String valueBase64, + final String transactionId, + OnSuccessCallback successCallback, + OnErrorCallback errorCallback) { + BluetoothGattDescriptor nativeDescriptor = descriptor.getNativeDescriptor(); + + if (nativeDescriptor.getUuid().equals(Constants.CLIENT_CHARACTERISTIC_CONFIG_UUID)) { + errorCallback.onError(BleErrorUtils.descriptorWriteNotAllowed(UUIDConverter.fromUUID(nativeDescriptor.getUuid()))); + return; + } + + final RxBleConnection connection = getConnectionOrEmitError(descriptor.getDeviceId(), errorCallback); + if (connection == null) { + return; + } + + final byte[] value; + try { + value = Base64Converter.decode(valueBase64); + } catch (Throwable e) { + String uuid = UUIDConverter.fromUUID(nativeDescriptor.getUuid()); + errorCallback.onError(BleErrorUtils.invalidWriteDataForDescriptor(valueBase64, uuid)); + return; + } + + final SafeExecutor safeExecutor = new SafeExecutor<>(successCallback, errorCallback); + + final Disposable subscription = connection + .writeDescriptor(nativeDescriptor, value) + .doOnDispose(() -> { + safeExecutor.error(BleErrorUtils.cancelled()); + pendingTransactions.removeSubscription(transactionId); + }) + .subscribe(() -> { + descriptor.logValue("Write to", value); + descriptor.setValue(value); + safeExecutor.success(new Descriptor(descriptor)); + pendingTransactions.removeSubscription(transactionId); + }, error -> { + safeExecutor.error(errorConverter.toError(error)); + pendingTransactions.removeSubscription(transactionId); + }); + + pendingTransactions.replaceSubscription(transactionId, subscription); + } + + // Mark: Descriptors getters ------------------------------------------------------------------- + + private Descriptor getDescriptor(@NonNull final String deviceId, + @NonNull final String serviceUUID, + @NonNull final String characteristicUUID, + @NonNull final String descriptorUUID) throws BleError { + final UUID[] UUIDs = UUIDConverter.convert(serviceUUID, characteristicUUID, descriptorUUID); + if (UUIDs == null) { + throw BleErrorUtils.invalidIdentifiers(serviceUUID, characteristicUUID, descriptorUUID); + } + + final Device device = connectedDevices.get(deviceId); + if (device == null) { + throw BleErrorUtils.deviceNotConnected(deviceId); + } + + final Service service = device.getServiceByUUID(UUIDs[0]); + if (service == null) { + throw BleErrorUtils.serviceNotFound(serviceUUID); + } + + final Characteristic characteristic = service.getCharacteristicByUUID(UUIDs[1]); + if (characteristic == null) { + throw BleErrorUtils.characteristicNotFound(characteristicUUID); + } + + final Descriptor descriptor = characteristic.getDescriptorByUUID(UUIDs[2]); + if (descriptor == null) { + throw BleErrorUtils.descriptorNotFound(descriptorUUID); + } + + return descriptor; + } + + private Descriptor getDescriptor(final int serviceIdentifier, + @NonNull final String characteristicUUID, + @NonNull final String descriptorUUID) throws BleError { + final UUID[] UUIDs = UUIDConverter.convert(characteristicUUID, descriptorUUID); + if (UUIDs == null) { + throw BleErrorUtils.invalidIdentifiers(characteristicUUID, descriptorUUID); + } + + final Service service = discoveredServices.get(serviceIdentifier); + if (service == null) { + throw BleErrorUtils.serviceNotFound(Integer.toString(serviceIdentifier)); + } + + final Characteristic characteristic = service.getCharacteristicByUUID(UUIDs[0]); + if (characteristic == null) { + throw BleErrorUtils.characteristicNotFound(characteristicUUID); + } + + final Descriptor descriptor = characteristic.getDescriptorByUUID(UUIDs[1]); + if (descriptor == null) { + throw BleErrorUtils.descriptorNotFound(descriptorUUID); + } + + return descriptor; + } + + private Descriptor getDescriptor(final int characteristicIdentifier, + @NonNull final String descriptorUUID) throws BleError { + final UUID uuid = UUIDConverter.convert(descriptorUUID); + if (uuid == null) { + throw BleErrorUtils.invalidIdentifiers(descriptorUUID); + } + + final Characteristic characteristic = discoveredCharacteristics.get(characteristicIdentifier); + if (characteristic == null) { + throw BleErrorUtils.characteristicNotFound(Integer.toString(characteristicIdentifier)); + } + + final Descriptor descriptor = characteristic.getDescriptorByUUID(uuid); + if (descriptor == null) { + throw BleErrorUtils.descriptorNotFound(descriptorUUID); + } + + return descriptor; + } + + private Descriptor getDescriptor(final int descriptorIdentifier) throws BleError { + + final Descriptor descriptor = discoveredDescriptors.get(descriptorIdentifier); + if (descriptor == null) { + throw BleErrorUtils.descriptorNotFound(Integer.toString(descriptorIdentifier)); + } + + return descriptor; + } + + @Override + public void cancelTransaction(String transactionId) { + pendingTransactions.removeSubscription(transactionId); + } + + public void setLogLevel(String logLevel) { + currentLogLevel = LogLevel.toLogLevel(logLevel); + RxBleLog.setLogLevel(currentLogLevel); + } + + @Override + public String getLogLevel() { + return LogLevel.fromLogLevel(currentLogLevel); + } + + private Disposable monitorAdapterStateChanges(Context context, + final OnEventCallback onAdapterStateChangeCallback) { + if (!supportsBluetoothLowEnergy()) { + return null; + } + + return new RxBleAdapterStateObservable(context) + .map(this::mapRxBleAdapterStateToLocalBluetoothState) + .subscribe(onAdapterStateChangeCallback::onEvent); + } + + private boolean supportsBluetoothLowEnergy() { + return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE); + } + + @BluetoothState + private String mapRxBleAdapterStateToLocalBluetoothState( + RxBleAdapterStateObservable.BleAdapterState rxBleAdapterState + ) { + if (rxBleAdapterState == RxBleAdapterStateObservable.BleAdapterState.STATE_ON) { + return BluetoothState.POWERED_ON; + } else if (rxBleAdapterState == RxBleAdapterStateObservable.BleAdapterState.STATE_OFF) { + return BluetoothState.POWERED_OFF; + } else { + return BluetoothState.RESETTING; + } + } + + @SuppressLint("MissingPermission") + private void changeAdapterState(final RxBleAdapterStateObservable.BleAdapterState desiredAdapterState, + final String transactionId, + final OnSuccessCallback onSuccessCallback, + final OnErrorCallback onErrorCallback) { + if (bluetoothManager == null) { + onErrorCallback.onError(new BleError(BleErrorCode.BluetoothStateChangeFailed, "BluetoothManager is null", null)); + return; + } + + final SafeExecutor safeExecutor = new SafeExecutor<>(onSuccessCallback, onErrorCallback); + + final Disposable subscription = new RxBleAdapterStateObservable(context) + .takeUntil(actualAdapterState -> desiredAdapterState == actualAdapterState) + .firstOrError() + .doOnDispose(() -> { + safeExecutor.error(BleErrorUtils.cancelled()); + pendingTransactions.removeSubscription(transactionId); + }) + .subscribe(state -> { + safeExecutor.success(null); + pendingTransactions.removeSubscription(transactionId); + }, error -> { + safeExecutor.error(errorConverter.toError(error)); + pendingTransactions.removeSubscription(transactionId); + }); + + + boolean desiredAndInitialStateAreSame = false; + try { + if (desiredAdapterState == RxBleAdapterStateObservable.BleAdapterState.STATE_ON) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (context instanceof Activity) { + ((Activity) context).startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), 1); + desiredAndInitialStateAreSame = true; + } + } else { + desiredAndInitialStateAreSame = !bluetoothAdapter.enable(); + } + } else { + desiredAndInitialStateAreSame = !bluetoothAdapter.disable(); + } + } catch (SecurityException e) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + onErrorCallback.onError(new BleError( + BleErrorCode.BluetoothUnauthorized, + "Method requires BLUETOOTH_CONNECT permission", + null) + ); + } else { + onErrorCallback.onError(new BleError( + BleErrorCode.BluetoothUnauthorized, + "Method requires BLUETOOTH_ADMIN permission", + null) + ); + } + } catch (Exception e) { + onErrorCallback.onError(new BleError( + BleErrorCode.BluetoothStateChangeFailed, + String.format("Couldn't set bluetooth adapter state because of: %s", e.getMessage() != null ? e.getMessage() : "unknown error"), + null) + ); + } + if (desiredAndInitialStateAreSame) { + subscription.dispose(); + onErrorCallback.onError(new BleError( + BleErrorCode.BluetoothStateChangeFailed, + String.format("Couldn't set bluetooth adapter state to %s", desiredAdapterState.toString()), + null)); + } else { + pendingTransactions.replaceSubscription(transactionId, subscription); + } + } + + @BluetoothState + private String mapNativeAdapterStateToLocalBluetoothState(int adapterState) { + switch (adapterState) { + case BluetoothAdapter.STATE_OFF: + return BluetoothState.POWERED_OFF; + case BluetoothAdapter.STATE_ON: + return BluetoothState.POWERED_ON; + case BluetoothAdapter.STATE_TURNING_OFF: + // fallthrough + case BluetoothAdapter.STATE_TURNING_ON: + return BluetoothState.RESETTING; + default: + return BluetoothState.UNKNOWN; + } + } + + private void safeStartDeviceScan(final UUID[] uuids, + final int scanMode, + int callbackType, + final OnEventCallback onEventCallback, + final OnErrorCallback onErrorCallback) { + if (rxBleClient == null) { + onErrorCallback.onError(new BleError(BleErrorCode.BluetoothManagerDestroyed, "BleManager not created when tried to start device scan", null)); + return; + } + + ScanSettings scanSettings = new ScanSettings.Builder() + .setScanMode(scanMode) + .setCallbackType(callbackType) + .build(); + + int length = uuids == null ? 0 : uuids.length; + ScanFilter[] filters = new ScanFilter[length]; + for (int i = 0; i < length; i++) { + filters[i] = new ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(uuids[i].toString())).build(); + } + + scanSubscription = rxBleClient + .scanBleDevices(scanSettings, filters) + .subscribe(scanResult -> { + String deviceId = scanResult.getBleDevice().getMacAddress(); + if (!discoveredDevices.containsKey(deviceId)) { + discoveredDevices.put(deviceId, rxBleDeviceToDeviceMapper.map(scanResult.getBleDevice(), null)); + } + onEventCallback.onEvent(rxScanResultToScanResultMapper.map(scanResult)); + }, throwable -> onErrorCallback.onError(errorConverter.toError(throwable))); + } + + @NonNull + private Device getDeviceById(@NonNull final String deviceId) throws BleError { + final Device device = connectedDevices.get(deviceId); + if (device == null) { + throw BleErrorUtils.deviceNotConnected(deviceId); + } + return device; + } + + @Nullable + private RxBleConnection getConnectionOrEmitError(@NonNull final String deviceId, + @NonNull OnErrorCallback onErrorCallback) { + final RxBleConnection connection = activeConnections.get(deviceId); + if (connection == null) { + onErrorCallback.onError(BleErrorUtils.deviceNotConnected(deviceId)); + return null; + } + return connection; + } + + private void safeConnectToDevice(final RxBleDevice device, + final boolean autoConnect, + final int requestMtu, + final RefreshGattMoment refreshGattMoment, + final Long timeout, + final int connectionPriority, + final OnSuccessCallback onSuccessCallback, + final OnEventCallback onConnectionStateChangedCallback, + final OnErrorCallback onErrorCallback) { + + final SafeExecutor safeExecutor = new SafeExecutor<>(onSuccessCallback, onErrorCallback); + + Observable connect = device + .establishConnection(autoConnect) + .doOnSubscribe(disposable -> onConnectionStateChangedCallback.onEvent(ConnectionState.CONNECTING)) + .doOnDispose(() -> { + safeExecutor.error(BleErrorUtils.cancelled()); + onDeviceDisconnected(device); + onConnectionStateChangedCallback.onEvent(ConnectionState.DISCONNECTED); + }); + + if (refreshGattMoment == RefreshGattMoment.ON_CONNECTED) { + connect = connect.flatMap(rxBleConnection -> rxBleConnection + .queue(new RefreshGattCustomOperation()) + .map(refreshGattSuccess -> rxBleConnection)); + } + + if (connectionPriority > 0) { + connect = connect.flatMap(rxBleConnection -> rxBleConnection + .requestConnectionPriority(connectionPriority, 1, TimeUnit.MILLISECONDS) + .andThen(Observable.just(rxBleConnection)) + ); + } + + if (requestMtu > 0) { + connect = connect.flatMap(rxBleConnection -> + rxBleConnection.requestMtu(requestMtu) + .map(integer -> rxBleConnection) + .toObservable() + ); + } + + if (timeout != null) { + connect = connect.timeout(timeout, TimeUnit.MILLISECONDS); + } + + + final Disposable subscription = connect + .subscribe(rxBleConnection -> { + Device localDevice = rxBleDeviceToDeviceMapper.map(device, rxBleConnection); + onConnectionStateChangedCallback.onEvent(ConnectionState.CONNECTED); + cleanServicesAndCharacteristicsForDevice(localDevice); + connectedDevices.put(device.getMacAddress(), localDevice); + activeConnections.put(device.getMacAddress(), rxBleConnection); + safeExecutor.success(localDevice); + }, error -> { + BleError bleError = errorConverter.toError(error); + safeExecutor.error(bleError); + onDeviceDisconnected(device); + }); + + connectingDevices.replaceSubscription(device.getMacAddress(), subscription); + } + + private void onDeviceDisconnected(RxBleDevice rxDevice) { + activeConnections.remove(rxDevice.getMacAddress()); + Device device = connectedDevices.remove(rxDevice.getMacAddress()); + if (device == null) { + return; + } + + cleanServicesAndCharacteristicsForDevice(device); + connectingDevices.removeSubscription(device.getId()); + } + + private void safeDiscoverAllServicesAndCharacteristicsForDevice(final Device device, + final String transactionId, + final OnSuccessCallback onSuccessCallback, + final OnErrorCallback onErrorCallback) { + final RxBleConnection connection = getConnectionOrEmitError(device.getId(), onErrorCallback); + if (connection == null) { + return; + } + + final SafeExecutor safeExecutor = new SafeExecutor<>(onSuccessCallback, onErrorCallback); + + final Disposable subscription = connection + .discoverServices() + .doOnDispose(() -> { + safeExecutor.error(BleErrorUtils.cancelled()); + pendingTransactions.removeSubscription(transactionId); + }) + .subscribe(rxBleDeviceServices -> { + ArrayList services = new ArrayList<>(); + for (BluetoothGattService gattService : rxBleDeviceServices.getBluetoothGattServices()) { + Service service = serviceFactory.create(device.getId(), gattService); + discoveredServices.put(service.getId(), service); + services.add(service); + + for (BluetoothGattCharacteristic gattCharacteristic : gattService.getCharacteristics()) { + Characteristic characteristic = new Characteristic(service, gattCharacteristic); + discoveredCharacteristics.put(characteristic.getId(), characteristic); + + for (BluetoothGattDescriptor gattDescriptor : gattCharacteristic.getDescriptors()) { + Descriptor descriptor = new Descriptor(characteristic, gattDescriptor); + discoveredDescriptors.put(descriptor.getId(), descriptor); + } + } + } + device.setServices(services); + // Moved from onSuccess method from old RxJava1 implementation + safeExecutor.success(device); + pendingTransactions.removeSubscription(transactionId); + }, throwable -> { + safeExecutor.error(errorConverter.toError(throwable)); + pendingTransactions.removeSubscription(transactionId); + }); + + pendingTransactions.replaceSubscription(transactionId, subscription); + } + + private void safeReadCharacteristicForDevice(final Characteristic characteristic, + final String transactionId, + final OnSuccessCallback onSuccessCallback, + final OnErrorCallback onErrorCallback) { + final RxBleConnection connection = getConnectionOrEmitError(characteristic.getDeviceId(), onErrorCallback); + if (connection == null) { + return; + } + + final SafeExecutor safeExecutor = new SafeExecutor<>(onSuccessCallback, onErrorCallback); + + final Disposable subscription = connection + .readCharacteristic(characteristic.gattCharacteristic) + .doOnDispose(() -> { + safeExecutor.error(BleErrorUtils.cancelled()); + pendingTransactions.removeSubscription(transactionId); + }) + .subscribe(bytes -> { + characteristic.logValue("Read from", bytes); + characteristic.setValue(bytes); + safeExecutor.success(new Characteristic(characteristic)); + pendingTransactions.removeSubscription(transactionId); + }, throwable -> { + safeExecutor.error(errorConverter.toError(throwable)); + pendingTransactions.removeSubscription(transactionId); + }); + + pendingTransactions.replaceSubscription(transactionId, subscription); + } + + private void writeCharacteristicWithValue(final Characteristic characteristic, + final String valueBase64, + final Boolean response, + final String transactionId, + OnSuccessCallback onSuccessCallback, + OnErrorCallback onErrorCallback) { + final byte[] value; + try { + value = Base64Converter.decode(valueBase64); + } catch (Throwable error) { + onErrorCallback.onError( + BleErrorUtils.invalidWriteDataForCharacteristic(valueBase64, + UUIDConverter.fromUUID(characteristic.getUuid()))); + return; + } + + characteristic.setWriteType(response ? + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT : + BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); + + safeWriteCharacteristicForDevice( + characteristic, + value, + transactionId, + onSuccessCallback, + onErrorCallback); + } + + private void safeWriteCharacteristicForDevice(final Characteristic characteristic, + final byte[] value, + final String transactionId, + final OnSuccessCallback onSuccessCallback, + final OnErrorCallback onErrorCallback) { + final RxBleConnection connection = getConnectionOrEmitError(characteristic.getDeviceId(), onErrorCallback); + if (connection == null) { + return; + } + + final SafeExecutor safeExecutor = new SafeExecutor<>(onSuccessCallback, onErrorCallback); + + final Disposable subscription = connection + .writeCharacteristic(characteristic.gattCharacteristic, value) + .doOnDispose(() -> { + safeExecutor.error(BleErrorUtils.cancelled()); + pendingTransactions.removeSubscription(transactionId); + }) + .subscribe(bytes -> { + characteristic.logValue("Write to", bytes); + characteristic.setValue(bytes); + safeExecutor.success(new Characteristic(characteristic)); + pendingTransactions.removeSubscription(transactionId); + }, throwable -> { + safeExecutor.error(errorConverter.toError(throwable)); + pendingTransactions.removeSubscription(transactionId); + }); + + pendingTransactions.replaceSubscription(transactionId, subscription); + } + + private void safeMonitorCharacteristicForDevice(final Characteristic characteristic, + final String transactionId, + final OnEventCallback onEventCallback, + final OnErrorCallback onErrorCallback) { + final RxBleConnection connection = getConnectionOrEmitError(characteristic.getDeviceId(), onErrorCallback); + if (connection == null) { + return; + } + + final SafeExecutor safeExecutor = new SafeExecutor<>(null, onErrorCallback); + + final Disposable subscription = Observable.defer(() -> { + BluetoothGattDescriptor cccDescriptor = characteristic.getGattDescriptor(Constants.CLIENT_CHARACTERISTIC_CONFIG_UUID); + NotificationSetupMode setupMode = cccDescriptor != null + ? NotificationSetupMode.QUICK_SETUP + : NotificationSetupMode.COMPAT; + if (characteristic.isNotifiable()) { + return connection.setupNotification(characteristic.gattCharacteristic, setupMode); + } + + if (characteristic.isIndicatable()) { + return connection.setupIndication(characteristic.gattCharacteristic, setupMode); + } + + return Observable.error(new CannotMonitorCharacteristicException(characteristic)); + }) + .flatMap(observable -> observable) + .toFlowable(BackpressureStrategy.BUFFER) + .observeOn(Schedulers.computation()) + .doOnCancel(() -> { + safeExecutor.error(BleErrorUtils.cancelled()); + pendingTransactions.removeSubscription(transactionId); + }) + .doOnComplete(() -> pendingTransactions.removeSubscription(transactionId)) + .subscribe(bytes -> { + characteristic.logValue("Notification from", bytes); + characteristic.setValue(bytes); + onEventCallback.onEvent(new Characteristic(characteristic)); + }, throwable -> { + safeExecutor.error(errorConverter.toError(throwable)); + pendingTransactions.removeSubscription(transactionId); + }); + + pendingTransactions.replaceSubscription(transactionId, subscription); + } + + @Nullable + private Characteristic getCharacteristicOrEmitError(@NonNull final String deviceId, + @NonNull final String serviceUUID, + @NonNull final String characteristicUUID, + @NonNull final OnErrorCallback onErrorCallback) { + + final UUID[] UUIDs = UUIDConverter.convert(serviceUUID, characteristicUUID); + if (UUIDs == null) { + onErrorCallback.onError(BleErrorUtils.invalidIdentifiers(serviceUUID, characteristicUUID)); + return null; + } + + final Device device = connectedDevices.get(deviceId); + if (device == null) { + onErrorCallback.onError(BleErrorUtils.deviceNotConnected(deviceId)); + return null; + } + + final Service service = device.getServiceByUUID(UUIDs[0]); + if (service == null) { + onErrorCallback.onError(BleErrorUtils.serviceNotFound(serviceUUID)); + return null; + } + + final Characteristic characteristic = service.getCharacteristicByUUID(UUIDs[1]); + if (characteristic == null) { + onErrorCallback.onError(BleErrorUtils.characteristicNotFound(characteristicUUID)); + return null; + } + + return characteristic; + } + + @Nullable + private Characteristic getCharacteristicOrEmitError(final int serviceIdentifier, + @NonNull final String characteristicUUID, + @NonNull final OnErrorCallback onErrorCallback) { + + final UUID uuid = UUIDConverter.convert(characteristicUUID); + if (uuid == null) { + onErrorCallback.onError(BleErrorUtils.invalidIdentifiers(characteristicUUID)); + return null; + } + + final Service service = discoveredServices.get(serviceIdentifier); + if (service == null) { + onErrorCallback.onError(BleErrorUtils.serviceNotFound(Integer.toString(serviceIdentifier))); + return null; + } + + final Characteristic characteristic = service.getCharacteristicByUUID(uuid); + if (characteristic == null) { + onErrorCallback.onError(BleErrorUtils.characteristicNotFound(characteristicUUID)); + return null; + } + + return characteristic; + } + + @Nullable + private Characteristic getCharacteristicOrEmitError(final int characteristicIdentifier, + @NonNull final OnErrorCallback onErrorCallback) { + + final Characteristic characteristic = discoveredCharacteristics.get(characteristicIdentifier); + if (characteristic == null) { + onErrorCallback.onError(BleErrorUtils.characteristicNotFound(Integer.toString(characteristicIdentifier))); + return null; + } + + return characteristic; + } + + private void cleanServicesAndCharacteristicsForDevice(@NonNull Device device) { + for (int i = discoveredServices.size() - 1; i >= 0; i--) { + int key = discoveredServices.keyAt(i); + Service service = discoveredServices.get(key); + + if (service.getDeviceID().equals(device.getId())) { + discoveredServices.remove(key); + } + } + for (int i = discoveredCharacteristics.size() - 1; i >= 0; i--) { + int key = discoveredCharacteristics.keyAt(i); + Characteristic characteristic = discoveredCharacteristics.get(key); + + if (characteristic.getDeviceId().equals(device.getId())) { + discoveredCharacteristics.remove(key); + } + } + + for (int i = discoveredDescriptors.size() - 1; i >= 0; i--) { + int key = discoveredDescriptors.keyAt(i); + Descriptor descriptor = discoveredDescriptors.get(key); + if (descriptor.getDeviceId().equals(device.getId())) { + discoveredDescriptors.remove(key); + } + } + } +} diff --git a/android/src/main/java/com/bleplx/adapter/Characteristic.java b/android/src/main/java/com/bleplx/adapter/Characteristic.java new file mode 100755 index 00000000..1e1fb7a8 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/Characteristic.java @@ -0,0 +1,151 @@ +package com.bleplx.adapter; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bleplx.adapter.utils.ByteUtils; +import com.bleplx.adapter.utils.Constants; +import com.bleplx.adapter.utils.IdGenerator; +import com.bleplx.adapter.utils.IdGeneratorKey; +import com.polidea.rxandroidble2.internal.RxBleLog; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** @noinspection ALL*/ +public class Characteristic { + + final private int id; + final private int serviceID; + final private UUID serviceUUID; + final private String deviceID; + private byte[] value; + final BluetoothGattCharacteristic gattCharacteristic; + + public void setValue(byte[] value) { + this.value = value; + } + + public Characteristic(@NonNull Service service, @NonNull BluetoothGattCharacteristic gattCharacteristic) { + this.deviceID = service.getDeviceID(); + this.serviceUUID = service.getUuid(); + this.serviceID = service.getId(); + this.gattCharacteristic = gattCharacteristic; + this.id = IdGenerator.getIdForKey(new IdGeneratorKey(deviceID, gattCharacteristic.getUuid(), gattCharacteristic.getInstanceId())); + } + + public Characteristic(int id, @NonNull Service service, BluetoothGattCharacteristic gattCharacteristic) { + this.id = id; + this.deviceID = service.getDeviceID(); + this.serviceUUID = service.getUuid(); + this.serviceID = service.getId(); + this.gattCharacteristic = gattCharacteristic; + } + + public Characteristic(Characteristic other) { + id = other.id; + serviceID = other.serviceID; + serviceUUID = other.serviceUUID; + deviceID = other.deviceID; + if (other.value != null) value = other.value.clone(); + gattCharacteristic = other.gattCharacteristic; + } + + public int getId() { + return this.id; + } + + public UUID getUuid() { + return gattCharacteristic.getUuid(); + } + + public int getServiceID() { + return serviceID; + } + + public UUID getServiceUUID() { + return serviceUUID; + } + + public String getDeviceId() { + return deviceID; + } + + public int getInstanceId() { + return gattCharacteristic.getInstanceId(); + } + + public BluetoothGattDescriptor getGattDescriptor(UUID uuid) { + return gattCharacteristic.getDescriptor(uuid); + } + + public void setWriteType(int writeType) { + gattCharacteristic.setWriteType(writeType); + } + + public boolean isReadable() { + return (gattCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) != 0; + } + + public boolean isWritableWithResponse() { + return (gattCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0; + } + + public boolean isWritableWithoutResponse() { + return (gattCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0; + } + + public boolean isNotifiable() { + return (gattCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0; + } + + public List getDescriptors() { + ArrayList descriptors = new ArrayList<>(gattCharacteristic.getDescriptors().size()); + for (BluetoothGattDescriptor gattDescriptor : gattCharacteristic.getDescriptors()) { + descriptors.add(new Descriptor(this, gattDescriptor)); + } + return descriptors; + } + + public boolean isNotifying() { + BluetoothGattDescriptor descriptor = gattCharacteristic.getDescriptor(Constants.CLIENT_CHARACTERISTIC_CONFIG_UUID); + boolean isNotifying = false; + if (descriptor != null) { + byte[] descriptorValue = descriptor.getValue(); + if (descriptorValue != null) { + isNotifying = (descriptorValue[0] & 0x01) != 0; + } + } + return isNotifying; + } + + public boolean isIndicatable() { + return (gattCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0; + } + + public byte[] getValue() { + return value; + } + + @Nullable + public Descriptor getDescriptorByUUID(@NonNull UUID uuid) { + BluetoothGattDescriptor descriptor = this.gattCharacteristic.getDescriptor(uuid); + if (descriptor == null) return null; + return new Descriptor(this, descriptor); + } + + void logValue(String message, byte[] value) { + if (value == null) { + value = gattCharacteristic.getValue(); + } + String hexValue = value != null ? ByteUtils.bytesToHex(value) : "(null)"; + RxBleLog.v(message + + " Characteristic(uuid: " + gattCharacteristic.getUuid().toString() + + ", id: " + id + + ", value: " + hexValue + ")"); + } +} diff --git a/android/src/main/java/com/bleplx/adapter/ConnectionOptions.java b/android/src/main/java/com/bleplx/adapter/ConnectionOptions.java new file mode 100644 index 00000000..3041d204 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/ConnectionOptions.java @@ -0,0 +1,72 @@ +package com.bleplx.adapter; + +import androidx.annotation.Nullable; + +import com.bleplx.adapter.utils.Constants.ConnectionPriority; + +public class ConnectionOptions { + + /** + * Whether to directly connect to the remote device (false) or to automatically connect as soon + * as the remote device becomes available (true). + */ + private final boolean autoConnect; + + /** + * Whether MTU size will be negotiated to this value. It is not guaranteed to get it after + * connection is successful. + */ + private final int requestMTU; + + /** + * Whether action will be taken to reset services cache. This option may be useful when a + * peripheral's firmware was updated and it's services/characteristics were + * added/removed/altered. {@link ...} + */ + private final RefreshGattMoment refreshGattMoment; + + /** + * Number of milliseconds after connection is automatically timed out. In case of race condition + * were connection is established right after timeout event, device will be disconnected + * immediately. Time out may happen earlier then specified due to OS specific behavior. + */ + @Nullable + private final Long timeoutInMillis; + + @ConnectionPriority + private final int connectionPriority; + + public ConnectionOptions(Boolean autoConnect, + int requestMTU, + RefreshGattMoment refreshGattMoment, + @Nullable Long timeoutInMillis, + int connectionPriority) { + this.autoConnect = autoConnect; + this.requestMTU = requestMTU; + this.refreshGattMoment = refreshGattMoment; + this.timeoutInMillis = timeoutInMillis; + this.connectionPriority = connectionPriority; + } + + public Boolean getAutoConnect() { + return autoConnect; + } + + public int getRequestMTU() { + return requestMTU; + } + + public RefreshGattMoment getRefreshGattMoment() { + return refreshGattMoment; + } + + @Nullable + public Long getTimeoutInMillis() { + return timeoutInMillis; + } + + @ConnectionPriority + public int getConnectionPriority() { + return connectionPriority; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/ConnectionState.java b/android/src/main/java/com/bleplx/adapter/ConnectionState.java new file mode 100644 index 00000000..b76358e5 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/ConnectionState.java @@ -0,0 +1,12 @@ +package com.bleplx.adapter; + +public enum ConnectionState { + + CONNECTING("connecting"), CONNECTED("connected"), DISCONNECTING("disconnecting"), DISCONNECTED("disconnected"); + + public final String value; + + ConnectionState(String value) { + this.value = value; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/Descriptor.java b/android/src/main/java/com/bleplx/adapter/Descriptor.java new file mode 100644 index 00000000..103cc9aa --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/Descriptor.java @@ -0,0 +1,115 @@ +package com.bleplx.adapter; + +import android.bluetooth.BluetoothGattDescriptor; + +import androidx.annotation.NonNull; + +import com.bleplx.adapter.utils.ByteUtils; +import com.bleplx.adapter.utils.IdGenerator; +import com.bleplx.adapter.utils.IdGeneratorKey; +import com.polidea.rxandroidble2.internal.RxBleLog; + +import java.util.UUID; + +/** @noinspection unused*/ +public class Descriptor { + final private int characteristicId; + final private int serviceId; + final private UUID characteristicUuid; + final private UUID serviceUuid; + final private String deviceId; + final private BluetoothGattDescriptor descriptor; + final private int id; + final private UUID uuid; + private byte[] value = null; + + public Descriptor(@NonNull Characteristic characteristic, @NonNull BluetoothGattDescriptor gattDescriptor) { + this.characteristicId = characteristic.getId(); + this.characteristicUuid = characteristic.getUuid(); + this.serviceId = characteristic.getServiceID(); + this.serviceUuid = characteristic.getServiceUUID(); + this.descriptor = gattDescriptor; + this.deviceId = characteristic.getDeviceId(); + this.id = IdGenerator.getIdForKey(new IdGeneratorKey(deviceId, descriptor.getUuid(), characteristicId)); + this.uuid = gattDescriptor.getUuid(); + } + + //secondary constructor, not used by MBA itself, but which can be used by external plugins (eg. BLEmulator) + public Descriptor(int characteristicId, int serviceId, UUID characteristicUuid, UUID serviceUuid, String deviceId, BluetoothGattDescriptor descriptor, int id, UUID uuid) { + this.characteristicId = characteristicId; + this.serviceId = serviceId; + this.characteristicUuid = characteristicUuid; + this.serviceUuid = serviceUuid; + this.deviceId = deviceId; + this.descriptor = descriptor; + this.id = id; + this.uuid = uuid; + } + + public Descriptor(Descriptor other) { + characteristicUuid = other.characteristicUuid; + characteristicId = other.characteristicId; + serviceUuid = other.serviceUuid; + serviceId = other.serviceId; + deviceId = other.deviceId; + descriptor = other.descriptor; + id = other.id; + uuid = other.uuid; + if (other.value != null) value = other.value.clone(); + } + + public int getId() { + return id; + } + + public String getDeviceId() { + return deviceId; + } + + public int getCharacteristicId() { + return characteristicId; + } + + public int getServiceId() { + return serviceId; + } + + public UUID getCharacteristicUuid() { + return characteristicUuid; + } + + public UUID getServiceUuid() { + return serviceUuid; + } + + public UUID getUuid() { + return uuid; + } + + public byte[] getValue() { + return value; + } + + public void setValue(byte[] value) { + this.value = value; + } + + public void setValueFromCache() { + value = descriptor.getValue(); + } + + public BluetoothGattDescriptor getNativeDescriptor() { + return descriptor; + } + + public void logValue(String message, byte[] value) { + if (value == null) { + value = descriptor.getValue(); + } + String hexValue = value != null ? ByteUtils.bytesToHex(value) : "(null)"; + RxBleLog.v(message + + " Descriptor(uuid: " + descriptor.getUuid().toString() + + ", id: " + id + + ", value: " + hexValue + ")"); + } +} diff --git a/android/src/main/java/com/bleplx/adapter/Device.java b/android/src/main/java/com/bleplx/adapter/Device.java new file mode 100755 index 00000000..05335267 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/Device.java @@ -0,0 +1,81 @@ +package com.bleplx.adapter; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; +import java.util.UUID; + +/** @noinspection unused*/ +public class Device { + + private String id; + private String name; + @Nullable + private Integer rssi; + @Nullable + private Integer mtu; + @Nullable + private List services; + + public Device(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Nullable + public Integer getRssi() { + return rssi; + } + + public void setRssi(@Nullable Integer rssi) { + this.rssi = rssi; + } + + @Nullable + public Integer getMtu() { + return mtu; + } + + public void setMtu(@Nullable Integer mtu) { + this.mtu = mtu; + } + + @Nullable + public List getServices() { + return services; + } + + public void setServices(@Nullable List services) { + this.services = services; + } + + @Nullable + public Service getServiceByUUID(@NonNull UUID uuid) { + if (services == null) { + return null; + } + + for (Service service : services) { + if (uuid.equals(service.getUuid())) + return service; + } + return null; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/OnErrorCallback.java b/android/src/main/java/com/bleplx/adapter/OnErrorCallback.java new file mode 100644 index 00000000..8c6d1bba --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/OnErrorCallback.java @@ -0,0 +1,8 @@ +package com.bleplx.adapter; + +import com.bleplx.adapter.errors.BleError; + +public interface OnErrorCallback { + + void onError(BleError error); +} diff --git a/android/src/main/java/com/bleplx/adapter/OnEventCallback.java b/android/src/main/java/com/bleplx/adapter/OnEventCallback.java new file mode 100644 index 00000000..71d378e4 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/OnEventCallback.java @@ -0,0 +1,6 @@ +package com.bleplx.adapter; + +public interface OnEventCallback { + + void onEvent(T data); +} diff --git a/android/src/main/java/com/bleplx/adapter/OnSuccessCallback.java b/android/src/main/java/com/bleplx/adapter/OnSuccessCallback.java new file mode 100644 index 00000000..1dada60f --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/OnSuccessCallback.java @@ -0,0 +1,6 @@ +package com.bleplx.adapter; + +public interface OnSuccessCallback { + + void onSuccess(T data); +} diff --git a/android/src/main/java/com/bleplx/adapter/RefreshGattMoment.java b/android/src/main/java/com/bleplx/adapter/RefreshGattMoment.java new file mode 100755 index 00000000..92435684 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/RefreshGattMoment.java @@ -0,0 +1,20 @@ +package com.bleplx.adapter; + + +public enum RefreshGattMoment { + + ON_CONNECTED("OnConnected"); + + final String name; + + RefreshGattMoment(String name) { + this.name = name; + } + + public static RefreshGattMoment getByName(String name) { + for (RefreshGattMoment refreshGattMoment : RefreshGattMoment.values()) { + if (refreshGattMoment.name.equals(name)) return refreshGattMoment; + } + return null; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/ScanResult.java b/android/src/main/java/com/bleplx/adapter/ScanResult.java new file mode 100644 index 00000000..c3a89ba0 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/ScanResult.java @@ -0,0 +1,132 @@ +package com.bleplx.adapter; + + +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; + +/** + * @noinspection unused + */ +public class ScanResult { + + private String deviceId; + private String deviceName; + private int rssi; + private int mtu; + private boolean isConnectable; + @Nullable + private UUID[] overflowServiceUUIDs; + private AdvertisementData advertisementData; + + public ScanResult(String deviceId, String deviceName, int rssi, int mtu, boolean isConnectable, @Nullable UUID[] overflowServiceUUIDs, AdvertisementData advertisementData) { + this.deviceId = deviceId; + this.deviceName = deviceName; + this.rssi = rssi; + this.mtu = mtu; + this.isConnectable = isConnectable; + this.overflowServiceUUIDs = overflowServiceUUIDs; + this.advertisementData = advertisementData; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public int getRssi() { + return rssi; + } + + public void setRssi(int rssi) { + this.rssi = rssi; + } + + public int getMtu() { + return mtu; + } + + public void setMtu(int mtu) { + this.mtu = mtu; + } + + public boolean isConnectable() { + return isConnectable; + } + + public void setConnectable(boolean connectable) { + isConnectable = connectable; + } + + public UUID[] getOverflowServiceUUIDs() { + return overflowServiceUUIDs; + } + + public void setOverflowServiceUUIDs(@Nullable UUID[] overflowServiceUUIDs) { + this.overflowServiceUUIDs = overflowServiceUUIDs; + } + + public AdvertisementData getAdvertisementData() { + return advertisementData; + } + + public void setAdvertisementData(AdvertisementData advertisementData) { + this.advertisementData = advertisementData; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ScanResult that = (ScanResult) o; + + if (rssi != that.rssi) return false; + if (mtu != that.mtu) return false; + if (isConnectable != that.isConnectable) return false; + if (!deviceId.equals(that.deviceId)) return false; + if (!Objects.equals(deviceName, that.deviceName)) + return false; + // Probably incorrect - comparing Object[] arrays with Arrays.equals + if (!Arrays.equals(overflowServiceUUIDs, that.overflowServiceUUIDs)) return false; + return Objects.equals(advertisementData, that.advertisementData); + } + + @Override + public int hashCode() { + int result = deviceId.hashCode(); + result = 31 * result + (deviceName != null ? deviceName.hashCode() : 0); + result = 31 * result + rssi; + result = 31 * result + mtu; + result = 31 * result + (isConnectable ? 1 : 0); + result = 31 * result + Arrays.hashCode(overflowServiceUUIDs); + result = 31 * result + (advertisementData != null ? advertisementData.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ScanResult{" + + "deviceId='" + deviceId + '\'' + + ", deviceName='" + deviceName + '\'' + + ", rssi=" + rssi + + ", mtu=" + mtu + + ", isConnectable=" + isConnectable + + ", overflowServiceUUIDs=" + Arrays.toString(overflowServiceUUIDs) + + ", advertisementData=" + advertisementData + + '}'; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/Service.java b/android/src/main/java/com/bleplx/adapter/Service.java new file mode 100755 index 00000000..68ff349d --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/Service.java @@ -0,0 +1,56 @@ +package com.bleplx.adapter; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** @noinspection unused*/ +public class Service { + + final private int id; + final private String deviceID; + final private BluetoothGattService btGattService; + + public Service(int id, String deviceID, BluetoothGattService btGattService) { + this.id = id; + this.deviceID = deviceID; + this.btGattService = btGattService; + } + + public int getId() { + return this.id; + } + + public UUID getUuid() { + return btGattService.getUuid(); + } + + public String getDeviceID() { + return deviceID; + } + + public boolean isPrimary() { + return btGattService.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY; + } + + @Nullable + public Characteristic getCharacteristicByUUID(@NonNull UUID uuid) { + BluetoothGattCharacteristic characteristic = btGattService.getCharacteristic(uuid); + if (characteristic == null) return null; + return new Characteristic(this, characteristic); + } + + public List getCharacteristics() { + ArrayList characteristics = new ArrayList<>(btGattService.getCharacteristics().size()); + for (BluetoothGattCharacteristic gattCharacteristic : btGattService.getCharacteristics()) { + characteristics.add(new Characteristic(this, gattCharacteristic)); + } + return characteristics; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/errors/BleError.java b/android/src/main/java/com/bleplx/adapter/errors/BleError.java new file mode 100755 index 00000000..89a179a5 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/errors/BleError.java @@ -0,0 +1,32 @@ +package com.bleplx.adapter.errors; + + +public class BleError extends Throwable { + + public BleErrorCode errorCode; + public Integer androidCode; + public String reason; + public String deviceID; + public String serviceUUID; + public String characteristicUUID; + public String descriptorUUID; + public String internalMessage; + + public BleError(BleErrorCode errorCode, String reason, Integer androidCode) { + this.errorCode = errorCode; + this.reason = reason; + this.androidCode = androidCode; + } + + @Override + public String getMessage() { + return "Error code: " + errorCode + + ", android code: " + androidCode + + ", reason" + reason + + ", deviceId" + deviceID + + ", serviceUuid" + serviceUUID + + ", characteristicUuid" + characteristicUUID + + ", descriptorUuid" + descriptorUUID + + ", internalMessage" + internalMessage; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/errors/BleErrorCode.java b/android/src/main/java/com/bleplx/adapter/errors/BleErrorCode.java new file mode 100755 index 00000000..bd17a8fc --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/errors/BleErrorCode.java @@ -0,0 +1,57 @@ +package com.bleplx.adapter.errors; + + +public enum BleErrorCode { + + UnknownError(0), + BluetoothManagerDestroyed(1), + OperationCancelled(2), + OperationTimedOut(3), + OperationStartFailed(4), + InvalidIdentifiers(5), + + BluetoothUnsupported(100), + BluetoothUnauthorized(101), + BluetoothPoweredOff(102), + BluetoothInUnknownState(103), + BluetoothResetting(104), + BluetoothStateChangeFailed(105), + + DeviceConnectionFailed(200), + DeviceDisconnected(201), + DeviceRSSIReadFailed(202), + DeviceAlreadyConnected(203), + DeviceNotFound(204), + DeviceNotConnected(205), + DeviceMTUChangeFailed(206), + + ServicesDiscoveryFailed(300), + IncludedServicesDiscoveryFailed(301), + ServiceNotFound(302), + ServicesNotDiscovered(303), + + CharacteristicsDiscoveryFailed(400), + CharacteristicWriteFailed(401), + CharacteristicReadFailed(402), + CharacteristicNotifyChangeFailed(403), + CharacteristicNotFound(404), + CharacteristicsNotDiscovered(405), + CharacteristicInvalidDataFormat(406), + + DescriptorsDiscoveryFailed(500), + DescriptorWriteFailed(501), + DescriptorReadFailed(502), + DescriptorNotFound(503), + DescriptorsNotDiscovered(504), + DescriptorInvalidDataFormat(505), + DescriptorWriteNotAllowed(506), + + ScanStartFailed(600), + LocationServicesDisabled(601); + + public final int code; + + BleErrorCode(int code) { + this.code = code; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/errors/BleErrorUtils.java b/android/src/main/java/com/bleplx/adapter/errors/BleErrorUtils.java new file mode 100755 index 00000000..fed5c0a8 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/errors/BleErrorUtils.java @@ -0,0 +1,86 @@ +package com.bleplx.adapter.errors; + + +import androidx.annotation.NonNull; + +public class BleErrorUtils { + + public static BleError cancelled() { + return new BleError(BleErrorCode.OperationCancelled, null, null); + } + + static public BleError invalidIdentifiers(@NonNull String... identifiers) { + StringBuilder identifiersJoined = new StringBuilder(); + for (String identifier: identifiers) { + identifiersJoined.append(identifier).append(", "); + } + + BleError bleError = new BleError(BleErrorCode.InvalidIdentifiers, null, null); + bleError.internalMessage = identifiersJoined.toString(); + return bleError; + } + + static public BleError deviceNotFound(String uuid) { + BleError bleError = new BleError(BleErrorCode.DeviceNotFound, null, null); + bleError.deviceID = uuid; + return bleError; + } + + static public BleError deviceNotConnected(String uuid) { + BleError bleError = new BleError(BleErrorCode.DeviceNotConnected, null, null); + bleError.deviceID = uuid; + return bleError; + } + + static public BleError characteristicNotFound(String uuid) { + BleError bleError = new BleError(BleErrorCode.CharacteristicNotFound, null, null); + bleError.characteristicUUID = uuid; + return bleError; + } + + static public BleError invalidWriteDataForCharacteristic(String data, String uuid) { + BleError bleError = new BleError(BleErrorCode.CharacteristicInvalidDataFormat, null, null); + bleError.characteristicUUID = uuid; + bleError.internalMessage = data; + return bleError; + } + + static public BleError descriptorNotFound(String uuid) { + BleError bleError = new BleError(BleErrorCode.DescriptorNotFound, null, null); + bleError.descriptorUUID = uuid; + return bleError; + } + + static public BleError invalidWriteDataForDescriptor(String data, String uuid) { + BleError bleError = new BleError(BleErrorCode.DescriptorInvalidDataFormat, null, null); + bleError.descriptorUUID = uuid; + bleError.internalMessage = data; + return bleError; + } + + static public BleError descriptorWriteNotAllowed(String uuid) { + BleError bleError = new BleError(BleErrorCode.DescriptorWriteNotAllowed, null, null); + bleError.descriptorUUID = uuid; + return bleError; + } + + static public BleError serviceNotFound(String uuid) { + BleError bleError = new BleError(BleErrorCode.ServiceNotFound, null, null); + bleError.serviceUUID = uuid; + return bleError; + } + + static public BleError cannotMonitorCharacteristic(String reason, String deviceID, String serviceUUID, String characteristicUUID) { + BleError bleError = new BleError(BleErrorCode.CharacteristicNotifyChangeFailed, reason, null); + bleError.deviceID = deviceID; + bleError.serviceUUID = serviceUUID; + bleError.characteristicUUID = characteristicUUID; + return bleError; + } + + public static BleError deviceServicesNotDiscovered(String deviceID) { + BleError bleError = new BleError(BleErrorCode.ServicesNotDiscovered, null, null); + bleError.deviceID = deviceID; + return bleError; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/errors/ErrorConverter.java b/android/src/main/java/com/bleplx/adapter/errors/ErrorConverter.java new file mode 100755 index 00000000..df3c4b6f --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/errors/ErrorConverter.java @@ -0,0 +1,219 @@ +package com.bleplx.adapter.errors; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; + +import com.bleplx.adapter.Characteristic; +import com.bleplx.adapter.exceptions.CannotMonitorCharacteristicException; +import com.bleplx.adapter.utils.UUIDConverter; +import com.polidea.rxandroidble2.exceptions.BleAlreadyConnectedException; +import com.polidea.rxandroidble2.exceptions.BleCannotSetCharacteristicNotificationException; +import com.polidea.rxandroidble2.exceptions.BleCharacteristicNotFoundException; +import com.polidea.rxandroidble2.exceptions.BleConflictingNotificationAlreadySetException; +import com.polidea.rxandroidble2.exceptions.BleDisconnectedException; +import com.polidea.rxandroidble2.exceptions.BleGattCallbackTimeoutException; +import com.polidea.rxandroidble2.exceptions.BleGattCannotStartException; +import com.polidea.rxandroidble2.exceptions.BleGattCharacteristicException; +import com.polidea.rxandroidble2.exceptions.BleGattDescriptorException; +import com.polidea.rxandroidble2.exceptions.BleGattException; +import com.polidea.rxandroidble2.exceptions.BleGattOperationType; +import com.polidea.rxandroidble2.exceptions.BleScanException; +import com.polidea.rxandroidble2.exceptions.BleServiceNotFoundException; + +import java.util.UUID; +import java.util.concurrent.TimeoutException; + +public class ErrorConverter { + + public BleError toError(Throwable throwable) { + + // Custom exceptions ----------------------------------------------------------------------- + + if (throwable instanceof CannotMonitorCharacteristicException) { + CannotMonitorCharacteristicException exception = (CannotMonitorCharacteristicException) throwable; + Characteristic characteristic = exception.getCharacteristic(); + // TODO: Missing deviceID + return BleErrorUtils.cannotMonitorCharacteristic( + throwable.getMessage(), + null, + UUIDConverter.fromUUID(characteristic.getServiceUUID()), + UUIDConverter.fromUUID(characteristic.getUuid())); + } + + // RxSwift exceptions ---------------------------------------------------------------------- + + if (throwable instanceof TimeoutException) { + return new BleError(BleErrorCode.OperationTimedOut, throwable.getMessage(), null); + } + + // RxAndroidBle exceptions ----------------------------------------------------------------- + + if (throwable instanceof BleAlreadyConnectedException) { + // TODO: Missing deviceID + return new BleError(BleErrorCode.DeviceAlreadyConnected, throwable.getMessage(), null); + } + + if (throwable instanceof BleCannotSetCharacteristicNotificationException) { + BluetoothGattCharacteristic gattCharacteristic = ((BleCannotSetCharacteristicNotificationException) throwable).getBluetoothGattCharacteristic(); + BluetoothGattService gattService = gattCharacteristic.getService(); + // TODO: Missing deviceID + return BleErrorUtils.cannotMonitorCharacteristic( + throwable.getMessage(), + null, + UUIDConverter.fromUUID(gattService.getUuid()), + UUIDConverter.fromUUID(gattCharacteristic.getUuid())); + } + + + if (throwable instanceof BleCharacteristicNotFoundException) { + UUID uuid = ((BleCharacteristicNotFoundException) throwable).getCharacteristicUUID(); + BleError bleError = new BleError(BleErrorCode.CharacteristicNotFound, throwable.getMessage(), null); + bleError.characteristicUUID = UUIDConverter.fromUUID(uuid); + return bleError; + } + + if (throwable instanceof BleConflictingNotificationAlreadySetException) { + UUID characteristicUUID = ((BleConflictingNotificationAlreadySetException) throwable).getCharacteristicUuid(); + // TODO: Missing Service UUID and device ID + return BleErrorUtils.cannotMonitorCharacteristic(throwable.getMessage(), null, null, UUIDConverter.fromUUID(characteristicUUID)); + } + + if (throwable instanceof BleDisconnectedException) { + BleDisconnectedException bleDisconnectedException = (BleDisconnectedException) throwable; + BleError bleError = new BleError(BleErrorCode.DeviceDisconnected, throwable.getMessage(), bleDisconnectedException.state); + bleError.deviceID = bleDisconnectedException.bluetoothDeviceAddress; + return bleError; + } + + if (throwable instanceof BleScanException) { + return toError((BleScanException) throwable); + } + + if (throwable instanceof BleServiceNotFoundException) { + BleError bleError = new BleError(BleErrorCode.ServiceNotFound, throwable.getMessage(), null); + bleError.serviceUUID = UUIDConverter.fromUUID(((BleServiceNotFoundException) throwable).getServiceUUID()); + return bleError; + } + + // RxAndroidBle (GATT) exceptions ---------------------------------------------------------- + + if (throwable instanceof BleGattCallbackTimeoutException) { + return new BleError(BleErrorCode.OperationTimedOut, throwable.getMessage(), ((BleGattCallbackTimeoutException) throwable).getStatus()); + } + + if (throwable instanceof BleGattCannotStartException) { + return new BleError(BleErrorCode.OperationStartFailed, throwable.getMessage(), ((BleGattCannotStartException) throwable).getStatus()); + } + + if (throwable instanceof BleGattCharacteristicException) { + BleGattCharacteristicException exception = (BleGattCharacteristicException) throwable; + int code = exception.getStatus(); + BleGattOperationType operationType = exception.getBleGattOperationType(); + + return toError(code, + throwable.getMessage(), + operationType, + exception.getMacAddress(), + UUIDConverter.fromUUID(exception.characteristic.getService().getUuid()), + UUIDConverter.fromUUID(exception.characteristic.getUuid()), + null); + } + + if (throwable instanceof BleGattDescriptorException) { + BleGattDescriptorException exception = (BleGattDescriptorException) throwable; + int code = exception.getStatus(); + BleGattOperationType operationType = exception.getBleGattOperationType(); + + return toError(code, + throwable.getMessage(), + operationType, + exception.getMacAddress(), + UUIDConverter.fromUUID(exception.descriptor.getCharacteristic().getService().getUuid()), + UUIDConverter.fromUUID(exception.descriptor.getCharacteristic().getUuid()), + UUIDConverter.fromUUID(exception.descriptor.getUuid())); + } + + if (throwable instanceof BleGattException) { + BleGattException exception = (BleGattException) throwable; + int code = exception.getStatus(); + BleGattOperationType operationType = exception.getBleGattOperationType(); + + return toError(code, + throwable.getMessage(), + operationType, + exception.getMacAddress(), + null, + null, + null); + } + + return new BleError(BleErrorCode.UnknownError, throwable.toString(), null); + } + + private BleError toError(int code, String message, BleGattOperationType operationType, String deviceID, String serviceUUID, String characteristicUUID, String descriptorUUID) { + if (BleGattOperationType.CONNECTION_STATE == operationType) { + BleError bleError = new BleError(BleErrorCode.DeviceDisconnected, message, code); + bleError.deviceID = deviceID; + return bleError; + } else if (BleGattOperationType.SERVICE_DISCOVERY == operationType) { + BleError bleError = new BleError(BleErrorCode.ServicesDiscoveryFailed, message, code); + bleError.deviceID = deviceID; + return bleError; + } else if (BleGattOperationType.CHARACTERISTIC_READ == operationType || BleGattOperationType.CHARACTERISTIC_CHANGED == operationType) { + BleError bleError = new BleError(BleErrorCode.CharacteristicReadFailed, message, code); + bleError.deviceID = deviceID; + bleError.serviceUUID = serviceUUID; + bleError.characteristicUUID = characteristicUUID; + return bleError; + } else if (BleGattOperationType.CHARACTERISTIC_WRITE == operationType || BleGattOperationType.CHARACTERISTIC_LONG_WRITE == operationType || BleGattOperationType.RELIABLE_WRITE_COMPLETED == operationType) { + BleError bleError = new BleError(BleErrorCode.CharacteristicWriteFailed, message, code); + bleError.deviceID = deviceID; + bleError.serviceUUID = serviceUUID; + bleError.characteristicUUID = characteristicUUID; + return bleError; + } else if (BleGattOperationType.DESCRIPTOR_READ == operationType) { + BleError bleError = new BleError(BleErrorCode.DescriptorReadFailed, message, code); + bleError.deviceID = deviceID; + bleError.serviceUUID = serviceUUID; + bleError.characteristicUUID = characteristicUUID; + bleError.descriptorUUID = descriptorUUID; + return bleError; + } else if (BleGattOperationType.DESCRIPTOR_WRITE == operationType) { + BleError bleError = new BleError(BleErrorCode.DescriptorWriteFailed, message, code); + bleError.deviceID = deviceID; + bleError.serviceUUID = serviceUUID; + bleError.characteristicUUID = characteristicUUID; + bleError.descriptorUUID = descriptorUUID; + return bleError; + } else if (BleGattOperationType.READ_RSSI == operationType) { + BleError bleError = new BleError(BleErrorCode.DeviceRSSIReadFailed, message, code); + bleError.deviceID = deviceID; + return bleError; + } else if (BleGattOperationType.ON_MTU_CHANGED == operationType) { + BleError bleError = new BleError(BleErrorCode.DeviceMTUChangeFailed, message, code); + bleError.deviceID = deviceID; + return bleError; + } else if (BleGattOperationType.CONNECTION_PRIORITY_CHANGE == operationType) { + // TODO: Handle? + } + + return new BleError(BleErrorCode.UnknownError, message, code); + } + + private BleError toError(BleScanException bleScanException) { + final int reason = bleScanException.getReason(); + switch (reason) { + case BleScanException.BLUETOOTH_DISABLED: + return new BleError(BleErrorCode.BluetoothPoweredOff, bleScanException.getMessage(), null); + case BleScanException.BLUETOOTH_NOT_AVAILABLE: + return new BleError(BleErrorCode.BluetoothUnsupported, bleScanException.getMessage(), null); + case BleScanException.LOCATION_PERMISSION_MISSING: + return new BleError(BleErrorCode.BluetoothUnauthorized, bleScanException.getMessage(), null); + case BleScanException.LOCATION_SERVICES_DISABLED: + return new BleError(BleErrorCode.LocationServicesDisabled, bleScanException.getMessage(), null); + case BleScanException.BLUETOOTH_CANNOT_START: + default: + return new BleError(BleErrorCode.ScanStartFailed, bleScanException.getMessage(), null); + } + } +} diff --git a/android/src/main/java/com/bleplx/adapter/exceptions/CannotMonitorCharacteristicException.java b/android/src/main/java/com/bleplx/adapter/exceptions/CannotMonitorCharacteristicException.java new file mode 100755 index 00000000..a76d06cb --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/exceptions/CannotMonitorCharacteristicException.java @@ -0,0 +1,15 @@ +package com.bleplx.adapter.exceptions; + +import com.bleplx.adapter.Characteristic; + +public class CannotMonitorCharacteristicException extends RuntimeException { + private Characteristic characteristic; + + public CannotMonitorCharacteristicException(Characteristic characteristic) { + this.characteristic = characteristic; + } + + public Characteristic getCharacteristic() { + return characteristic; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/Base64Converter.java b/android/src/main/java/com/bleplx/adapter/utils/Base64Converter.java new file mode 100755 index 00000000..0acb5343 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/Base64Converter.java @@ -0,0 +1,12 @@ +package com.bleplx.adapter.utils; + +import android.util.Base64; + +public class Base64Converter { + public static String encode(byte[] bytes) { + return Base64.encodeToString(bytes, Base64.NO_WRAP); + } + public static byte[] decode(String base64) { + return Base64.decode(base64, Base64.NO_WRAP); + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/ByteUtils.java b/android/src/main/java/com/bleplx/adapter/utils/ByteUtils.java new file mode 100644 index 00000000..86bcbea4 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/ByteUtils.java @@ -0,0 +1,15 @@ +package com.bleplx.adapter.utils; + +public class ByteUtils { + private final static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/Constants.java b/android/src/main/java/com/bleplx/adapter/utils/Constants.java new file mode 100755 index 00000000..20fec4fe --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/Constants.java @@ -0,0 +1,66 @@ +package com.bleplx.adapter.utils; + + +import androidx.annotation.IntDef; +import androidx.annotation.StringDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.UUID; + +public interface Constants { + + @StringDef({ + BluetoothState.UNKNOWN, + BluetoothState.RESETTING, + BluetoothState.UNSUPPORTED, + BluetoothState.UNAUTHORIZED, + BluetoothState.POWERED_OFF, + BluetoothState.POWERED_ON} + ) + @Retention(RetentionPolicy.SOURCE) + @interface BluetoothState { + + String UNKNOWN = "Unknown"; + String RESETTING = "Resetting"; + String UNSUPPORTED = "Unsupported"; + String UNAUTHORIZED = "Unauthorized"; + String POWERED_OFF = "PoweredOff"; + String POWERED_ON = "PoweredOn"; + } + + @StringDef({ + BluetoothLogLevel.NONE, + BluetoothLogLevel.VERBOSE, + BluetoothLogLevel.DEBUG, + BluetoothLogLevel.INFO, + BluetoothLogLevel.WARNING, + BluetoothLogLevel.ERROR} + ) + @Retention(RetentionPolicy.SOURCE) + @interface BluetoothLogLevel { + + String NONE = "None"; + String VERBOSE = "Verbose"; + String DEBUG = "Debug"; + String INFO = "Info"; + String WARNING = "Warning"; + String ERROR = "Error"; + } + + @IntDef({ + ConnectionPriority.BALANCED, + ConnectionPriority.HIGH, + ConnectionPriority.LOW_POWER} + ) + @Retention(RetentionPolicy.SOURCE) + @interface ConnectionPriority { + + int BALANCED = 0; + int HIGH = 1; + int LOW_POWER = 2; + } + + int MINIMUM_MTU = 23; + UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/DisposableMap.java b/android/src/main/java/com/bleplx/adapter/utils/DisposableMap.java new file mode 100755 index 00000000..94f20ac5 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/DisposableMap.java @@ -0,0 +1,39 @@ +package com.bleplx.adapter.utils; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import io.reactivex.disposables.Disposable; + +public class DisposableMap { + + final private Map subscriptions = new HashMap<>(); + + public synchronized void replaceSubscription(String key, Disposable subscription) { + Disposable oldSubscription = subscriptions.put(key, subscription); + if (oldSubscription != null && !oldSubscription.isDisposed()) { + oldSubscription.dispose(); + } + } + + public synchronized boolean removeSubscription(String key) { + Disposable subscription = subscriptions.remove(key); + if (subscription == null) return false; + if (!subscription.isDisposed()) { + subscription.dispose(); + } + return true; + } + + public synchronized void removeAllSubscriptions() { + Iterator> it = subscriptions.entrySet().iterator(); + while (it.hasNext()) { + Disposable subscription = it.next().getValue(); + it.remove(); + if (!subscription.isDisposed()) { + subscription.dispose(); + } + } + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/IdGenerator.java b/android/src/main/java/com/bleplx/adapter/utils/IdGenerator.java new file mode 100755 index 00000000..94305d3b --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/IdGenerator.java @@ -0,0 +1,22 @@ +package com.bleplx.adapter.utils; + +import java.util.HashMap; + +public class IdGenerator { + private static HashMap idMap = new HashMap<>(); + private static int nextKey = 0; + + public static int getIdForKey(IdGeneratorKey idGeneratorKey) { + Integer id = idMap.get(idGeneratorKey); + if (id != null) { + return id; + } + idMap.put(idGeneratorKey, ++nextKey); + return nextKey; + } + + public static void clear() { + idMap.clear(); + nextKey = 0; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/IdGeneratorKey.java b/android/src/main/java/com/bleplx/adapter/utils/IdGeneratorKey.java new file mode 100755 index 00000000..ca193cd8 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/IdGeneratorKey.java @@ -0,0 +1,37 @@ +package com.bleplx.adapter.utils; + + +import java.util.UUID; + +public class IdGeneratorKey { + + private final String deviceAddress; + private final UUID uuid; + private final int id; + + public IdGeneratorKey(String deviceAddress, UUID uuid, int id) { + this.deviceAddress = deviceAddress; + this.uuid = uuid; + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IdGeneratorKey that = (IdGeneratorKey) o; + + if (id != that.id) return false; + if (!deviceAddress.equals(that.deviceAddress)) return false; + return uuid.equals(that.uuid); + } + + @Override + public int hashCode() { + int result = deviceAddress.hashCode(); + result = 31 * result + uuid.hashCode(); + result = 31 * result + id; + return result; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/LogLevel.java b/android/src/main/java/com/bleplx/adapter/utils/LogLevel.java new file mode 100755 index 00000000..11f8628e --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/LogLevel.java @@ -0,0 +1,47 @@ +package com.bleplx.adapter.utils; + + +import com.polidea.rxandroidble2.internal.RxBleLog; + +public class LogLevel { + + @RxBleLog.LogLevel + public static int toLogLevel(String logLevel) { + switch (logLevel) { + case Constants.BluetoothLogLevel.VERBOSE: + return RxBleLog.VERBOSE; + case Constants.BluetoothLogLevel.DEBUG: + return RxBleLog.DEBUG; + case Constants.BluetoothLogLevel.INFO: + return RxBleLog.INFO; + case Constants.BluetoothLogLevel.WARNING: + return RxBleLog.WARN; + case Constants.BluetoothLogLevel.ERROR: + return RxBleLog.ERROR; + case Constants.BluetoothLogLevel.NONE: + // fallthrough + default: + return RxBleLog.NONE; + } + } + + @Constants.BluetoothLogLevel + public static String fromLogLevel(int logLevel) { + switch (logLevel) { + case RxBleLog.VERBOSE: + return Constants.BluetoothLogLevel.VERBOSE; + case RxBleLog.DEBUG: + return Constants.BluetoothLogLevel.DEBUG; + case RxBleLog.INFO: + return Constants.BluetoothLogLevel.INFO; + case RxBleLog.WARN: + return Constants.BluetoothLogLevel.WARNING; + case RxBleLog.ERROR: + return Constants.BluetoothLogLevel.ERROR; + case RxBleLog.NONE: + // fallthrough + default: + return Constants.BluetoothLogLevel.NONE; + } + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/RefreshGattCustomOperation.java b/android/src/main/java/com/bleplx/adapter/utils/RefreshGattCustomOperation.java new file mode 100755 index 00000000..20b12f18 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/RefreshGattCustomOperation.java @@ -0,0 +1,48 @@ +package com.bleplx.adapter.utils; + +import android.bluetooth.BluetoothGatt; + +import androidx.annotation.NonNull; + +import com.polidea.rxandroidble2.RxBleCustomOperation; +import com.polidea.rxandroidble2.internal.RxBleLog; +import com.polidea.rxandroidble2.internal.connection.RxBleGattCallback; + +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.Scheduler; + +public class RefreshGattCustomOperation implements RxBleCustomOperation { + + /** @noinspection unchecked, JavaReflectionMemberAccess, DataFlowIssue */ + @NonNull + @Override + public Observable asObservable( + final BluetoothGatt bluetoothGatt, + final RxBleGattCallback rxBleGattCallback, + final Scheduler scheduler + ) { + return Observable.ambArray( + Observable.fromCallable(() -> { + boolean success = false; + try { + Method bluetoothGattRefreshFunction = bluetoothGatt.getClass().getMethod("refresh"); + + success = (Boolean) bluetoothGattRefreshFunction.invoke(bluetoothGatt); + + if (!success) RxBleLog.d("BluetoothGatt.refresh() returned false"); + } catch (Exception e) { + RxBleLog.d(e, "Could not call function BluetoothGatt.refresh()"); + } + + RxBleLog.i("Calling BluetoothGatt.refresh() status: %s", success ? "Success" : "Failure"); + return success; + }) + .subscribeOn(scheduler) + .delay(1, TimeUnit.SECONDS, scheduler), + rxBleGattCallback.observeDisconnect() + ); + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/SafeExecutor.java b/android/src/main/java/com/bleplx/adapter/utils/SafeExecutor.java new file mode 100644 index 00000000..c4943fb9 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/SafeExecutor.java @@ -0,0 +1,33 @@ +package com.bleplx.adapter.utils; + +import androidx.annotation.Nullable; + +import com.bleplx.adapter.OnErrorCallback; +import com.bleplx.adapter.OnSuccessCallback; +import com.bleplx.adapter.errors.BleError; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class SafeExecutor { + + private final OnSuccessCallback successCallback; + private final OnErrorCallback errorCallback; + private final AtomicBoolean wasExecuted = new AtomicBoolean(false); + + public SafeExecutor(@Nullable OnSuccessCallback successCallback, @Nullable OnErrorCallback errorCallback) { + this.successCallback = successCallback; + this.errorCallback = errorCallback; + } + + public void success(T data) { + if (wasExecuted.compareAndSet(false, true) && successCallback != null) { + successCallback.onSuccess(data); + } + } + + public void error(BleError error) { + if (wasExecuted.compareAndSet(false, true) && errorCallback != null) { + errorCallback.onError(error); + } + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/ServiceFactory.java b/android/src/main/java/com/bleplx/adapter/utils/ServiceFactory.java new file mode 100644 index 00000000..cd58e485 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/ServiceFactory.java @@ -0,0 +1,16 @@ +package com.bleplx.adapter.utils; + +import android.bluetooth.BluetoothGattService; + +import com.bleplx.adapter.Service; + +public class ServiceFactory { + + public Service create(String deviceId, BluetoothGattService btGattService) { + return new Service( + IdGenerator.getIdForKey(new IdGeneratorKey(deviceId, btGattService.getUuid(), btGattService.getInstanceId())), + deviceId, + btGattService + ); + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/UUIDConverter.java b/android/src/main/java/com/bleplx/adapter/utils/UUIDConverter.java new file mode 100755 index 00000000..141172d2 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/UUIDConverter.java @@ -0,0 +1,51 @@ +package com.bleplx.adapter.utils; + + +import java.util.UUID; + +public class UUIDConverter { + + private static String baseUUIDPrefix = "0000"; + private static String baseUUIDSuffix = "-0000-1000-8000-00805F9B34FB"; + + public static UUID convert(String sUUID) { + if (sUUID == null) return null; + + if (sUUID.length() == 4) { + sUUID = baseUUIDPrefix + sUUID + baseUUIDSuffix; + } else if (sUUID.length() == 8) { + sUUID = sUUID + baseUUIDSuffix; + } + + try { + return UUID.fromString(sUUID); + } catch (Throwable e) { + return null; + } + } + + public static UUID[] convert(String... sUUIDs) { + UUID[] UUIDs = new UUID[sUUIDs.length]; + for (int i = 0; i < sUUIDs.length; i++) { + + if (sUUIDs[i] == null) return null; + + if (sUUIDs[i].length() == 4) { + sUUIDs[i] = baseUUIDPrefix + sUUIDs[i] + baseUUIDSuffix; + } else if (sUUIDs[i].length() == 8) { + sUUIDs[i] = sUUIDs[i] + baseUUIDSuffix; + } + + try { + UUIDs[i] = UUID.fromString(sUUIDs[i]); + } catch (Throwable e) { + return null; + } + } + return UUIDs; + } + + public static String fromUUID(UUID uuid) { + return uuid.toString().toLowerCase(); + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/mapper/RxBleDeviceToDeviceMapper.java b/android/src/main/java/com/bleplx/adapter/utils/mapper/RxBleDeviceToDeviceMapper.java new file mode 100644 index 00000000..67c42229 --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/mapper/RxBleDeviceToDeviceMapper.java @@ -0,0 +1,19 @@ +package com.bleplx.adapter.utils.mapper; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bleplx.adapter.Device; +import com.polidea.rxandroidble2.RxBleConnection; +import com.polidea.rxandroidble2.RxBleDevice; + +public class RxBleDeviceToDeviceMapper { + + public Device map(@NonNull RxBleDevice rxDevice, @Nullable RxBleConnection connection) { + Device device = new Device(rxDevice.getMacAddress(), rxDevice.getName()); + if (connection != null) { + device.setMtu(connection.getMtu()); + } + return device; + } +} diff --git a/android/src/main/java/com/bleplx/adapter/utils/mapper/RxScanResultToScanResultMapper.java b/android/src/main/java/com/bleplx/adapter/utils/mapper/RxScanResultToScanResultMapper.java new file mode 100644 index 00000000..3417b63a --- /dev/null +++ b/android/src/main/java/com/bleplx/adapter/utils/mapper/RxScanResultToScanResultMapper.java @@ -0,0 +1,20 @@ +package com.bleplx.adapter.utils.mapper; + +import com.bleplx.adapter.AdvertisementData; +import com.bleplx.adapter.ScanResult; +import com.bleplx.adapter.utils.Constants; + +public class RxScanResultToScanResultMapper { + + public ScanResult map(com.polidea.rxandroidble2.scan.ScanResult rxScanResult) { + return new ScanResult( + rxScanResult.getBleDevice().getMacAddress(), + rxScanResult.getBleDevice().getName(), + rxScanResult.getRssi(), + Constants.MINIMUM_MTU, + false, //isConnectable flag is not available on Android + null, //overflowServiceUUIDs are not available on Android + AdvertisementData.parseScanResponseData(rxScanResult.getScanRecord().getBytes()) + ); + } +} diff --git a/android/src/main/java/com/bleplx/converter/BleErrorToJsObjectConverter.java b/android/src/main/java/com/bleplx/converter/BleErrorToJsObjectConverter.java index ac15a1b7..22168eb3 100644 --- a/android/src/main/java/com/bleplx/converter/BleErrorToJsObjectConverter.java +++ b/android/src/main/java/com/bleplx/converter/BleErrorToJsObjectConverter.java @@ -4,7 +4,7 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; -import com.polidea.multiplatformbleadapter.errors.BleError; +import com.bleplx.adapter.errors.BleError; public class BleErrorToJsObjectConverter { @@ -63,4 +63,4 @@ private void appendString(StringBuilder stringBuilder, String key, String value) stringBuilder.append("\""); } } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/bleplx/converter/CharacteristicToJsObjectConverter.java b/android/src/main/java/com/bleplx/converter/CharacteristicToJsObjectConverter.java index 1be25281..292499bf 100644 --- a/android/src/main/java/com/bleplx/converter/CharacteristicToJsObjectConverter.java +++ b/android/src/main/java/com/bleplx/converter/CharacteristicToJsObjectConverter.java @@ -2,9 +2,9 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; -import com.polidea.multiplatformbleadapter.Characteristic; -import com.polidea.multiplatformbleadapter.utils.Base64Converter; -import com.polidea.multiplatformbleadapter.utils.UUIDConverter; +import com.bleplx.adapter.Characteristic; +import com.bleplx.adapter.utils.Base64Converter; +import com.bleplx.adapter.utils.UUIDConverter; public class CharacteristicToJsObjectConverter extends JSObjectConverter { diff --git a/android/src/main/java/com/bleplx/converter/DescriptorToJsObjectConverter.java b/android/src/main/java/com/bleplx/converter/DescriptorToJsObjectConverter.java index 68e9f52f..88cb16c0 100644 --- a/android/src/main/java/com/bleplx/converter/DescriptorToJsObjectConverter.java +++ b/android/src/main/java/com/bleplx/converter/DescriptorToJsObjectConverter.java @@ -2,9 +2,9 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; -import com.polidea.multiplatformbleadapter.Descriptor; -import com.polidea.multiplatformbleadapter.utils.Base64Converter; -import com.polidea.multiplatformbleadapter.utils.UUIDConverter; +import com.bleplx.adapter.Descriptor; +import com.bleplx.adapter.utils.Base64Converter; +import com.bleplx.adapter.utils.UUIDConverter; public class DescriptorToJsObjectConverter extends JSObjectConverter { @@ -36,4 +36,4 @@ public WritableMap toJSObject(Descriptor descriptor) { js.putString(Metadata.VALUE, descriptor.getValue() != null ? Base64Converter.encode(descriptor.getValue()) : null); return js; } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/bleplx/converter/DeviceToJsObjectConverter.java b/android/src/main/java/com/bleplx/converter/DeviceToJsObjectConverter.java index e4e7166c..f439c761 100644 --- a/android/src/main/java/com/bleplx/converter/DeviceToJsObjectConverter.java +++ b/android/src/main/java/com/bleplx/converter/DeviceToJsObjectConverter.java @@ -2,8 +2,8 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; -import com.polidea.multiplatformbleadapter.Device; -import com.polidea.multiplatformbleadapter.utils.Constants; +import com.bleplx.adapter.Device; +import com.bleplx.adapter.utils.Constants; public class DeviceToJsObjectConverter extends JSObjectConverter { diff --git a/android/src/main/java/com/bleplx/converter/ScanResultToJsObjectConverter.java b/android/src/main/java/com/bleplx/converter/ScanResultToJsObjectConverter.java index eabe3dbd..40cb4ff6 100644 --- a/android/src/main/java/com/bleplx/converter/ScanResultToJsObjectConverter.java +++ b/android/src/main/java/com/bleplx/converter/ScanResultToJsObjectConverter.java @@ -5,10 +5,10 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; -import com.polidea.multiplatformbleadapter.AdvertisementData; -import com.polidea.multiplatformbleadapter.ScanResult; -import com.polidea.multiplatformbleadapter.utils.Base64Converter; -import com.polidea.multiplatformbleadapter.utils.UUIDConverter; +import com.bleplx.adapter.AdvertisementData; +import com.bleplx.adapter.ScanResult; +import com.bleplx.adapter.utils.Base64Converter; +import com.bleplx.adapter.utils.UUIDConverter; import java.util.Map; import java.util.UUID; diff --git a/android/src/main/java/com/bleplx/converter/ServiceToJsObjectConverter.java b/android/src/main/java/com/bleplx/converter/ServiceToJsObjectConverter.java index f713ac7a..f1af91b3 100644 --- a/android/src/main/java/com/bleplx/converter/ServiceToJsObjectConverter.java +++ b/android/src/main/java/com/bleplx/converter/ServiceToJsObjectConverter.java @@ -2,8 +2,8 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; -import com.polidea.multiplatformbleadapter.Service; -import com.polidea.multiplatformbleadapter.utils.UUIDConverter; +import com.bleplx.adapter.Service; +import com.bleplx.adapter.utils.UUIDConverter; public class ServiceToJsObjectConverter extends JSObjectConverter {