From 8176a4ad2276d0f343f7b5ff1f6ecf7a329c7541 Mon Sep 17 00:00:00 2001 From: Jukka Oikarinen Date: Fri, 5 Nov 2021 10:00:56 +0200 Subject: [PATCH] Issue #209: Fixed. api.shutDown did not correctly release it resources. --- .../library/build.gradle | 2 +- .../ble/exceptions/BleNotAvailableInDevice.kt | 6 + .../api/ble/exceptions/BleStartScanError.java | 14 - .../api/ble/exceptions/BleStartScanError.kt | 3 + .../BleAdvertisementContent.java | 6 +- .../ble/model/gatt/BleGattTxInterface.java | 6 +- .../ble/model/gatt/client/BleHrClient.java | 7 +- .../ble/model/gatt/client/pmd/BlePMDClient.kt | 20 +- .../ble/model/gatt/client/pmd/PmdFeature.kt | 5 +- .../gatt/client/pmd/PmdMeasurementType.kt | 1 + .../model/gatt/client/pmd/model/PpgData.kt | 2 - .../gatt/client/pmd/model/PressureData.kt | 22 +- .../gatt/client/pmd/model/TemperatureData.kt | 60 +++ .../ble/bluedroid/host/BDBondingListener.java | 4 +- .../bluedroid/host/BDDeviceListenerImpl.java | 375 +++++++++-------- .../bluedroid/host/BDDeviceSessionImpl.java | 23 +- .../ble/bluedroid/host/BDScanCallback.java | 388 ------------------ .../ble/bluedroid/host/BDScanCallback.kt | 329 +++++++++++++++ .../host/connection/ConnectionHandler.java | 16 +- .../java/com/polar/sdk/api/PolarBleApi.java | 6 +- .../polar/sdk/api/PolarBleApiDefaultImpl.kt | 2 +- .../java/com/polar/sdk/impl/BDBleApiImpl.java | 175 +++++--- .../pmd/{ => model}/GnssLocationDataTest.kt | 4 +- .../client/pmd/{ => model}/PpgDataTest.kt | 4 +- .../client/pmd/{ => model}/PpiDataTest.kt | 4 +- .../pmd/{ => model}/PressureDataTest.kt | 29 +- .../client/pmd/model/TemperatureDataTest.kt | 67 +++ 27 files changed, 890 insertions(+), 690 deletions(-) create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleNotAvailableInDevice.kt delete mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleStartScanError.java create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleStartScanError.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/TemperatureData.kt delete mode 100755 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDScanCallback.java create mode 100755 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDScanCallback.kt rename sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/{ => model}/GnssLocationDataTest.kt (99%) rename sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/{ => model}/PpgDataTest.kt (99%) rename sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/{ => model}/PpiDataTest.kt (99%) rename sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/{ => model}/PressureDataTest.kt (75%) create mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/TemperatureDataTest.kt diff --git a/sources/Android/android-communications/library/build.gradle b/sources/Android/android-communications/library/build.gradle index e4216acf..aa849743 100755 --- a/sources/Android/android-communications/library/build.gradle +++ b/sources/Android/android-communications/library/build.gradle @@ -155,7 +155,7 @@ protobuf { dependencies { implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' - implementation 'io.reactivex.rxjava3:rxjava:3.0.4' + implementation 'io.reactivex.rxjava3:rxjava:3.1.1' implementation 'commons-io:commons-io:2.10.0' implementation 'androidx.annotation:annotation:1.2.0' implementation "androidx.core:core-ktx:1.6.0" diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleNotAvailableInDevice.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleNotAvailableInDevice.kt new file mode 100644 index 00000000..9e1d1871 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleNotAvailableInDevice.kt @@ -0,0 +1,6 @@ +package com.polar.androidcommunications.api.ble.exceptions + +/** + * Error indicating the device is not supporting BLE + */ +class BleNotAvailableInDevice(message: String) : Exception(message) \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleStartScanError.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleStartScanError.java deleted file mode 100644 index d84e5a8e..00000000 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleStartScanError.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.polar.androidcommunications.api.ble.exceptions; - -public class BleStartScanError extends Exception { - private final int error; - - public BleStartScanError(String message, int error) { - super(message + " failed with error: " + error); - this.error = error; - } - - public int getError() { - return error; - } -} diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleStartScanError.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleStartScanError.kt new file mode 100644 index 00000000..226ef99a --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleStartScanError.kt @@ -0,0 +1,3 @@ +package com.polar.androidcommunications.api.ble.exceptions + +class BleStartScanError(message: String, val error: Int) : Exception("$message failed with error: $error") \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/advertisement/BleAdvertisementContent.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/advertisement/BleAdvertisementContent.java index 1d9b1370..d931ffc3 100644 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/advertisement/BleAdvertisementContent.java +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/advertisement/BleAdvertisementContent.java @@ -1,5 +1,8 @@ package com.polar.androidcommunications.api.ble.model.advertisement; +import static com.polar.androidcommunications.api.ble.model.polar.PolarAdvDataUtility.getPolarModelNameFromAdvLocalName; +import static com.polar.androidcommunications.api.ble.model.polar.PolarAdvDataUtility.isPolarDevice; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -12,9 +15,6 @@ import java.util.List; import java.util.Objects; -import static com.polar.androidcommunications.api.ble.model.polar.PolarAdvDataUtility.getPolarModelNameFromAdvLocalName; -import static com.polar.androidcommunications.api.ble.model.polar.PolarAdvDataUtility.isPolarDevice; - public class BleAdvertisementContent { public static final String BLE_ADV_POLAR_PREFIX_IN_LOCAL_NAME = "Polar"; diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/BleGattTxInterface.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/BleGattTxInterface.java index 0f6cfbb9..b2e681a1 100644 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/BleGattTxInterface.java +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/BleGattTxInterface.java @@ -1,5 +1,9 @@ package com.polar.androidcommunications.api.ble.model.gatt; +import com.polar.androidcommunications.api.ble.exceptions.BleCharacteristicNotFound; +import com.polar.androidcommunications.api.ble.exceptions.BleGattNotInitialized; +import com.polar.androidcommunications.api.ble.exceptions.BleServiceNotFound; + import java.util.List; import java.util.UUID; @@ -13,7 +17,7 @@ public interface BleGattTxInterface { void readValue(BleGattBase gattServiceBase, UUID serviceUuid, UUID characteristicUuid) throws Exception; - void setCharacteristicNotify(BleGattBase gattServiceBase, UUID serviceUuid, UUID characteristicUuid, boolean enable) throws Exception; + void setCharacteristicNotify(BleGattBase gattServiceBase, UUID serviceUuid, UUID characteristicUuid, boolean enable) throws BleCharacteristicNotFound, BleServiceNotFound, BleGattNotInitialized; boolean isConnected(); // for client diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/BleHrClient.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/BleHrClient.java index d15f38af..a5b44e3d 100755 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/BleHrClient.java +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/BleHrClient.java @@ -126,7 +126,12 @@ public Flowable observeHrNotifications(final boolean checkCo .doFinally(() -> { BleLogger.d(TAG, "Stop observing HR"); removeCharacteristicNotification(HR_MEASUREMENT); - getTxInterface().setCharacteristicNotify(this, HR_SERVICE, HR_MEASUREMENT, false); + try { + getTxInterface().setCharacteristicNotify(this, HR_SERVICE, HR_MEASUREMENT, false); + } catch (Exception e) { + // this may happen if connection is already closed, no need sent the exception to downstream + BleLogger.d(TAG, "HR client is not able to set characteristic notify to false. Reason " + e.toString()); + } }); } } diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClient.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClient.kt index 81b2d3b4..4571574c 100644 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClient.kt +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClient.kt @@ -22,9 +22,10 @@ import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import kotlin.math.ceil + /** -* BLE Client for Polar Measurement Data Service (aka. PMD service) -* */ + * BLE Client for Polar Measurement Data Service (aka. PMD service) + * */ class BlePMDClient(txInterface: BleGattTxInterface) : BleGattBase(txInterface, PMD_SERVICE) { private val pmdCpInputQueue = LinkedBlockingQueue>() private val ecgObservers = AtomicSet>() @@ -35,6 +36,7 @@ class BlePMDClient(txInterface: BleGattTxInterface) : BleGattBase(txInterface, P private val ppiObservers = AtomicSet>() private val pressureObservers = AtomicSet>() private val locationObservers = AtomicSet>() + private val temperatureObservers = AtomicSet>() private val rdObservers = AtomicSet>() private var pmdFeatureData: ByteArray? = null private val controlPointMutex = Object() @@ -95,7 +97,7 @@ class BlePMDClient(txInterface: BleGattTxInterface) : BleGattBase(txInterface, P val ieee754 = it return java.lang.Float.intBitsToFloat(ieee754) } - BleLogger.e(TAG, "No factor found for type: $type") + BleLogger.w(TAG, "No factor found for type: $type") return 1.0f } @@ -169,6 +171,13 @@ class BlePMDClient(txInterface: BleGattTxInterface) : BleGattBase(txInterface, P emitter.onNext(GnssLocationData.parseDataFromDataFrame(isCompressedFrameType, frameType, content, factor, timeStamp)) } } + PmdMeasurementType.TEMPERATURE -> { + val factor = fetchFactor(PmdMeasurementType.TEMPERATURE) + RxUtils.emitNext(temperatureObservers) { emitter: FlowableEmitter -> + emitter.onNext(TemperatureData.parseDataFromDataFrame(isCompressedFrameType, frameType, content, factor, timeStamp)) + } + } + else -> { val rdData = ByteArray(data.size - 1) System.arraycopy(data, 1, content, 0, content.size) @@ -423,6 +432,10 @@ class BlePMDClient(txInterface: BleGattTxInterface) : BleGattBase(txInterface, P return RxUtils.monitorNotifications(locationObservers, txInterface, checkConnection) } + fun monitorTemperatureNotifications(checkConnection: Boolean): Flowable { + return RxUtils.monitorNotifications(temperatureObservers, txInterface, checkConnection) + } + override fun clientReady(checkConnection: Boolean): Completable { return Completable.concatArray( waitNotificationEnabled(PMD_CP, true), @@ -453,6 +466,7 @@ class BlePMDClient(txInterface: BleGattTxInterface) : BleGattBase(txInterface, P RxUtils.postExceptionAndClearList(magnetometerObservers, throwable) RxUtils.postExceptionAndClearList(pressureObservers, throwable) RxUtils.postExceptionAndClearList(locationObservers, throwable) + RxUtils.postExceptionAndClearList(temperatureObservers, throwable) } companion object { diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdFeature.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdFeature.kt index 140123a1..1d41f361 100644 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdFeature.kt +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdFeature.kt @@ -19,11 +19,14 @@ class PmdFeature(data: ByteArray) { @JvmField val magnetometerSupported: Boolean = (data[1].toUInt() and 0x40u) != 0u + @JvmField + val locationSupported: Boolean = (data[2].toUInt() and 0x04u) != 0u + @JvmField val barometerSupported: Boolean = (data[2].toUInt() and 0x08u) != 0u @JvmField - val locationSupported: Boolean = (data[2].toUInt() and 0x04u) != 0u + val temperatureSupported: Boolean = (data[2].toUInt() and 0x10u) != 0u @JvmField val sdkModeSupported: Boolean = (data[2].toUInt() and 0x02u) != 0u diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdMeasurementType.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdMeasurementType.kt index 3a33ce4b..6b2336e0 100644 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdMeasurementType.kt +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdMeasurementType.kt @@ -10,6 +10,7 @@ enum class PmdMeasurementType(val numVal: Int) { SDK_MODE(9), LOCATION(10), PRESSURE(11), + TEMPERATURE(12), UNKNOWN_TYPE(0xff); companion object { diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpgData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpgData.kt index bcfc0d1a..ab8879b4 100644 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpgData.kt +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpgData.kt @@ -1,6 +1,5 @@ package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model -import com.polar.androidcommunications.api.ble.BleLogger import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClientUtils import com.polar.androidcommunications.common.ble.BleUtils @@ -83,7 +82,6 @@ class PpgData internal constructor(val timeStamp: Long) { var dataType4Counter = 0 private fun dataFromRawType4(frame: ByteArray, timeStamp: Long): PpgData { dataType4Counter++ - BleLogger.d("TESTING", "PPG Data4 received. Counter: $dataType4Counter timeStamp: $timeStamp") val ppgData = PpgData(timeStamp) var offset = 0 diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PressureData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PressureData.kt index 48196e2f..0045ad3e 100644 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PressureData.kt +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PressureData.kt @@ -2,6 +2,7 @@ package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient.PmdDataFieldEncoding +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClientUtils import java.lang.Float.intBitsToFloat import java.util.* @@ -23,15 +24,18 @@ class PressureData internal constructor(val timeStamp: Long) { fun parseDataFromDataFrame(isCompressed: Boolean, frameType: BlePMDClient.PmdDataFrameType, frame: ByteArray, factor: Float, timeStamp: Long): PressureData { return if (isCompressed) { when (frameType) { - BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromType0(frame, factor, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromCompressedType0(frame, factor, timeStamp) else -> throw java.lang.Exception("Compressed FrameType: $frameType is not supported by Pressure data parser") } } else { - throw java.lang.Exception("Raw FrameType: $frameType is not supported by Pressure data parser") + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromRawType0(frame, timeStamp) + else -> throw java.lang.Exception("Raw FrameType: $frameType is not supported by Pressure data parser") + } } } - private fun dataFromType0(frame: ByteArray, factor: Float, timeStamp: Long): PressureData { + private fun dataFromCompressedType0(frame: ByteArray, factor: Float, timeStamp: Long): PressureData { val samples = BlePMDClient.parseDeltaFramesAll(frame, 1, 32, PmdDataFieldEncoding.FLOAT_IEEE754) val pressureData = PressureData(timeStamp) for (sample in samples) { @@ -40,5 +44,17 @@ class PressureData internal constructor(val timeStamp: Long) { } return pressureData } + + private fun dataFromRawType0(frame: ByteArray, timeStamp: Long): PressureData { + val pressureData = PressureData(timeStamp) + var offset = 0 + + while (offset < frame.size) { + val pressure = BlePMDClientUtils.parseFrameDataField(frame.sliceArray(offset..(offset + 3)), PmdDataFieldEncoding.FLOAT_IEEE754) as Float + offset += 4 + pressureData.pressureSamples.add(PressureSample(pressure)) + } + return pressureData + } } } \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/TemperatureData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/TemperatureData.kt new file mode 100644 index 00000000..c34b75c8 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/TemperatureData.kt @@ -0,0 +1,60 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient.PmdDataFieldEncoding +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClientUtils +import java.lang.Float.intBitsToFloat +import java.util.* + +/** + * Temperature data + * @param timeStamp ns in epoch time. The time stamp represent time of last sample in [temperatureSamples] list + */ +class TemperatureData internal constructor(val timeStamp: Long) { + + data class TemperatureSample internal constructor( + // Sample contains signed temperature value in celcius + val temperature: Float + ) + + @JvmField + val temperatureSamples: MutableList = ArrayList() + + companion object { + fun parseDataFromDataFrame(isCompressed: Boolean, frameType: BlePMDClient.PmdDataFrameType, frame: ByteArray, factor: Float, timeStamp: Long): TemperatureData { + return if (isCompressed) { + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromCompressedType0(frame, factor, timeStamp) + else -> throw java.lang.Exception("Compressed FrameType: $frameType is not supported by Temperature data parser") + } + } else { + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromRawType0(frame, timeStamp) + else -> throw java.lang.Exception("Raw FrameType: $frameType is not supported by Temperature data parser") + } + } + } + + private fun dataFromCompressedType0(frame: ByteArray, factor: Float, timeStamp: Long): TemperatureData { + val samples = BlePMDClient.parseDeltaFramesAll(frame, 1, 32, PmdDataFieldEncoding.FLOAT_IEEE754) + val temperatureData = TemperatureData(timeStamp) + for (sample in samples) { + val pressure = if (factor != 1.0f) intBitsToFloat(sample[0]) * factor else intBitsToFloat(sample[0]) + temperatureData.temperatureSamples.add(TemperatureSample(pressure)) + } + return temperatureData + } + + private fun dataFromRawType0(frame: ByteArray, timeStamp: Long): TemperatureData { + val temperatureData = TemperatureData(timeStamp) + var offset = 0 + + while (offset < frame.size) { + val temperature = BlePMDClientUtils.parseFrameDataField(frame.sliceArray(offset..(offset + 3)), PmdDataFieldEncoding.FLOAT_IEEE754) as Float + offset += 4 + temperatureData.temperatureSamples.add(TemperatureSample(temperature)) + } + return temperatureData + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDBondingListener.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDBondingListener.java index 3a058a38..ec85de4d 100644 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDBondingListener.java +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDBondingListener.java @@ -20,7 +20,7 @@ interface AuthenticationObserverInterface { void bondNone(); } - private final static String TAG = BDBondingListener.class.getSimpleName(); + private static final String TAG = BDBondingListener.class.getSimpleName(); private final Context context; private final AtomicSet authenticationObservers = new AtomicSet<>(); @@ -38,7 +38,7 @@ void stopBroadcastReceiver() { } } - static abstract class BondingObserver implements AuthenticationObserverInterface { + abstract static class BondingObserver implements AuthenticationObserverInterface { private final BluetoothDevice device; BondingObserver(BluetoothDevice device) { diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceListenerImpl.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceListenerImpl.java index 930bad50..285b6412 100755 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceListenerImpl.java +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceListenerImpl.java @@ -15,6 +15,7 @@ import com.polar.androidcommunications.api.ble.BleDeviceListener; import com.polar.androidcommunications.api.ble.BleLogger; +import com.polar.androidcommunications.api.ble.exceptions.BleNotAvailableInDevice; import com.polar.androidcommunications.api.ble.exceptions.BleStartScanError; import com.polar.androidcommunications.api.ble.model.BleDeviceSession; import com.polar.androidcommunications.api.ble.model.advertisement.BleAdvertisementContent; @@ -43,12 +44,7 @@ import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.subjects.BehaviorSubject; -public class BDDeviceListenerImpl extends BleDeviceListener implements - BDScanCallback.BDScanCallbackInterface, - ScannerInterface, - ConnectionInterface, - BDPowerListener.BlePowerState, - ConnectionHandlerObserver { +public class BDDeviceListenerImpl extends BleDeviceListener { private static final String TAG = BDDeviceListenerImpl.class.getSimpleName(); private BluetoothAdapter bluetoothAdapter; @@ -66,18 +62,24 @@ public class BDDeviceListenerImpl extends BleDeviceListener implements private BlePowerStateChangedCallback powerStateChangedCallback = null; public BDDeviceListenerImpl(@NonNull final Context context, - @NonNull Set> clients) { + @NonNull Set> clients) throws BleNotAvailableInDevice { super(clients); this.context = context; btManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); + if (btManager != null) { bluetoothAdapter = btManager.getAdapter(); } - connectionHandler = new ConnectionHandler(this, this, this); + // Guard + if (btManager == null || bluetoothAdapter == null) { + throw new BleNotAvailableInDevice("Device doesn't support BLE"); + } + + connectionHandler = new ConnectionHandler(connectionInterface, scannerInterface, connectionHandlerObserver); gattCallback = new BDGattCallback(context, connectionHandler, sessions); bondingManager = new BDBondingListener(context); - scanCallback = new BDScanCallback(context, btManager, this); - powerManager = new BDPowerListener(bluetoothAdapter, context, this); + scanCallback = new BDScanCallback(context, bluetoothAdapter, scanCallbackInterface); + powerManager = new BDPowerListener(bluetoothAdapter, context, blePowerStateListener); } @Override @@ -199,58 +201,6 @@ public int removeAllSessions(@NonNull Set i return count; } - @Override - public void connectDevice(final BDDeviceSessionImpl session) { - BluetoothGatt gatt; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - int mask = BluetoothDevice.PHY_LE_1M_MASK; - if (bluetoothAdapter.isLe2MPhySupported()) mask |= BluetoothDevice.PHY_LE_2M_MASK; - gatt = session.getBluetoothDevice().connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE, mask); - } else { - gatt = session.getBluetoothDevice().connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE); - } - } else { - gatt = session.getBluetoothDevice().connectGatt(context, false, gattCallback); - } - synchronized (session.getGattMutex()) { - session.setGatt(gatt); - } - } - - @Override - public void disconnectDevice(final BDDeviceSessionImpl session) { - synchronized (session.getGattMutex()) { - if (session.getGatt() != null) { - session.getGatt().disconnect(); - } - } - } - - @Override - public void cancelDeviceConnection(BDDeviceSessionImpl session) { - synchronized (session.getGattMutex()) { - if (session.getGatt() != null) { - session.getGatt().disconnect(); - } - } - } - - @Override - public boolean isPowered() { - return bleActive(); - } - - @Override - public void connectionHandlerResumeScanning() { - scanCallback.startScan(); - } - - @Override - public void connectionHandlerRequestStopScanning() { - scanCallback.stopScan(); - } - @Override public void setPowerMode(@PowerMode int mode) { switch (mode) { @@ -267,64 +217,198 @@ public void setPowerMode(@PowerMode int mode) { } } - @Override - public void deviceDiscovered(BluetoothDevice device, int rssi, byte[] scanRecord, BleUtils.EVENT_TYPE type) { - BDDeviceSessionImpl deviceSession = sessions.getSession(device); - HashMap advData = BleUtils.advertisementBytes2Map(scanRecord); - final String manufacturer = Build.MANUFACTURER; - final String name = device.getName(); - if (name != null && manufacturer.equalsIgnoreCase("samsung")) { - // BIG NOTE, this is workaround for uber stupid samsung bug where they mess up whoms advertisement data belongs to who - advData.remove(BleUtils.AD_TYPE.GAP_ADTYPE_LOCAL_NAME_SHORT); - advData.put(BleUtils.AD_TYPE.GAP_ADTYPE_LOCAL_NAME_COMPLETE, name.getBytes()); + private final ConnectionInterface connectionInterface = new ConnectionInterface() { + @Override + public void connectDevice(final BDDeviceSessionImpl session) { + BluetoothGatt gatt; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int mask = BluetoothDevice.PHY_LE_1M_MASK; + if (bluetoothAdapter.isLe2MPhySupported()) + mask |= BluetoothDevice.PHY_LE_2M_MASK; + gatt = session.getBluetoothDevice().connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE, mask); + } else { + gatt = session.getBluetoothDevice().connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE); + } + } else { + gatt = session.getBluetoothDevice().connectGatt(context, false, gattCallback); + } + synchronized (session.getGattMutex()) { + session.setGatt(gatt); + } + } + + @Override + public void disconnectDevice(final BDDeviceSessionImpl session) { + synchronized (session.getGattMutex()) { + if (session.getGatt() != null) { + session.getGatt().disconnect(); + } + } + } + + @Override + public void cancelDeviceConnection(BDDeviceSessionImpl session) { + synchronized (session.getGattMutex()) { + if (session.getGatt() != null) { + session.getGatt().disconnect(); + } + } + } + + @Override + public boolean isPowered() { + return bleActive(); + } + }; + + private final BDScanCallback.BDScanCallbackInterface scanCallbackInterface = new BDScanCallback.BDScanCallbackInterface() { + @Override + public boolean isScanningNeeded() { + return observers.size() != 0 || sessions.fetch(smartPolarDeviceSession1 -> smartPolarDeviceSession1.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK) != null; + } + + @Override + public void scanStartError(int error) { + RxUtils.postError(observers, new BleStartScanError("scan start failed ", error)); } - if (deviceSession == null) { - final BleAdvertisementContent content = new BleAdvertisementContent(); - content.processAdvertisementData(advData, type, rssi); - if (preFilter == null || preFilter.process(content)) { - if (content.getPolarHrAdvertisement().isPresent() && - content.getPolarDeviceIdInt() != 0 && - (content.getPolarDeviceType().equals("H10") || - content.getPolarDeviceType().equals("H9"))) { - // check if old can be found, NOTE this is a special case for H10 only (random static address) - BDDeviceSessionImpl oldSession = sessions.fetch(smartPolarDeviceSession1 -> smartPolarDeviceSession1.getAdvertisementContent().getPolarDeviceId().equals(content.getPolarDeviceId())); - if (oldSession != null && (oldSession.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_CLOSED || - oldSession.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK)) { - BleLogger.d(TAG, "old polar device found name: " + oldSession.getAdvertisementContent().getName() + " dev name: " + device.getName() + " old name: " + oldSession.getBluetoothDevice().getName() + " old addr: " + oldSession.getAddress() + " device: " + device.toString()); - oldSession.setBluetoothDevice(device); - deviceSession = oldSession; + public void deviceDiscovered(BluetoothDevice device, int rssi, byte[] scanRecord, BleUtils.EVENT_TYPE type) { + BDDeviceSessionImpl deviceSession = sessions.getSession(device); + HashMap advData = BleUtils.advertisementBytes2Map(scanRecord); + final String manufacturer = Build.MANUFACTURER; + final String name = device.getName(); + if (name != null && manufacturer.equalsIgnoreCase("samsung")) { + // BIG NOTE, this is workaround for uber stupid samsung bug where they mess up whoms advertisement data belongs to who + advData.remove(BleUtils.AD_TYPE.GAP_ADTYPE_LOCAL_NAME_SHORT); + advData.put(BleUtils.AD_TYPE.GAP_ADTYPE_LOCAL_NAME_COMPLETE, name.getBytes()); + } + + if (deviceSession == null) { + final BleAdvertisementContent content = new BleAdvertisementContent(); + content.processAdvertisementData(advData, type, rssi); + if (preFilter == null || preFilter.process(content)) { + if (content.getPolarHrAdvertisement().isPresent() + && content.getPolarDeviceIdInt() != 0 + && (content.getPolarDeviceType().equals("H10") || content.getPolarDeviceType().equals("H9"))) { + // check if old can be found, NOTE this is a special case for H10 only (random static address) + BDDeviceSessionImpl oldSession = sessions.fetch(smartPolarDeviceSession1 -> smartPolarDeviceSession1.getAdvertisementContent().getPolarDeviceId().equals(content.getPolarDeviceId())); + if (oldSession != null + && (oldSession.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_CLOSED + || oldSession.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK)) { + BleLogger.d(TAG, "old polar device found name: " + oldSession.getAdvertisementContent().getName() + " dev name: " + device.getName() + " old name: " + oldSession.getBluetoothDevice().getName() + " old addr: " + oldSession.getAddress() + " device: " + device.toString()); + oldSession.setBluetoothDevice(device); + deviceSession = oldSession; + deviceSession.getAdvertisementContent().processAdvertisementData(advData, type, rssi); + } + } + if (deviceSession == null) { + deviceSession = new BDDeviceSessionImpl(context, device, scanCallback, bondingManager, factory); deviceSession.getAdvertisementContent().processAdvertisementData(advData, type, rssi); + BleLogger.d(TAG, "new device allocated name: " + deviceSession.getAdvertisementContent().getName()); + sessions.addSession(deviceSession); } - } - if (deviceSession == null) { - deviceSession = new BDDeviceSessionImpl(context, device, scanCallback, bondingManager, factory); - deviceSession.getAdvertisementContent().processAdvertisementData(advData, type, rssi); - BleLogger.d(TAG, "new device allocated name: " + deviceSession.getAdvertisementContent().getName()); - sessions.addSession(deviceSession); + } else { + // device is not desired + return; } } else { - // device is not desired - return; + deviceSession.getAdvertisementContent().processAdvertisementData(advData, type, rssi); } - } else { - deviceSession.getAdvertisementContent().processAdvertisementData(advData, type, rssi); + + connectionHandler.advertisementHeadReceived(deviceSession); + final BDDeviceSessionImpl finalDeviceSession = deviceSession; + RxUtils.emitNext(observers, object -> object.onNext(finalDeviceSession)); } + }; - connectionHandler.advertisementHeadReceived(deviceSession); - final BDDeviceSessionImpl finalDeviceSession = deviceSession; - RxUtils.emitNext(observers, object -> object.onNext(finalDeviceSession)); - } + private final ScannerInterface scannerInterface = new ScannerInterface() { + @Override + public void connectionHandlerResumeScanning() { + scanCallback.startScan(); + } - @Override - public void scanStartError(int error) { - RxUtils.postError(observers, new BleStartScanError("scan start failed ", error)); - } + @Override + public void connectionHandlerRequestStopScanning() { + scanCallback.stopScan(); + } + }; - @Override - public boolean isScanningNeeded() { - return observers.size() != 0 || sessions.fetch(smartPolarDeviceSession1 -> smartPolarDeviceSession1.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK) != null; - } + private final ConnectionHandlerObserver connectionHandlerObserver = new ConnectionHandlerObserver() { + @Override + public void deviceSessionStateChanged(@NonNull BDDeviceSessionImpl session) { + if (sessions.fetch(smartPolarDeviceSession1 -> smartPolarDeviceSession1.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK) != null) { + scanCallback.clientAdded(); + } else { + scanCallback.clientRemoved(); + } + if (changedCallback != null || deviceSessionStateSubject.hasObservers()) { + if (session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK && + session.getPreviousState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN) { + //NOTE special case, we were connected so propagate closed event( informal ) + if (changedCallback != null) + changedCallback.stateChanged(session, BleDeviceSession.DeviceSessionState.SESSION_CLOSED); + deviceSessionStateSubject.onNext(new Pair<>(session, BleDeviceSession.DeviceSessionState.SESSION_CLOSED)); + if (session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK) { + if (changedCallback != null) + changedCallback.stateChanged(session, BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK); + deviceSessionStateSubject.onNext(new Pair<>(session, BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK)); + } + } else { + if (changedCallback != null) + changedCallback.stateChanged(session, session.getSessionState()); + deviceSessionStateSubject.onNext(new Pair<>(session, session.getSessionState())); + } + } + } + + @Override + public void deviceConnected(@NonNull BDDeviceSessionImpl session) { + } + + @Override + public void deviceDisconnected(@NonNull BDDeviceSessionImpl session) { + session.handleDisconnection(); + session.reset(); + } + + @Override + public void deviceConnectionCancelled(@NonNull BDDeviceSessionImpl session) { + session.reset(); + } + }; + + private final BDPowerListener.BlePowerState blePowerStateListener = new BDPowerListener.BlePowerState() { + @Override + public void blePoweredOff() { + BleLogger.e(TAG, "BLE powered off"); + scanCallback.powerOff(); + if (powerStateChangedCallback != null) { + powerStateChangedCallback.stateChanged(false); + } + for (BDDeviceSessionImpl deviceSession : sessions.getSessions().objects()) { + switch (deviceSession.getSessionState()) { + case SESSION_OPEN: + case SESSION_OPENING: + case SESSION_CLOSING: + gattCallback.onConnectionStateChange(deviceSession.getGatt(), 0, BluetoothProfile.STATE_DISCONNECTED); + break; + default: + connectionHandler.deviceDisconnected(deviceSession); + break; + } + } + } + + @Override + public void blePoweredOn() { + BleLogger.d(TAG, "BLE powered on"); + scanCallback.powerOn(); + if (powerStateChangedCallback != null) { + powerStateChangedCallback.stateChanged(true); + } + } + }; @Override public void setBlePowerStateCallback(@Nullable BlePowerStateChangedCallback cb) { @@ -361,82 +445,9 @@ public void setAutomaticReconnection(boolean automaticReconnection) { connectionHandler.setAutomaticReconnection(automaticReconnection); } - @Override - public void blePoweredOff() { - BleLogger.e(TAG, "BLE powered off"); - scanCallback.powerOff(); - if (powerStateChangedCallback != null) { - powerStateChangedCallback.stateChanged(false); - } - for (BDDeviceSessionImpl deviceSession : sessions.getSessions().objects()) { - switch (deviceSession.getSessionState()) { - case SESSION_OPEN: - case SESSION_OPENING: - case SESSION_CLOSING: - gattCallback.onConnectionStateChange(deviceSession.getGatt(), 0, BluetoothGatt.STATE_DISCONNECTED); - break; - default: - connectionHandler.deviceDisconnected(deviceSession); - break; - } - } - } - - @Override - public void blePoweredOn() { - BleLogger.d(TAG, "BLE powered on"); - scanCallback.powerOn(); - if (powerStateChangedCallback != null) { - powerStateChangedCallback.stateChanged(true); - } - } - @NonNull @Override public Observable> monitorDeviceSessionState() { return deviceSessionStateSubject; } - - @Override - public void deviceSessionStateChanged(@NonNull BDDeviceSessionImpl session) { - if (sessions.fetch(smartPolarDeviceSession1 -> smartPolarDeviceSession1.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK) != null) { - scanCallback.clientAdded(); - } else { - scanCallback.clientRemoved(); - } - if (changedCallback != null || deviceSessionStateSubject.hasObservers()) { - if (session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK && - session.getPreviousState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN) { - //NOTE special case, we were connected so propagate closed event( informal ) - if (changedCallback != null) - changedCallback.stateChanged(session, BleDeviceSession.DeviceSessionState.SESSION_CLOSED); - deviceSessionStateSubject.onNext(new Pair<>(session, BleDeviceSession.DeviceSessionState.SESSION_CLOSED)); - if (session.getSessionState() == BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK) { - if (changedCallback != null) - changedCallback.stateChanged(session, BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK); - deviceSessionStateSubject.onNext(new Pair<>(session, BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK)); - } - } else { - if (changedCallback != null) - changedCallback.stateChanged(session, session.getSessionState()); - deviceSessionStateSubject.onNext(new Pair<>(session, session.getSessionState())); - } - } - } - - @Override - public void deviceConnected(@NonNull BDDeviceSessionImpl session) { - - } - - @Override - public void deviceDisconnected(@NonNull BDDeviceSessionImpl session) { - session.handleDisconnection(); - session.reset(); - } - - @Override - public void deviceConnectionCancelled(@NonNull BDDeviceSessionImpl session) { - session.reset(); - } } diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceSessionImpl.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceSessionImpl.java index e8d4f0d3..c9ad0966 100755 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceSessionImpl.java +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceSessionImpl.java @@ -45,9 +45,9 @@ public class BDDeviceSessionImpl extends BleDeviceSession implements BleGattTxInterface { private static final UUID DESCRIPTOR_CCC = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); - private final static String TAG = BDDeviceSessionImpl.class.getSimpleName(); + private static final String TAG = BDDeviceSessionImpl.class.getSimpleName(); // gatt is the only shared object between threads - final private Object gattMutex = new Object(); + private final Object gattMutex = new Object(); Disposable serviceDiscovery; private final LinkedBlockingDeque attOperations = new LinkedBlockingDeque<>(); @@ -56,9 +56,9 @@ public class BDDeviceSessionImpl extends BleDeviceSession implements BleGattTxIn private BluetoothGatt gatt; private BDScanCallback bleScanCallback; private BDBondingListener bondingManager; - private AtomicSet>> servicesSubscriberAtomicList = new AtomicSet<>(); - private AtomicSet> rssiObservers = new AtomicSet<>(); - private List subscriptions = new ArrayList<>(); + private final AtomicSet>> servicesSubscriberAtomicList = new AtomicSet<>(); + private final AtomicSet> rssiObservers = new AtomicSet<>(); + private final List subscriptions = new ArrayList<>(); private Context context; private Handler handler; @@ -120,8 +120,8 @@ void resetGatt() { } catch (Exception e) { BleLogger.e(TAG, "gatt error: " + e.toString()); } + gatt = null; } - gatt = null; } } @@ -210,10 +210,8 @@ public boolean clearGattCache() { synchronized (gattMutex) { if (gatt != null) { try { - Method localMethod = gatt.getClass().getMethod("refresh", new Class[0]); - if (localMethod != null) { - result = ((Boolean) localMethod.invoke(gatt, new Object[0])).booleanValue(); - } + Method localMethod = gatt.getClass().getMethod("refresh"); + result = ((Boolean) localMethod.invoke(gatt, new Object[0])).booleanValue(); } catch (Exception localException) { BleLogger.e(TAG, "An exception occured while refreshing device"); } @@ -384,7 +382,8 @@ public void readValue(BleGattBase gattclient, UUID serviceUuid, UUID characteris } @Override - public void setCharacteristicNotify(BleGattBase gattclient, UUID serviceUuid, UUID characteristicUuid, boolean enable) throws Exception { + public void setCharacteristicNotify(BleGattBase gattClient, UUID serviceUuid, UUID characteristicUuid, boolean enable) + throws BleCharacteristicNotFound, BleServiceNotFound, BleGattNotInitialized { synchronized (gattMutex) { if (gatt != null) { for (BluetoothGattService service : gatt.getServices()) { @@ -421,7 +420,7 @@ public Single> monitorServicesDiscovered(final boolean checkConnectio observer[0] = subscriber; servicesSubscriberAtomicList.add(subscriber); synchronized (gattMutex) { - if (gatt != null && gatt.getServices().size() != 0) { + if (gatt != null && !gatt.getServices().isEmpty()) { List s = gatt.getServices(); List uuids = new ArrayList<>(); for (BluetoothGattService service : s) { diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDScanCallback.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDScanCallback.java deleted file mode 100755 index b2a9b815..00000000 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDScanCallback.java +++ /dev/null @@ -1,388 +0,0 @@ -package com.polar.androidcommunications.enpoints.ble.bluedroid.host; - -import android.annotation.SuppressLint; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothManager; -import android.bluetooth.le.ScanCallback; -import android.bluetooth.le.ScanFilter; -import android.bluetooth.le.ScanResult; -import android.bluetooth.le.ScanSettings; -import android.content.Context; -import android.os.Build; - -import androidx.annotation.Nullable; - -import com.polar.androidcommunications.api.ble.BleLogger; -import com.polar.androidcommunications.common.ble.BleUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.core.Scheduler; -import io.reactivex.rxjava3.disposables.Disposable; -import io.reactivex.rxjava3.schedulers.Schedulers; - -class BDScanCallback extends ScanCallback { - - private enum ScanAction { - ENTRY, - EXIT, - CLIENT_START_SCAN, - CLIENT_REMOVED, - ADMIN_START_SCAN, - ADMIN_STOP_SCAN, - BLE_POWER_OFF, - BLE_POWER_ON - } - - private enum ScannerState { - IDLE, - STOPPED, - SCANNING - } - - private final static String TAG = BDScanCallback.class.getSimpleName(); - private final BluetoothAdapter bluetoothAdapter; - private ScannerState state = ScannerState.IDLE; - private boolean lowPowerEnabled = false; - private int adminStops = 0; - private Disposable timer = null; - private List filters = null; - private final List scanPool = new ArrayList<>(); - private final Scheduler delayScheduler; - private Disposable delaySubscription; - private Disposable opportunisticScanTimer; - private boolean opportunistic = true; - - // scan window limit, for android's "is scanning too frequently" - private final static int SCAN_WINDOW_LIMIT = 30000; - - public interface BDScanCallbackInterface { - void deviceDiscovered(final BluetoothDevice device, int rssi, byte[] scanRecord, BleUtils.EVENT_TYPE type); - - void scanStartError(int error); - - boolean isScanningNeeded(); - } - - private final BDScanCallbackInterface scanCallbackInterface; - - BDScanCallback(Context context, - BluetoothManager btManager, - BDScanCallbackInterface scanCallbackInterface) { - this.scanCallbackInterface = scanCallbackInterface; - this.delayScheduler = AndroidSchedulers.from(context.getMainLooper()); - this.bluetoothAdapter = btManager.getAdapter(); - } - - void setOpportunistic(boolean opportunistic) { - this.opportunistic = opportunistic; - } - - void setLowPowerEnabled(boolean lowPowerEnabled) { - this.lowPowerEnabled = lowPowerEnabled; - } - - void setScanFilters(@Nullable List filters) { - stopScan(); - this.filters = filters; - startScan(); - } - - void clientAdded() { - commandState(ScanAction.CLIENT_START_SCAN); - } - - void clientRemoved() { - commandState(ScanAction.CLIENT_REMOVED); - } - - void stopScan() { - commandState(ScanAction.ADMIN_STOP_SCAN); - } - - void startScan() { - commandState(ScanAction.ADMIN_START_SCAN); - } - - void powerOn() { - commandState(ScanAction.BLE_POWER_ON); - } - - void powerOff() { - commandState(ScanAction.BLE_POWER_OFF); - } - - private void commandState(ScanAction action) { - BleLogger.d(TAG, "commandState state:" + state.toString() + " action: " + action.toString()); - switch (state) { - case IDLE: { - scannerIdleState(action); - break; - } - case STOPPED: { - scannerAdminState(action); - break; - } - case SCANNING: { - scannerScanningState(action); - break; - } - } - } - - private void changeState(ScannerState newState) { - commandState(ScanAction.EXIT); - this.state = newState; - commandState(ScanAction.ENTRY); - } - - private void scannerIdleState(ScanAction action) { - switch (action) { - case ENTRY: { - if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) { - if (scanCallbackInterface.isScanningNeeded()) { - changeState(ScannerState.SCANNING); - } - } - break; - } - case EXIT: - case ADMIN_START_SCAN: - case BLE_POWER_OFF: { - break; - } - case CLIENT_START_SCAN: { - if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) { - if (scanCallbackInterface.isScanningNeeded()) { - changeState(ScannerState.SCANNING); - } - } else { - BleLogger.d(TAG, "Skipped scan start, because of ble power off"); - } - break; - } - case ADMIN_STOP_SCAN: { - changeState(ScannerState.STOPPED); - break; - } - case BLE_POWER_ON: { - if (scanCallbackInterface.isScanningNeeded()) { - // if there is atleast one client waiting - changeState(ScannerState.SCANNING); - } - break; - } - } - } - - private void scannerAdminState(ScanAction action) { - // forced stopped state - switch (action) { - case ENTRY: { - adminStops = 1; - break; - } - case EXIT: { - adminStops = 0; - break; - } - case ADMIN_START_SCAN: { - // go through idle state back to scanning, if needed - --adminStops; - if (adminStops <= 0) { - changeState(ScannerState.IDLE); - } else { - BleLogger.d(TAG, "Waiting admins to call start c: " + adminStops); - } - break; - } - case ADMIN_STOP_SCAN: { - ++adminStops; - break; - } - case BLE_POWER_OFF: { - changeState(ScannerState.IDLE); - break; - } - case CLIENT_REMOVED: - case CLIENT_START_SCAN: - case BLE_POWER_ON: { - // do nothing - break; - } - } - } - - private void scannerScanningState(ScanAction action) { - switch (action) { - case ENTRY: { - // start scanning - startScanning(); - break; - } - case EXIT: { - // stop scanning - stopScanning(); - if (opportunisticScanTimer != null) { - opportunisticScanTimer.dispose(); - opportunisticScanTimer = null; - } - if (timer != null) { - timer.dispose(); - timer = null; - } - break; - } - case CLIENT_START_SCAN: { - // do nothing - break; - } - case CLIENT_REMOVED: { - if (!scanCallbackInterface.isScanningNeeded()) { - // scanning is not needed anymore - changeState(ScannerState.IDLE); - } - break; - } - case ADMIN_STOP_SCAN: { - changeState(ScannerState.STOPPED); - break; - } - case BLE_POWER_OFF: { - changeState(ScannerState.IDLE); - break; - } - case ADMIN_START_SCAN: - // skip - break; - case BLE_POWER_ON: { - // should not happen - BleLogger.e(TAG, "INCORRECT event received in scanning state: " + action); - break; - } - } - } - - @Override - public void onScanResult(int callbackType, ScanResult result) { - scanCallbackInterface.deviceDiscovered(result.getDevice(), result.getRssi(), result.getScanRecord() != null ? result.getScanRecord().getBytes() : new byte[]{}, fetchAdvType(result)); - } - - @Override - public void onBatchScanResults(List results) { - for (ScanResult result : results) { - scanCallbackInterface.deviceDiscovered(result.getDevice(), result.getRssi(), result.getScanRecord() != null ? result.getScanRecord().getBytes() : new byte[]{}, fetchAdvType(result)); - } - } - - @Override - public void onScanFailed(int errorCode) { - BleLogger.e(TAG, "START scan error: " + errorCode); - scanCallbackInterface.scanStartError(errorCode); - } - - @SuppressLint("CheckResult") - private void startScanning() { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (scanPool.size() != 0) { - long elapsed = System.currentTimeMillis() - scanPool.get(0); - if (scanPool.size() > 3 && elapsed < SCAN_WINDOW_LIMIT) { - long sift = (SCAN_WINDOW_LIMIT - elapsed) + 200; - BleLogger.d(TAG, "Prevent scanning too frequently delay: " + sift + "ms" + " elapsed: " + elapsed + "ms"); - if (delaySubscription != null) { - delaySubscription.dispose(); - delaySubscription = null; - } - delaySubscription = Observable.timer(sift, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.io()).observeOn(delayScheduler).subscribe( - aLong -> { - // do nothing - }, - throwable -> BleLogger.e(TAG, "timer failed: " + throwable.getLocalizedMessage()), - () -> { - BleLogger.d(TAG, "delayed scan starting"); - if (scanPool.size() != 0) scanPool.remove(0); - startLScan(); - }); - return; - } - } - BleLogger.d(TAG, "timestamps left: " + scanPool.size()); - } - startLScan(); - } - - private BleUtils.EVENT_TYPE fetchAdvType(ScanResult result) { - BleUtils.EVENT_TYPE type = BleUtils.EVENT_TYPE.ADV_IND; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !result.isConnectable()) { - type = BleUtils.EVENT_TYPE.ADV_NONCONN_IND; - } - return type; - } - - @SuppressLint("NewApi") - private void startLScan() { - BleLogger.d(TAG, "Scan started -->"); - final ScanSettings scanSettings; - if (!lowPowerEnabled) { - scanSettings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(); - } else { - scanSettings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER).build(); - } - try { - callStartScanL(scanSettings); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && opportunistic) { - opportunisticScanTimer = Observable.interval(30, TimeUnit.MINUTES).subscribeOn(Schedulers.io()).observeOn(delayScheduler).subscribe( - aLong -> { - BleLogger.d(TAG, "RESTARTING scan to avoid opportunistic"); - stopScanning(); - callStartScanL(scanSettings); - }, - throwable -> BleLogger.e(TAG, "TIMER failed: " + throwable.getLocalizedMessage()), - () -> { - // non produced - } - ); - } - BleLogger.d(TAG, "Scan started <--"); - } catch (NullPointerException ex) { - BleLogger.e(TAG, "startScan did throw null pointer exception"); - changeState(ScannerState.IDLE); - } - } - - @SuppressLint("NewApi") - private void callStartScanL(ScanSettings scanSettings) { - try { - bluetoothAdapter.getBluetoothLeScanner().startScan(filters, scanSettings, this); - } catch (Exception e) { - BleLogger.e(TAG, "Failed to start scan e: " + e.getLocalizedMessage()); - changeState(ScannerState.IDLE); - return; - } - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - scanPool.removeIf(aLong -> (System.currentTimeMillis() - aLong) >= SCAN_WINDOW_LIMIT); - scanPool.add(System.currentTimeMillis()); - } - } - - @SuppressLint("CheckResult") - private void stopScanning() { - BleLogger.d(TAG, "Stop scanning"); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (delaySubscription != null) { - delaySubscription.dispose(); - delaySubscription = null; - } - } - try { - bluetoothAdapter.getBluetoothLeScanner().stopScan(this); - } catch (Exception ex) { - BleLogger.e(TAG, "stopScan did throw exception: " + ex.getLocalizedMessage()); - } - } -} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDScanCallback.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDScanCallback.kt new file mode 100755 index 00000000..57f682bb --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDScanCallback.kt @@ -0,0 +1,329 @@ +package com.polar.androidcommunications.enpoints.ble.bluedroid.host + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanFilter +import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings +import android.content.Context +import android.os.Build +import com.polar.androidcommunications.api.ble.BleLogger +import com.polar.androidcommunications.common.ble.BleUtils.EVENT_TYPE +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Scheduler +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.schedulers.Schedulers +import java.util.* +import java.util.concurrent.TimeUnit + +internal class BDScanCallback( + context: Context, + private val bluetoothAdapter: BluetoothAdapter, + private val scanCallbackInterface: BDScanCallbackInterface +) { + + private enum class ScanAction { + ENTRY, + EXIT, + CLIENT_START_SCAN, + CLIENT_REMOVED, + ADMIN_START_SCAN, + ADMIN_STOP_SCAN, + BLE_POWER_OFF, + BLE_POWER_ON + } + + private enum class ScannerState { + IDLE, + STOPPED, + SCANNING + } + + private val scanPool: MutableList = ArrayList() + private val delayScheduler: Scheduler = AndroidSchedulers.from(context.mainLooper) + private var state = ScannerState.IDLE + var lowPowerEnabled = false + var opportunistic = true + private var adminStops = 0 + private var filters: List? = null + private var delaySubscription: Disposable? = null + private var opportunisticScanTimer: Disposable? = null + + internal interface BDScanCallbackInterface { + fun deviceDiscovered(device: BluetoothDevice?, rssi: Int, scanRecord: ByteArray?, type: EVENT_TYPE?) + fun scanStartError(error: Int) + fun isScanningNeeded(): Boolean + } + + fun setScanFilters(filters: List?) { + stopScan() + this.filters = filters + startScan() + } + + fun clientAdded() { + commandState(ScanAction.CLIENT_START_SCAN) + } + + fun clientRemoved() { + commandState(ScanAction.CLIENT_REMOVED) + } + + fun stopScan() { + commandState(ScanAction.ADMIN_STOP_SCAN) + } + + fun startScan() { + commandState(ScanAction.ADMIN_START_SCAN) + } + + fun powerOn() { + commandState(ScanAction.BLE_POWER_ON) + } + + fun powerOff() { + commandState(ScanAction.BLE_POWER_OFF) + } + + private val leScanCallback: ScanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + super.onScanResult(callbackType, result) + val bytes = result.scanRecord?.bytes ?: byteArrayOf() + scanCallbackInterface.deviceDiscovered(result.device, result.rssi, bytes, fetchAdvType(result)) + } + + override fun onBatchScanResults(results: List) { + for (result in results) { + val bytes = result.scanRecord?.bytes ?: byteArrayOf() + scanCallbackInterface.deviceDiscovered(result.device, result.rssi, bytes, fetchAdvType(result)) + } + } + + override fun onScanFailed(errorCode: Int) { + BleLogger.e(TAG, "START scan error: $errorCode") + scanCallbackInterface.scanStartError(errorCode) + } + } + + private fun commandState(action: ScanAction) { + BleLogger.d(TAG, "commandState state:$state action: $action") + when (state) { + ScannerState.IDLE -> { + scannerIdleState(action) + } + ScannerState.STOPPED -> { + scannerAdminState(action) + } + ScannerState.SCANNING -> { + scannerScanningState(action) + } + } + } + + private fun changeState(newState: ScannerState) { + commandState(ScanAction.EXIT) + state = newState + commandState(ScanAction.ENTRY) + } + + private fun scannerIdleState(action: ScanAction) { + when (action) { + ScanAction.ENTRY -> { + if (bluetoothAdapter.isEnabled && scanCallbackInterface.isScanningNeeded()) { + changeState(ScannerState.SCANNING) + } + } + ScanAction.EXIT, + ScanAction.ADMIN_START_SCAN, + ScanAction.CLIENT_REMOVED, + ScanAction.BLE_POWER_OFF -> { + /* no-op */ + } + ScanAction.CLIENT_START_SCAN -> { + if (bluetoothAdapter.isEnabled) { + if (scanCallbackInterface.isScanningNeeded()) { + changeState(ScannerState.SCANNING) + } + } else { + BleLogger.d(TAG, "Skipped scan start, because of ble power off") + } + } + ScanAction.ADMIN_STOP_SCAN -> { + changeState(ScannerState.STOPPED) + } + ScanAction.BLE_POWER_ON -> { + if (scanCallbackInterface.isScanningNeeded()) { + // if there is at least one client waiting + changeState(ScannerState.SCANNING) + } + } + } + } + + private fun scannerAdminState(action: ScanAction) { + // forced stopped state + when (action) { + ScanAction.ENTRY -> { + adminStops = 1 + } + ScanAction.EXIT -> { + adminStops = 0 + } + ScanAction.ADMIN_START_SCAN -> { + // go through idle state back to scanning, if needed + --adminStops + if (adminStops <= 0) { + changeState(ScannerState.IDLE) + } else { + BleLogger.d(TAG, "Waiting admins to call start c: $adminStops") + } + } + ScanAction.ADMIN_STOP_SCAN -> { + ++adminStops + } + ScanAction.BLE_POWER_OFF -> { + changeState(ScannerState.IDLE) + } + ScanAction.CLIENT_REMOVED, + ScanAction.CLIENT_START_SCAN, + ScanAction.BLE_POWER_ON -> { + /* no-op */ + } + } + } + + private fun scannerScanningState(action: ScanAction) { + when (action) { + ScanAction.ENTRY -> { + // start scanning + startScanning() + } + ScanAction.EXIT -> { + // stop scanning + stopScanning() + opportunisticScanTimer?.dispose() + opportunisticScanTimer = null + } + ScanAction.CLIENT_REMOVED -> { + if (!scanCallbackInterface.isScanningNeeded()) { + // scanning is not needed anymore + changeState(ScannerState.IDLE) + } + } + ScanAction.ADMIN_STOP_SCAN -> { + changeState(ScannerState.STOPPED) + } + ScanAction.BLE_POWER_OFF -> { + changeState(ScannerState.IDLE) + } + ScanAction.BLE_POWER_ON -> { + // should not happen + BleLogger.e(TAG, "INCORRECT event received in scanning state: $action") + } + ScanAction.CLIENT_START_SCAN, + ScanAction.ADMIN_START_SCAN -> { + /* no-op */ + } + } + } + + private fun startScanning() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (scanPool.isNotEmpty()) { + val elapsed = System.currentTimeMillis() - scanPool[0] + if (scanPool.size > 3 && elapsed < SCAN_WINDOW_LIMIT) { + val sift = SCAN_WINDOW_LIMIT - elapsed + 200 + BleLogger.d(TAG, "Prevent scanning too frequently delay: " + sift + "ms" + " elapsed: " + elapsed + "ms") + delaySubscription?.dispose() + delaySubscription = Observable.timer(sift, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(delayScheduler) + .subscribe( + { }, + { throwable: Throwable -> BleLogger.e(TAG, "timer failed: " + throwable.localizedMessage) }, + { + BleLogger.d(TAG, "delayed scan starting") + if (scanPool.isNotEmpty()) scanPool.removeAt(0) + startLScan() + }) + return + } + } + BleLogger.d(TAG, "timestamps left: " + scanPool.size) + } + startLScan() + } + + private fun fetchAdvType(result: ScanResult): EVENT_TYPE { + var type = EVENT_TYPE.ADV_IND + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !result.isConnectable) { + type = EVENT_TYPE.ADV_NONCONN_IND + } + return type + } + + private fun startLScan() { + BleLogger.d(TAG, "Scan started -->") + val scanSettings: ScanSettings = if (!lowPowerEnabled) { + ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build() + } else { + ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER).build() + } + try { + callStartScanL(scanSettings) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && opportunistic) { + opportunisticScanTimer = Observable.interval(30, TimeUnit.MINUTES) + .subscribeOn(Schedulers.io()) + .observeOn(delayScheduler) + .subscribe( + { + BleLogger.d(TAG, "RESTARTING scan to avoid opportunistic") + stopScanning() + callStartScanL(scanSettings) + }, + { throwable: Throwable -> BleLogger.e(TAG, "TIMER failed: " + throwable.localizedMessage) } + ) + } + BleLogger.d(TAG, "Scan started <--") + } catch (ex: NullPointerException) { + BleLogger.e(TAG, "startScan did throw null pointer exception") + changeState(ScannerState.IDLE) + } + } + + private fun callStartScanL(scanSettings: ScanSettings) { + try { + bluetoothAdapter.bluetoothLeScanner.startScan(filters, scanSettings, leScanCallback) + } catch (e: Exception) { + BleLogger.e(TAG, "Failed to start scan e: " + e.localizedMessage) + changeState(ScannerState.IDLE) + return + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + scanPool.removeIf { aLong: Long -> System.currentTimeMillis() - aLong >= SCAN_WINDOW_LIMIT } + scanPool.add(System.currentTimeMillis()) + } + } + + private fun stopScanning() { + BleLogger.d(TAG, "Stop scanning") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + delaySubscription?.dispose() + delaySubscription = null + } + try { + bluetoothAdapter.bluetoothLeScanner.stopScan(leScanCallback) + } catch (ex: Exception) { + BleLogger.e(TAG, "stopScan did throw exception: " + ex.localizedMessage) + } + } + + companion object { + private val TAG = BDScanCallback::class.java.simpleName + + // scan window limit, for android's "is scanning too frequently" + private const val SCAN_WINDOW_LIMIT = 30000 + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/connection/ConnectionHandler.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/connection/ConnectionHandler.java index 7c96e1a9..c07b8ffc 100755 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/connection/ConnectionHandler.java +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/connection/ConnectionHandler.java @@ -33,7 +33,7 @@ private enum ConnectionHandlerAction { DEVICE_DISCONNECTED } - private final static String TAG = ConnectionHandler.class.getSimpleName(); + private static final String TAG = ConnectionHandler.class.getSimpleName(); private ConnectionHandlerState state; private final ScannerInterface scannerInterface; private final ConnectionInterface connectionInterface; @@ -68,6 +68,8 @@ public void connectDevice(BDDeviceSessionImpl bleDeviceSession, boolean bluetoot updateSessionState(bleDeviceSession, BleDeviceSession.DeviceSessionState.SESSION_OPEN_PARK); break; } + default: + break; } } } @@ -116,7 +118,7 @@ private void updateSessionState(final BDDeviceSessionImpl bleDeviceSession, BleD } private boolean containsRequiredUuids(final BDDeviceSessionImpl session) { - if (session.getConnectionUuids().size() != 0) { + if (!session.getConnectionUuids().isEmpty()) { HashMap content = session.getAdvertisementContent().getAdvertisementData(); if (content.containsKey(BleUtils.AD_TYPE.GAP_ADTYPE_16BIT_MORE) || content.containsKey(BleUtils.AD_TYPE.GAP_ADTYPE_16BIT_COMPLETE)) { @@ -162,6 +164,8 @@ private void free(final BDDeviceSessionImpl session, ConnectionHandlerAction act updateSessionState(session, BleDeviceSession.DeviceSessionState.SESSION_OPEN); break; } + case SESSION_OPENING: + break; } break; } @@ -260,6 +264,10 @@ private void handleDisconnectDevice(BDDeviceSessionImpl session) { connectionInterface.disconnectDevice(session); break; } + case SESSION_CLOSED: + case SESSION_OPENING: + case SESSION_CLOSING: + break; } } @@ -277,6 +285,10 @@ private void handleDeviceDisconnected(BDDeviceSessionImpl session) { updateSessionState(session, BleDeviceSession.DeviceSessionState.SESSION_CLOSED); break; } + case SESSION_CLOSED: + case SESSION_OPENING: + case SESSION_OPEN_PARK: + break; } } } diff --git a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApi.java b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApi.java index 91bdf0d0..98c81912 100644 --- a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApi.java +++ b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApi.java @@ -131,7 +131,11 @@ protected PolarBleApi(final int features) { public abstract void setMtu(@IntRange(from = 70, to = 512) int mtu); /** - * Must be called when application is destroyed. + * Releases the SDK resources. When the SDK is used on scope of + * the android component (e.g. Activity or Service) then the shutDown may be called + * on component destroy function. After shutDown the new instance of the SDK is needed: + * + * @see PolarBleApiDefaultImpl#defaultImplementation */ public abstract void shutDown(); diff --git a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt index 1b1d73af..c8fe39d9 100644 --- a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt +++ b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt @@ -25,6 +25,6 @@ object PolarBleApiDefaultImpl { */ @JvmStatic fun versionInfo(): String { - return "3.2.2" + return "3.2.3" } } \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.java b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.java index ffb117bd..ff22872f 100644 --- a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.java +++ b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.java @@ -20,6 +20,7 @@ import com.polar.androidcommunications.api.ble.BleLogger; import com.polar.androidcommunications.api.ble.exceptions.BleControlPointCommandError; import com.polar.androidcommunications.api.ble.exceptions.BleDisconnected; +import com.polar.androidcommunications.api.ble.exceptions.BleNotAvailableInDevice; import com.polar.androidcommunications.api.ble.model.BleDeviceSession; import com.polar.androidcommunications.api.ble.model.advertisement.BlePolarHrAdvertisement; import com.polar.androidcommunications.api.ble.model.gatt.BleGattBase; @@ -103,56 +104,17 @@ public class BDBleApiImpl extends PolarBleApi implements BleDeviceListener.BlePowerStateChangedCallback { private static volatile BDBleApiImpl instance; - @NonNull - private final BleDeviceListener listener; private final Map connectSubscriptions = new HashMap<>(); - private final Scheduler scheduler; + private final Map deviceDataMonitorDisposable = new HashMap<>(); + private final Map stopPmdStreamingDisposable = new HashMap<>(); private final BleDeviceListener.BleSearchPreFilter filter = content -> !content.getPolarDeviceId().isEmpty() && !content.getPolarDeviceType().equals("mobile"); - private final Disposable deviceSessionStateMonitorDisposable; + private BleDeviceListener listener; + private Scheduler scheduler; + private Disposable devicesStateMonitorDisposable; private PolarBleApiCallbackProvider callback; private PolarBleApiLogger logger; - private final Consumer> deviceStateMonitorObserver = deviceSessionStatePair -> - { - BleDeviceSession session = deviceSessionStatePair.first; - BleDeviceSession.DeviceSessionState sessionState = deviceSessionStatePair.second; - - //Guard - if (session == null || sessionState == null) { - return; - } - - PolarDeviceInfo info = new PolarDeviceInfo( - session.getPolarDeviceId().isEmpty() ? session.getAddress() : session.getPolarDeviceId(), - session.getAddress(), - session.getRssi(), - session.getName(), - true); - switch (Objects.requireNonNull(sessionState)) { - case SESSION_OPEN: - if (callback != null) { - callback.deviceConnected(info); - } - setupDevice(session); - break; - case SESSION_CLOSED: - if (callback != null - && (session.getPreviousState() == SESSION_OPEN || session.getPreviousState() == BleDeviceSession.DeviceSessionState.SESSION_CLOSING)) { - callback.deviceDisconnected(info); - } - break; - case SESSION_OPENING: - if (callback != null) { - callback.deviceConnecting(info); - } - break; - default: - //Do nothing - break; - } - }; - - private BDBleApiImpl(@NonNull Context context, int features) { + private BDBleApiImpl(@NonNull Context context, int features) throws BleNotAvailableInDevice { super(features); Set> clients = new HashSet<>(); if ((this.features & PolarBleApi.FEATURE_HR) != 0) { @@ -172,7 +134,6 @@ private BDBleApiImpl(@NonNull Context context, int features) { } listener = new BDDeviceListenerImpl(context, clients); listener.setScanPreFilter(filter); - deviceSessionStateMonitorDisposable = listener.monitorDeviceSessionState().subscribe(deviceStateMonitorObserver); listener.setBlePowerStateCallback(this); scheduler = AndroidSchedulers.from(context.getMainLooper()); BleLogger.setLoggerInterface(new BleLogger.BleLoggerInterface() { @@ -196,7 +157,7 @@ public void i(String tag, String msg) { }); } - public static BDBleApiImpl getInstance(@NonNull Context context, int features) throws PolarBleSdkInstanceException { + public static BDBleApiImpl getInstance(@NonNull Context context, int features) throws PolarBleSdkInstanceException, BleNotAvailableInDevice { BDBleApiImpl result = instance; if (result != null) { if (result.features == features) { @@ -213,6 +174,10 @@ public static BDBleApiImpl getInstance(@NonNull Context context, int features) t } } + private static void clearInstance() { + instance = null; + } + private void enableAndroidScanFilter() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { List scanFilter = new ArrayList<>(); @@ -231,8 +196,35 @@ public void setMtu(int mtu) { @Override public void shutDown() { - deviceSessionStateMonitorDisposable.dispose(); + for (Map.Entry deviceDisposable : deviceDataMonitorDisposable.entrySet()) { + if (deviceDisposable.getValue().isDisposed()) { + deviceDisposable.getValue().dispose(); + } + } + if (devicesStateMonitorDisposable != null && !devicesStateMonitorDisposable.isDisposed()) { + devicesStateMonitorDisposable.dispose(); + } + devicesStateMonitorDisposable = null; + + for (Map.Entry connection : connectSubscriptions.entrySet()) { + if (!connection.getValue().isDisposed()) { + connection.getValue().dispose(); + } + } + + for (Map.Entry pmdStream : stopPmdStreamingDisposable.entrySet()) { + if (!pmdStream.getValue().isDisposed()) { + pmdStream.getValue().dispose(); + } + } + listener.shutDown(); + logger = null; + callback = null; + listener = null; + scheduler = null; + + clearInstance(); } @Override @@ -418,7 +410,7 @@ public Completable autoConnectToDevice(final int rssiLimit, @Nullable final Stri .doOnSuccess(set -> { List list = new ArrayList<>(set); Collections.sort(list, (o1, o2) -> o1.getRssi() > o2.getRssi() ? -1 : 1); - listener.openSessionDirect(list.get(0)); + openConnection(list.get(0)); log("auto connect search complete"); }) .toObservable() @@ -440,7 +432,7 @@ public void connectToDevice(@NonNull final String identifier) throws PolarInvali connectSubscriptions.remove(identifier); } if (session != null) { - listener.openSessionDirect(session); + openConnection(session); } else { connectSubscriptions.put(identifier, listener.search(false) @@ -448,9 +440,9 @@ public void connectToDevice(@NonNull final String identifier) throws PolarInvali .take(1) .observeOn(scheduler) .subscribe( - listener::openSessionDirect, - error -> logError(error.getMessage()), - () -> log("connect search complete") + this::openConnection, + error -> logError("connect search error with device: " + identifier + " error: " + error.getMessage()), + () -> log("connect search completed for " + identifier) )); } } @@ -711,6 +703,13 @@ private Flowable startStreaming(String identifier, } } + private void openConnection(@NonNull BleDeviceSession session) { + if (devicesStateMonitorDisposable == null || devicesStateMonitorDisposable.isDisposed()) { + devicesStateMonitorDisposable = listener.monitorDeviceSessionState().subscribe(deviceStateMonitorObserver); + } + listener.openSessionDirect(session); + } + @NonNull @Override public Flowable startEcgStreaming(@NonNull String identifier, @@ -919,18 +918,59 @@ protected BleDeviceSession sessionPsFtpClientReady(final @NonNull String identif protected void stopPmdStreaming(@NonNull BleDeviceSession session, @NonNull BlePMDClient client, @NonNull PmdMeasurementType type) { if (session.getSessionState() == SESSION_OPEN) { // stop streaming - client.stopMeasurement(type).subscribe( + Disposable disposable = client.stopMeasurement(type).subscribe( () -> { }, throwable -> logError("failed to stop pmd stream: " + throwable.getLocalizedMessage()) ); + stopPmdStreamingDisposable.put(session.getAddress(), disposable); } } - @SuppressLint("CheckResult") - protected void setupDevice(final @NonNull BleDeviceSession session) { + private final Consumer> deviceStateMonitorObserver = deviceSessionStatePair -> + { + BleDeviceSession session = deviceSessionStatePair.first; + BleDeviceSession.DeviceSessionState sessionState = deviceSessionStatePair.second; + + //Guard + if (session == null || sessionState == null) { + return; + } + + PolarDeviceInfo info = new PolarDeviceInfo( + session.getPolarDeviceId().isEmpty() ? session.getAddress() : session.getPolarDeviceId(), + session.getAddress(), + session.getRssi(), + session.getName(), + true); + switch (Objects.requireNonNull(sessionState)) { + case SESSION_OPEN: + if (callback != null) { + callback.deviceConnected(info); + } + setupDevice(session); + break; + case SESSION_CLOSED: + if (callback != null && (session.getPreviousState() == SESSION_OPEN || session.getPreviousState() == BleDeviceSession.DeviceSessionState.SESSION_CLOSING)) { + callback.deviceDisconnected(info); + } + tearDownDevice(session); + break; + case SESSION_OPENING: + if (callback != null) { + callback.deviceConnecting(info); + } + break; + default: + //Do nothing + break; + } + }; + + private void setupDevice(final @NonNull BleDeviceSession session) { final String deviceId = session.getPolarDeviceId().length() != 0 ? session.getPolarDeviceId() : session.getAddress(); - session.monitorServicesDiscovered(true) + + Disposable disposable = session.monitorServicesDiscovered(true) .observeOn(scheduler) .toFlowable() .flatMapIterable((Function, Iterable>) uuids -> uuids) @@ -1031,8 +1071,19 @@ protected void setupDevice(final @NonNull BleDeviceSession session) { .subscribe( o -> { }, - throwable -> logError(throwable.getMessage()), + throwable -> logError("Error while monitoring session services: " + throwable.getMessage()), () -> log("complete")); + deviceDataMonitorDisposable.put(session.getAddress(), disposable); + } + + private void tearDownDevice(final @NonNull BleDeviceSession session) { + final String address = session.getAddress(); + if (deviceDataMonitorDisposable.containsKey(address)) { + if (!deviceDataMonitorDisposable.get(address).isDisposed()) { + Objects.requireNonNull(deviceDataMonitorDisposable.get(address)).dispose(); + } + deviceDataMonitorDisposable.remove(address); + } } @NonNull @@ -1091,13 +1142,9 @@ protected void log(@NonNull String message) { } } - protected void logError(@Nullable String message) { + protected void logError(@NonNull String message) { if (logger != null) { - if (message != null) { - logger.message("Error: " + message); - } else { - logger.message("Error without known reason"); - } + logger.message("Error: " + message); } } } \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/GnssLocationDataTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GnssLocationDataTest.kt similarity index 99% rename from sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/GnssLocationDataTest.kt rename to sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GnssLocationDataTest.kt index e1581241..74c3afb1 100644 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/GnssLocationDataTest.kt +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GnssLocationDataTest.kt @@ -1,6 +1,6 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client.pmd +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model -import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.GnssLocationData +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient import org.junit.Assert import org.junit.Test import java.lang.Double.longBitsToDouble diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpgDataTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpgDataTest.kt similarity index 99% rename from sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpgDataTest.kt rename to sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpgDataTest.kt index 564a65e6..9d9102b9 100644 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpgDataTest.kt +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpgDataTest.kt @@ -1,6 +1,6 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client.pmd +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model -import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.PpgData +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient import org.junit.Assert import org.junit.Test diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpiDataTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpiDataTest.kt similarity index 99% rename from sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpiDataTest.kt rename to sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpiDataTest.kt index 80b63257..a2a0686c 100644 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpiDataTest.kt +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpiDataTest.kt @@ -1,6 +1,6 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client.pmd +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model -import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.PpiData +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient import org.junit.Assert.assertEquals import org.junit.Test diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PressureDataTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PressureDataTest.kt similarity index 75% rename from sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PressureDataTest.kt rename to sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PressureDataTest.kt index 8aa2d693..e76f9835 100644 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PressureDataTest.kt +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PressureDataTest.kt @@ -1,6 +1,6 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client.pmd +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model -import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.PressureData +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient import org.junit.Assert import org.junit.Test import java.lang.Float.intBitsToFloat @@ -8,7 +8,7 @@ import java.lang.Float.intBitsToFloat class PressureDataTest { @Test - fun `process pressure data type 0`() { + fun `process compressed pressure data type 0`() { // Arrange // HEX: C2 87 80 44 0A 01 1F BF // index type data @@ -72,4 +72,27 @@ class PressureDataTest { Assert.assertEquals(factor * sample0, pressureData.pressureSamples[0].pressure) Assert.assertEquals(factor * sample1, pressureData.pressureSamples[1].pressure) } + + @Test + fun `process raw pressure data type 0`() { + // Arrange + // HEX: AE 27 7B 44 + // index type data + // 0..3 Pressure data AE 27 7B 44 (0x447B27AE) + val timeStamp: Long = 0 + val frameType = BlePMDClient.PmdDataFrameType.TYPE_0 + val isCompressed = false + val expectedSamplesSize = 1 + val sample0 = intBitsToFloat(0x447B27AE) + val measurementFrame = byteArrayOf(0xAE.toByte(), 0x27.toByte(), 0x7B.toByte(), 0x44.toByte()) + val factor = 1.0f + + // Act + val pressureData = PressureData.parseDataFromDataFrame(isCompressed = isCompressed, frameType = frameType, frame = measurementFrame, factor = factor, timeStamp = timeStamp) + + // Assert + Assert.assertEquals(timeStamp, pressureData.timeStamp) + Assert.assertEquals(expectedSamplesSize, pressureData.pressureSamples.size) + Assert.assertEquals(sample0, pressureData.pressureSamples[0].pressure) + } } \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/TemperatureDataTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/TemperatureDataTest.kt new file mode 100644 index 00000000..7600740d --- /dev/null +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/TemperatureDataTest.kt @@ -0,0 +1,67 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import org.junit.Assert +import org.junit.Test +import java.lang.Float + +class TemperatureDataTest { + @Test + fun `process compressed temperature data type 0`() { + // Arrange + // HEX: EC 51 DC 41 03 02 00 + // index type data + // 0..3 Sample 1 (ref. sample) EC 51 DC 41 (0x41DC51EC) + // 4 Delta size 03 (3 bits) + // 5 Sample amount 01 (2 samples) + // 6.. Delta data 00 + // Delta sample 1 000b (0) + // Delta sample 2 000b (0) + + val expectedSamplesSize = 1 + 2 // reference sample + delta samples + val expectedTimeStamp = 578437695752307201L + val sample0 = Float.intBitsToFloat(0x41DC51EC) + val sample1 = Float.intBitsToFloat(0x41DC51EC + 0x0) + val sample2 = Float.intBitsToFloat(0x41DC51EC + 0x0) + + val measurementFrame = byteArrayOf( + 0xEC.toByte(), 0x51.toByte(), 0xDC.toByte(), 0x41.toByte(), 0x03.toByte(), 0x02.toByte(), 0x00.toByte() + ) + val frameType = BlePMDClient.PmdDataFrameType.TYPE_0 + val isCompressed = true + val factor = 1.0f + + // Act + val temperatureData = TemperatureData.parseDataFromDataFrame(isCompressed = isCompressed, frameType = frameType, frame = measurementFrame, factor = factor, timeStamp = expectedTimeStamp) + + // Assert + Assert.assertEquals(expectedTimeStamp, temperatureData.timeStamp) + Assert.assertEquals(expectedSamplesSize, temperatureData.temperatureSamples.size) + Assert.assertEquals(sample0, temperatureData.temperatureSamples[0].temperature) + Assert.assertEquals(sample1, temperatureData.temperatureSamples[1].temperature) + Assert.assertEquals(sample2, temperatureData.temperatureSamples[2].temperature) + } + + @Test + fun `process raw temperature data type 0`() { + // Arrange + // HEX: F6 28 C0 41 + // index type data + // 0..3 Temperature data F6 28 C0 41 (0x41C028F6) + val timeStamp: Long = 0 + val frameType = BlePMDClient.PmdDataFrameType.TYPE_0 + val isCompressed = false + val expectedSamplesSize = 1 + val sample0 = Float.intBitsToFloat(0x41C028F6) + val measurementFrame = byteArrayOf(0xF6.toByte(), 0x28.toByte(), 0xC0.toByte(), 0x41.toByte()) + val factor = 1.0f + + // Act + val temperatureData = TemperatureData.parseDataFromDataFrame(isCompressed = isCompressed, frameType = frameType, frame = measurementFrame, factor = factor, timeStamp = timeStamp) + + // Assert + Assert.assertEquals(timeStamp, temperatureData.timeStamp) + Assert.assertEquals(expectedSamplesSize, temperatureData.temperatureSamples.size) + Assert.assertEquals(sample0, temperatureData.temperatureSamples[0].temperature) + } +} \ No newline at end of file