diff --git a/profile_bps/build.gradle b/profile_bps/build.gradle index c0a5b435..ce45a593 100644 --- a/profile_bps/build.gradle +++ b/profile_bps/build.gradle @@ -7,6 +7,7 @@ dependencies { implementation project(":lib_utils") implementation libs.nordic.ble.common + implementation libs.nordic.ble.ktx implementation libs.nordic.navigation implementation libs.nordic.log diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSManager.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSManager.kt index 5c3922a4..bb455fe1 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSManager.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSManager.kt @@ -21,94 +21,48 @@ */ package no.nordicsemi.android.bps.repository -import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.content.Context import android.util.Log import dagger.hilt.android.qualifiers.ApplicationContext -import no.nordicsemi.android.ble.common.callback.bps.BloodPressureMeasurementDataCallback -import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureDataCallback -import no.nordicsemi.android.ble.common.profile.bp.BloodPressureTypes -import no.nordicsemi.android.ble.data.Data +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.ble.common.callback.bps.BloodPressureMeasurementResponse +import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureResponse +import no.nordicsemi.android.ble.ktx.asValidResponseFlow +import no.nordicsemi.android.ble.ktx.suspend import no.nordicsemi.android.bps.data.BPSRepository +import no.nordicsemi.android.log.Logger import no.nordicsemi.android.service.BatteryManager +import no.nordicsemi.android.service.CloseableCoroutineScope import java.util.* import javax.inject.Inject -import javax.inject.Singleton -/** Blood Pressure service UUID. */ val BPS_SERVICE_UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb") -/** Blood Pressure Measurement characteristic UUID. */ private val BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb") -/** Intermediate Cuff Pressure characteristic UUID. */ private val ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-00805f9b34fb") -@Singleton +@ViewModelScoped internal class BPSManager @Inject constructor( @ApplicationContext context: Context, private val dataHolder: BPSRepository ) : BatteryManager(context) { + private val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private var bpmCharacteristic: BluetoothGattCharacteristic? = null private var icpCharacteristic: BluetoothGattCharacteristic? = null - private val intermediateCuffPressureCallback = object : IntermediateCuffPressureDataCallback() { - - override fun onIntermediateCuffPressureReceived( - device: BluetoothDevice, - cuffPressure: Float, - unit: Int, - pulseRate: Float?, - userID: Int?, - status: BloodPressureTypes.BPMStatus?, - calendar: Calendar? - ) { - dataHolder.setIntermediateCuffPressure( - cuffPressure, - unit, - pulseRate, - userID, - status, - calendar - ) - } - - override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) { - log(Log.WARN, "Invalid ICP data received: $data") - } - } - - private val bloodPressureMeasurementDataCallback = object : BloodPressureMeasurementDataCallback() { - - override fun onBloodPressureMeasurementReceived( - device: BluetoothDevice, - systolic: Float, - diastolic: Float, - meanArterialPressure: Float, - unit: Int, - pulseRate: Float?, - userID: Int?, - status: BloodPressureTypes.BPMStatus?, - calendar: Calendar? - ) { - dataHolder.setBloodPressureMeasurement( - systolic, - diastolic, - meanArterialPressure, - unit, - pulseRate, - userID, - status, - calendar - ) - } - - override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) { - log(Log.WARN, "Invalid BPM data received: $data") - } + private val exceptionHandler = CoroutineExceptionHandler { _, t-> + Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) } override fun onBatteryLevelChanged(batteryLevel: Int) { @@ -119,19 +73,40 @@ internal class BPSManager @Inject constructor( override fun initialize() { super.initialize() - setNotificationCallback(icpCharacteristic) - .with(intermediateCuffPressureCallback) - setIndicationCallback(bpmCharacteristic) - .with(bloodPressureMeasurementDataCallback) - enableNotifications(icpCharacteristic) - .fail { device, status -> - log( - Log.WARN, - "Intermediate Cuff Pressure characteristic not found" + + setNotificationCallback(icpCharacteristic).asValidResponseFlow() + .onEach { + dataHolder.setIntermediateCuffPressure( + it.cuffPressure, + it.unit, + it.pulseRate, + it.userID, + it.status, + it.timestamp ) - } - .enqueue() - enableIndications(bpmCharacteristic).enqueue() + }.launchIn(scope) + + setIndicationCallback(bpmCharacteristic).asValidResponseFlow() + .onEach { + dataHolder.setBloodPressureMeasurement( + it.systolic, + it.diastolic, + it.meanArterialPressure, + it.unit, + it.pulseRate, + it.userID, + it.status, + it.timestamp + ) + }.launchIn(scope) + + scope.launch(exceptionHandler) { + enableNotifications(icpCharacteristic).suspend() + } + + scope.launch(exceptionHandler) { + enableIndications(bpmCharacteristic).suspend() + } } override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { @@ -143,7 +118,7 @@ internal class BPSManager @Inject constructor( return bpmCharacteristic != null } - override fun onServicesInvalidated() { } + override fun onServicesInvalidated() {} override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean { super.isOptionalServiceSupported(gatt) // ignore the result of this @@ -159,4 +134,8 @@ internal class BPSManager @Inject constructor( override fun getGattCallback(): BleManagerGattCallback { return BloodPressureManagerGattCallback() } + + fun release() { + scope.close() + } } diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt index dc5dbfbb..a74ba16c 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt @@ -100,5 +100,6 @@ internal class BPSViewModel @Inject constructor( override fun onCleared() { super.onCleared() repository.clear() + bpsManager.release() } } diff --git a/profile_cgms/build.gradle b/profile_cgms/build.gradle index ee04804e..129cd874 100644 --- a/profile_cgms/build.gradle +++ b/profile_cgms/build.gradle @@ -7,6 +7,7 @@ dependencies { implementation project(":lib_utils") implementation libs.nordic.ble.common + implementation libs.nordic.ble.ktx implementation libs.nordic.log implementation libs.nordic.theme diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMManager.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMManager.kt index b64a7126..6e6dbef2 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMManager.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMManager.kt @@ -21,42 +21,45 @@ */ package no.nordicsemi.android.cgms.repository -import android.annotation.SuppressLint -import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.content.Context import android.util.Log import android.util.SparseArray -import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback -import no.nordicsemi.android.ble.common.callback.cgm.CGMFeatureDataCallback -import no.nordicsemi.android.ble.common.callback.cgm.CGMSpecificOpsControlPointDataCallback -import no.nordicsemi.android.ble.common.callback.cgm.CGMStatusDataCallback -import no.nordicsemi.android.ble.common.callback.cgm.ContinuousGlucoseMeasurementDataCallback +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointResponse +import no.nordicsemi.android.ble.common.callback.cgm.CGMFeatureResponse +import no.nordicsemi.android.ble.common.callback.cgm.CGMSpecificOpsControlPointResponse +import no.nordicsemi.android.ble.common.callback.cgm.CGMStatusResponse +import no.nordicsemi.android.ble.common.callback.cgm.ContinuousGlucoseMeasurementResponse import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData import no.nordicsemi.android.ble.common.data.cgm.CGMSpecificOpsControlPointData import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback import no.nordicsemi.android.ble.common.profile.cgm.CGMSpecificOpsControlPointCallback -import no.nordicsemi.android.ble.common.profile.cgm.CGMTypes -import no.nordicsemi.android.ble.data.Data -import no.nordicsemi.android.cgms.data.* -import no.nordicsemi.android.log.LogContract +import no.nordicsemi.android.ble.ktx.asValidResponseFlow +import no.nordicsemi.android.ble.ktx.suspend +import no.nordicsemi.android.ble.ktx.suspendForValidResponse +import no.nordicsemi.android.cgms.data.CGMRecord +import no.nordicsemi.android.cgms.data.CGMRepository +import no.nordicsemi.android.cgms.data.RequestStatus import no.nordicsemi.android.service.BatteryManager import java.util.* -/** Cycling Speed and Cadence service UUID. */ val CGMS_SERVICE_UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb") private val CGM_STATUS_UUID = UUID.fromString("00002AA9-0000-1000-8000-00805f9b34fb") private val CGM_FEATURE_UUID = UUID.fromString("00002AA8-0000-1000-8000-00805f9b34fb") private val CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb") -private val CGM_OPS_CONTROL_POINT_UUID = - UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb") +private val CGM_OPS_CONTROL_POINT_UUID = UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb") -/** Record Access Control Point characteristic UUID. */ private val RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb") internal class CGMManager( context: Context, + private val scope: CoroutineScope, private val repository: CGMRepository ) : BatteryManager(context) { @@ -67,21 +70,16 @@ internal class CGMManager( private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null private val records: SparseArray = SparseArray() - /** A flag set to true if the remote device supports E2E CRC. */ private var secured = false - /** - * A flag set when records has been requested using RACP. This is to distinguish CGM packets - * received as continuous measurements or requested. - */ private var recordAccessRequestInProgress = false - /** - * The timestamp when the session has started. This is needed to display the user facing - * times of samples. - */ private var sessionStartTime: Long = 0 + private val exceptionHandler = CoroutineExceptionHandler { _, t-> + Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) + } + override fun onBatteryLevelChanged(batteryLevel: Int) { repository.emitNewBatteryLevel(batteryLevel) } @@ -90,247 +88,135 @@ internal class CGMManager( return CGMManagerGattCallback() } - /** - * BluetoothGatt mCallbacks for connection/disconnection, service discovery, - * receiving notification, etc. - */ private inner class CGMManagerGattCallback : BatteryManagerGattCallback() { override fun initialize() { - // Enable Battery service super.initialize() - // Read CGM Feature characteristic, mainly to see if the device supports E2E CRC. - // This is not supported in the experimental CGMS from the SDK. - readCharacteristic(cgmFeatureCharacteristic) - .with(object : CGMFeatureDataCallback() { - override fun onContinuousGlucoseMonitorFeaturesReceived( - device: BluetoothDevice, features: CGMTypes.CGMFeatures, - type: Int, sampleLocation: Int, secured: Boolean - ) { - this@CGMManager.secured = features.e2eCrcSupported - log( - LogContract.Log.Level.APPLICATION, - "E2E CRC feature " + if (this@CGMManager.secured) "supported" else "not supported" - ) - } - }) - .fail { _: BluetoothDevice?, _: Int -> - log( - Log.WARN, - "Could not read CGM Feature characteristic" - ) + scope.launch(exceptionHandler) { + val response = + readCharacteristic(cgmFeatureCharacteristic).suspendForValidResponse() + this@CGMManager.secured = response.features.e2eCrcSupported + } + + scope.launch(exceptionHandler) { + val response = + readCharacteristic(cgmStatusCharacteristic).suspendForValidResponse() + if (response.status?.sessionStopped == false) { + sessionStartTime = System.currentTimeMillis() - response.timeOffset * 60000L } - .enqueue() + } - // Check if the session is already started. This is not supported in the experimental CGMS from the SDK. - readCharacteristic(cgmStatusCharacteristic) - .with(object : CGMStatusDataCallback() { - override fun onContinuousGlucoseMonitorStatusChanged( - device: BluetoothDevice, - status: CGMTypes.CGMStatus, - timeOffset: Int, - secured: Boolean - ) { - if (!status.sessionStopped) { - sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L - log(LogContract.Log.Level.APPLICATION, "Session already started") - } - } - }) - .fail { _: BluetoothDevice?, _: Int -> - log( - Log.WARN, - "Could not read CGM Status characteristic" - ) - } - .enqueue() - - // Set notification and indication mCallbacks - setNotificationCallback(cgmMeasurementCharacteristic) - .with(object : ContinuousGlucoseMeasurementDataCallback() { - override fun onContinuousGlucoseMeasurementReceived( - device: BluetoothDevice, - glucoseConcentration: Float, - cgmTrend: Float?, - cgmQuality: Float?, - status: CGMTypes.CGMStatus?, - timeOffset: Int, - secured: Boolean - ) { - // If the CGM Status characteristic has not been read and the session was already started before, - // estimate the Session Start Time by subtracting timeOffset minutes from the current timestamp. - if (sessionStartTime == 0L && !recordAccessRequestInProgress) { - sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L - } - - // Calculate the sample timestamp based on the Session Start Time - val timestamp = - sessionStartTime + timeOffset * 60000L // Sequence number is in minutes since Start Session - val record = CGMRecord(timeOffset, glucoseConcentration, timestamp) - records.put(record.sequenceNumber, record) - repository.emitNewRecords(records.toList()) + setNotificationCallback(cgmMeasurementCharacteristic).asValidResponseFlow() + .onEach { + if (sessionStartTime == 0L && !recordAccessRequestInProgress) { + sessionStartTime = System.currentTimeMillis() - it.timeOffset * 60000L } - override fun onContinuousGlucoseMeasurementReceivedWithCrcError( - device: BluetoothDevice, - data: Data - ) { - log( - Log.WARN, - "Continuous Glucose Measurement record received with CRC error" - ) - } - }) - setIndicationCallback(cgmSpecificOpsControlPointCharacteristic) - .with(object : CGMSpecificOpsControlPointDataCallback() { - @SuppressLint("SwitchIntDef") - override fun onCGMSpecificOpsOperationCompleted( - device: BluetoothDevice, - @CGMSpecificOpsControlPointCallback.CGMOpCode requestCode: Int, - secured: Boolean - ) { - when (requestCode) { + val timestamp = sessionStartTime + it.timeOffset * 60000L + val record = CGMRecord(it.timeOffset, it.glucoseConcentration, timestamp) + records.put(record.sequenceNumber, record) + repository.emitNewRecords(records.toList()) + }.launchIn(scope) + + setIndicationCallback(cgmSpecificOpsControlPointCharacteristic).asValidResponseFlow() + .onEach { + if (it.isOperationCompleted) { + when (it.requestCode) { CGMSpecificOpsControlPointCallback.CGM_OP_CODE_START_SESSION -> sessionStartTime = System.currentTimeMillis() - CGMSpecificOpsControlPointCallback.CGM_OP_CODE_STOP_SESSION -> sessionStartTime = - 0 + CGMSpecificOpsControlPointCallback.CGM_OP_CODE_STOP_SESSION -> sessionStartTime = 0 } - } - - @SuppressLint("SwitchIntDef") - override fun onCGMSpecificOpsOperationError( - device: BluetoothDevice, - @CGMSpecificOpsControlPointCallback.CGMOpCode requestCode: Int, - @CGMSpecificOpsControlPointCallback.CGMErrorCode errorCode: Int, - secured: Boolean - ) { - when (requestCode) { - CGMSpecificOpsControlPointCallback.CGM_OP_CODE_START_SESSION -> { - if (errorCode == CGMSpecificOpsControlPointCallback.CGM_ERROR_PROCEDURE_NOT_COMPLETED) { - // Session was already started before. - // Looks like the CGM Status characteristic has not been read, - // otherwise we would have got the Session Start Time before. - // The Session Start Time will be calculated when a next CGM - // packet is received based on it's Time Offset. + } else { + when (it.requestCode) { + CGMSpecificOpsControlPointCallback.CGM_OP_CODE_START_SESSION -> + if (it.errorCode == CGMSpecificOpsControlPointCallback.CGM_ERROR_PROCEDURE_NOT_COMPLETED) { + sessionStartTime = 0 } - sessionStartTime = 0 - } - CGMSpecificOpsControlPointCallback.CGM_OP_CODE_STOP_SESSION -> sessionStartTime = - 0 + CGMSpecificOpsControlPointCallback.CGM_OP_CODE_STOP_SESSION -> sessionStartTime = 0 } } + }.launchIn(scope) - override fun onCGMSpecificOpsResponseReceivedWithCrcError( - device: BluetoothDevice, - data: Data - ) { - log(Log.ERROR, "Request failed: CRC error") - } - }) - setIndicationCallback(recordAccessControlPointCharacteristic) - .with(object : RecordAccessControlPointDataCallback() { - @SuppressLint("SwitchIntDef") - override fun onRecordAccessOperationCompleted( - device: BluetoothDevice, - @RecordAccessControlPointCallback.RACPOpCode requestCode: Int - ) { - when (requestCode) { - RecordAccessControlPointCallback.RACP_OP_CODE_ABORT_OPERATION -> repository.setRequestStatus(RequestStatus.ABORTED) - else -> { - recordAccessRequestInProgress = false - repository.setRequestStatus(RequestStatus.SUCCESS) - } - } + setIndicationCallback(recordAccessControlPointCharacteristic).asValidResponseFlow() + .onEach { + if (it.isOperationCompleted && !it.wereRecordsFound() && it.numberOfRecords > 0) { + onRecordsReceived(it) + } else if (it.isOperationCompleted && !it.wereRecordsFound()) { + onNoRecordsFound() + } else if (it.isOperationCompleted && it.wereRecordsFound()) { + onOperationCompleted(it) + } else if (it.errorCode > 0) { + onError(it) } + }.launchIn(scope) - override fun onRecordAccessOperationCompletedWithNoRecordsFound( - device: BluetoothDevice, - @RecordAccessControlPointCallback.RACPOpCode requestCode: Int - ) { - recordAccessRequestInProgress = false - repository.setRequestStatus(RequestStatus.SUCCESS) - } + scope.launch(exceptionHandler) { + enableNotifications(cgmMeasurementCharacteristic).suspend() + } + scope.launch(exceptionHandler) { + enableIndications(cgmSpecificOpsControlPointCharacteristic).suspend() + } + scope.launch(exceptionHandler) { + enableIndications(recordAccessControlPointCharacteristic).suspend() + } - override fun onNumberOfRecordsReceived( - device: BluetoothDevice, - numberOfRecords: Int - ) { - if (numberOfRecords > 0) { - if (records.size() > 0) { - val sequenceNumber = records.keyAt(records.size() - 1) + 1 - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo( - sequenceNumber - ) - ) - .enqueue() - } else { - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportAllStoredRecords() - ) - .enqueue() - } - } else { - recordAccessRequestInProgress = false - repository.setRequestStatus(RequestStatus.SUCCESS) - } - } - - override fun onRecordAccessOperationError( - device: BluetoothDevice, - @RecordAccessControlPointCallback.RACPOpCode requestCode: Int, - @RecordAccessControlPointCallback.RACPErrorCode errorCode: Int - ) { - log(Log.WARN, "Record Access operation failed (error $errorCode)") - if (errorCode == RecordAccessControlPointCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) { - repository.setRequestStatus(RequestStatus.NOT_SUPPORTED) - } else { - repository.setRequestStatus(RequestStatus.FAILED) - } - } - }) - - // Enable notifications and indications - enableNotifications(cgmMeasurementCharacteristic) - .fail { _: BluetoothDevice?, status: Int -> - log( - Log.WARN, - "Failed to enable Continuous Glucose Measurement notifications ($status)" - ) - } - .enqueue() - enableIndications(cgmSpecificOpsControlPointCharacteristic) - .fail { _: BluetoothDevice?, status: Int -> - log( - Log.WARN, - "Failed to enable CGM Specific Ops Control Point indications notifications ($status)" - ) - } - .enqueue() - enableIndications(recordAccessControlPointCharacteristic) - .fail { _: BluetoothDevice?, status: Int -> - log( - Log.WARN, - "Failed to enabled Record Access Control Point indications (error $status)" - ) - } - .enqueue() - - // Start Continuous Glucose session if hasn't been started before if (sessionStartTime == 0L) { - writeCharacteristic( - cgmSpecificOpsControlPointCharacteristic, - CGMSpecificOpsControlPointData.startSession(secured) + scope.launch(exceptionHandler) { + writeCharacteristic( + cgmSpecificOpsControlPointCharacteristic, + CGMSpecificOpsControlPointData.startSession(secured), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() + } + } + } + + private suspend fun onRecordsReceived(response: RecordAccessControlPointResponse) { + if (response.numberOfRecords > 0) { + if (records.size() > 0) { + val sequenceNumber = records.keyAt(records.size() - 1) + 1 + writeCharacteristic( + recordAccessControlPointCharacteristic, + RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo( + sequenceNumber + ), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() + } else { + writeCharacteristic( + recordAccessControlPointCharacteristic, + RecordAccessControlPointData.reportAllStoredRecords(), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() + } + } else { + recordAccessRequestInProgress = false + repository.setRequestStatus(RequestStatus.SUCCESS) + } + } + + private fun onNoRecordsFound() { + recordAccessRequestInProgress = false + repository.setRequestStatus(RequestStatus.SUCCESS) + } + + private fun onOperationCompleted(response: RecordAccessControlPointResponse) { + when (response.requestCode) { + RecordAccessControlPointCallback.RACP_OP_CODE_ABORT_OPERATION -> repository.setRequestStatus( + RequestStatus.ABORTED ) - .fail { _: BluetoothDevice?, status: Int -> - log( - LogContract.Log.Level.ERROR, - "Failed to start session (error $status)" - ) - } - .enqueue() + else -> { + recordAccessRequestInProgress = false + repository.setRequestStatus(RequestStatus.SUCCESS) + } + } + } + + private fun onError(response: RecordAccessControlPointResponse) { + if (response.errorCode == RecordAccessControlPointCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) { + repository.setRequestStatus(RequestStatus.NOT_SUPPORTED) + } else { + repository.setRequestStatus(RequestStatus.FAILED) } } @@ -360,118 +246,49 @@ internal class CGMManager( } } - /** - * Returns a list of CGM records obtained from this device. The key in the array is the - */ - fun getRecords(): SparseArray { - return records - } - - /** - * Clears the records list locally - */ fun clear() { records.clear() } - /** - * Sends the request to obtain the last (most recent) record from glucose device. - * The data will be returned to Glucose Measurement characteristic as a notification followed by - * Record Access Control Point indication with status code Success or other in case of error. - */ fun requestLastRecord() { if (recordAccessControlPointCharacteristic == null) return clear() repository.setRequestStatus(RequestStatus.PENDING) recordAccessRequestInProgress = true - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportLastStoredRecord() - ).enqueue() + scope.launch(exceptionHandler) { + writeCharacteristic( + recordAccessControlPointCharacteristic, + RecordAccessControlPointData.reportLastStoredRecord(), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() + } } - /** - * Sends the request to obtain the first (oldest) record from glucose device. - * The data will be returned to Glucose Measurement characteristic as a notification followed by - * Record Access Control Point indication with status code Success or other in case of error. - */ fun requestFirstRecord() { if (recordAccessControlPointCharacteristic == null) return clear() repository.setRequestStatus(RequestStatus.PENDING) recordAccessRequestInProgress = true - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportFirstStoredRecord() - ).enqueue() + scope.launch(exceptionHandler) { + writeCharacteristic( + recordAccessControlPointCharacteristic, + RecordAccessControlPointData.reportFirstStoredRecord(), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() + } } - /** - * Sends abort operation signal to the device. - */ - fun abort() { - if (recordAccessControlPointCharacteristic == null) return - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.abortOperation() - ).enqueue() - } - - /** - * Sends the request to obtain all records from glucose device. Initially we want to notify the - * user about the number of the records so the Report Number of Stored Records request is send. - * The data will be returned to Glucose Measurement characteristic as a notification followed by - * Record Access Control Point indication with status code Success or other in case of error. - */ fun requestAllRecords() { if (recordAccessControlPointCharacteristic == null) return clear() repository.setRequestStatus(RequestStatus.PENDING) recordAccessRequestInProgress = true - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportNumberOfAllStoredRecords() - ).enqueue() - } - - /** - * Sends the request to obtain all records from glucose device. Initially we want to notify the - * user about the number of the records so the Report Number of Stored Records request is send. - * The data will be returned to Glucose Measurement characteristic as a notification followed by - * Record Access Control Point indication with status code Success or other in case of error. - */ - fun refreshRecords() { - if (recordAccessControlPointCharacteristic == null) return - if (records.size() == 0) { - requestAllRecords() - } else { - repository.setRequestStatus(RequestStatus.PENDING) - - // Obtain the last sequence number - val sequenceNumber = records.keyAt(records.size() - 1) + 1 - recordAccessRequestInProgress = true + scope.launch(exceptionHandler) { writeCharacteristic( recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber) - ).enqueue() - // Info: - // Operators OPERATOR_GREATER_THEN_OR_EQUAL, OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by the CGMS sample from SDK - // The "Operation not supported" response will be received + RecordAccessControlPointData.reportNumberOfAllStoredRecords(), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() } } - - /** - * Sends the request to remove all stored records from the Continuous Glucose Monitor device. - * This feature is not supported by the CGMS sample from the SDK, so monitor will answer with - * the Op Code Not Supported error. - */ - fun deleteAllRecords() { - if (recordAccessControlPointCharacteristic == null) return - clear() - repository.setRequestStatus(RequestStatus.PENDING) - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.deleteAllStoredRecords() - ).enqueue() - } } diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt index 473e4917..aa684bb8 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt @@ -16,7 +16,7 @@ internal class CGMService : ForegroundBleService() { @Inject lateinit var repository: CGMRepository - override val manager: CGMManager by lazy { CGMManager(this, repository) } + override val manager: CGMManager by lazy { CGMManager(this, scope, repository) } override fun onCreate() { super.onCreate() diff --git a/profile_csc/build.gradle b/profile_csc/build.gradle index 21f90c18..859f9004 100644 --- a/profile_csc/build.gradle +++ b/profile_csc/build.gradle @@ -7,6 +7,7 @@ dependencies { implementation project(":lib_utils") implementation libs.nordic.ble.common + implementation libs.nordic.ble.ktx implementation libs.nordic.log implementation libs.nordic.theme diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt index 800b0d82..cb58fc3a 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt @@ -49,9 +49,9 @@ internal data class CSCData( fun displayTotalDistance(): String { return when (selectedSpeedUnit) { - SpeedUnit.M_S -> String.format("%.2f km", distance) - SpeedUnit.KM_H -> String.format("%.2f km", distance) - SpeedUnit.MPH -> String.format("%.2f mile", distance) + SpeedUnit.M_S -> String.format("%.2f km", totalDistance) + SpeedUnit.KM_H -> String.format("%.2f km", totalDistance) + SpeedUnit.MPH -> String.format("%.2f mile", totalDistance) } } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCManager.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCManager.kt index 1426f542..df75637f 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCManager.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCManager.kt @@ -21,30 +21,41 @@ */ package no.nordicsemi.android.csc.repository -import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.content.Context import android.util.Log -import androidx.annotation.FloatRange -import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback -import no.nordicsemi.android.ble.data.Data +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementResponse +import no.nordicsemi.android.ble.ktx.asValidResponseFlow +import no.nordicsemi.android.ble.ktx.suspend import no.nordicsemi.android.csc.data.CSCRepository import no.nordicsemi.android.csc.data.WheelSize import no.nordicsemi.android.service.BatteryManager import java.util.* -/** Cycling Speed and Cadence service UUID. */ val CSC_SERVICE_UUID: UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb") - -/** Cycling Speed and Cadence Measurement characteristic UUID. */ private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb") -internal class CSCManager(context: Context, private val repository: CSCRepository) : BatteryManager(context) { +internal class CSCManager( + context: Context, + private val scope: CoroutineScope, + private val repository: CSCRepository +) : BatteryManager(context) { private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null private var wheelSize: WheelSize = WheelSize() + private var previousResponse: CyclingSpeedAndCadenceMeasurementResponse? = null + + private val exceptionHandler = CoroutineExceptionHandler { context, t-> + Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) + } + override fun onBatteryLevelChanged(batteryLevel: Int) { repository.setBatteryLevel(batteryLevel) } @@ -57,47 +68,30 @@ internal class CSCManager(context: Context, private val repository: CSCRepositor wheelSize = value } - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving indication, etc. - */ private inner class CSCManagerGattCallback : BatteryManagerGattCallback() { override fun initialize() { super.initialize() - // CSC characteristic is required - setNotificationCallback(cscMeasurementCharacteristic) - .with(object : CyclingSpeedAndCadenceMeasurementDataCallback() { - - override fun getWheelCircumference(): Float { - return wheelSize.value.toFloat() - } - - override fun onDistanceChanged( - device: BluetoothDevice, - @FloatRange(from = 0.0) totalDistance: Float, - @FloatRange(from = 0.0) distance: Float, - @FloatRange(from = 0.0) speed: Float - ) { + setNotificationCallback(cscMeasurementCharacteristic).asValidResponseFlow() + .onEach { + previousResponse?.let { previousResponse -> + val wheelCircumference = wheelSize.value.toFloat() + val totalDistance = it.getTotalDistance(wheelSize.value.toFloat()) + val distance = it.getDistance(wheelCircumference, previousResponse) + val speed = it.getSpeed(wheelCircumference, previousResponse) repository.setNewDistance(totalDistance, distance, speed, wheelSize) - } - override fun onCrankDataChanged( - device: BluetoothDevice, - @FloatRange(from = 0.0) crankCadence: Float, - gearRatio: Float - ) { + val crankCadence = it.getCrankCadence(previousResponse) + val gearRatio = it.getGearRatio(previousResponse) repository.setNewCrankCadence(crankCadence, gearRatio, wheelSize) } - override fun onInvalidDataReceived( - device: BluetoothDevice, - data: Data - ) { - log(Log.WARN, "Invalid CSC Measurement data received: $data") - } - }) - enableNotifications(cscMeasurementCharacteristic).enqueue() + previousResponse = it + }.launchIn(scope) + + scope.launch(exceptionHandler) { + enableNotifications(cscMeasurementCharacteristic).suspend() + } } public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt index d0e1f6db..2ece5e23 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt @@ -17,7 +17,7 @@ internal class CSCService : ForegroundBleService() { @Inject lateinit var repository: CSCRepository - override val manager: CSCManager by lazy { CSCManager(this, repository) } + override val manager: CSCManager by lazy { CSCManager(this, scope, repository) } override fun onCreate() { super.onCreate() diff --git a/profile_gls/build.gradle b/profile_gls/build.gradle index 31c9dccd..e76677f4 100644 --- a/profile_gls/build.gradle +++ b/profile_gls/build.gradle @@ -9,6 +9,7 @@ dependencies { implementation libs.chart implementation libs.nordic.ble.common + implementation libs.nordic.ble.ktx implementation libs.nordic.theme implementation libs.nordic.ui.scanner implementation libs.nordic.navigation diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsContentView.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsContentView.kt index 36d8f6de..ee2f72da 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsContentView.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsContentView.kt @@ -14,184 +14,196 @@ import androidx.compose.ui.unit.dp import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.data.GLSRecord import no.nordicsemi.android.gls.main.view.toDisplayString +import no.nordicsemi.android.theme.view.ScreenSection @Composable internal fun GLSDetailsContentView(record: GLSRecord) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.padding(16.dp)) { - Field( - stringResource(id = R.string.gls_details_sequence_number), - record.sequenceNumber.toString() - ) - - record.time?.let { + ScreenSection() { Field( - stringResource(id = R.string.gls_details_date_and_time), - stringResource(R.string.gls_timestamp, it) + stringResource(id = R.string.gls_details_sequence_number), + record.sequenceNumber.toString() ) - } - Divider( - color = MaterialTheme.colorScheme.secondary, - thickness = 1.dp, - modifier = Modifier.padding(vertical = 16.dp) - ) - - record.type?.let { - Field(stringResource(id = R.string.gls_details_type), it.toDisplayString()) - Spacer(modifier = Modifier.size(4.dp)) - } - - record.sampleLocation?.let { - Field(stringResource(id = R.string.gls_details_location), it.toDisplayString()) - Spacer(modifier = Modifier.size(4.dp)) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom - ) { - Text( - text = stringResource(id = R.string.gls_details_glucose_condensation_title), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline - ) - Text( - text = stringResource( - id = R.string.gls_details_glucose_condensation_field, - record.glucoseConcentration, - record.unit.toDisplayString() - ), - style = MaterialTheme.typography.titleLarge - ) - } - - Divider( - color = MaterialTheme.colorScheme.secondary, - thickness = 1.dp, - modifier = Modifier.padding(vertical = 16.dp) - ) - - record.status?.let { - BooleanField( - stringResource(id = R.string.gls_details_battery_low), - it.deviceBatteryLow - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_sensor_malfunction), - it.sensorMalfunction - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_insufficient_sample), - it.sampleSizeInsufficient - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_strip_insertion_error), - it.stripInsertionError - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_strip_type_incorrect), - it.stripTypeIncorrect - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_sensor_result_too_high), - it.sensorResultHigherThenDeviceCanProcess - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_sensor_result_too_low), - it.sensorResultLowerThenDeviceCanProcess - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_temperature_too_high), - it.sensorTemperatureTooHigh - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_temperature_too_low), - it.sensorTemperatureTooLow - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_strip_pulled_too_soon), - it.sensorReadInterrupted - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_general_device_fault), - it.generalDeviceFault - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField(stringResource(id = R.string.gls_details_time_fault), it.timeFault) - Spacer(modifier = Modifier.size(4.dp)) - } - - Divider( - color = MaterialTheme.colorScheme.secondary, - thickness = 1.dp, - modifier = Modifier.padding(vertical = 16.dp) - ) - - record.context?.let { - Field( - stringResource(id = R.string.gls_context_title), - stringResource(id = R.string.gls_available) - ) - Spacer(modifier = Modifier.size(4.dp)) - it.carbohydrate?.let { + record.time?.let { Field( - stringResource(id = R.string.gls_context_carbohydrate), - it.toDisplayString() + stringResource(id = R.string.gls_details_date_and_time), + stringResource(R.string.gls_timestamp, it) + ) + } + + Divider( + color = MaterialTheme.colorScheme.secondary, + thickness = 1.dp, + modifier = Modifier.padding(vertical = 16.dp) + ) + + record.type?.let { + Field(stringResource(id = R.string.gls_details_type), it.toDisplayString()) + Spacer(modifier = Modifier.size(4.dp)) + } + + record.sampleLocation?.let { + Field(stringResource(id = R.string.gls_details_location), it.toDisplayString()) + Spacer(modifier = Modifier.size(4.dp)) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom + ) { + Text( + text = stringResource(id = R.string.gls_details_glucose_condensation_title), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + Text( + text = stringResource( + id = R.string.gls_details_glucose_condensation_field, + record.glucoseConcentration, + record.unit.toDisplayString() + ), + style = MaterialTheme.typography.titleLarge + ) + } + + record.status?.let { + Divider( + color = MaterialTheme.colorScheme.secondary, + thickness = 1.dp, + modifier = Modifier.padding(vertical = 16.dp) + ) + + BooleanField( + stringResource(id = R.string.gls_details_battery_low), + it.deviceBatteryLow ) Spacer(modifier = Modifier.size(4.dp)) - } - it.meal?.let { - Field(stringResource(id = R.string.gls_context_meal), it.toDisplayString()) - Spacer(modifier = Modifier.size(4.dp)) - } - it.tester?.let { - Field(stringResource(id = R.string.gls_context_tester), it.toDisplayString()) - Spacer(modifier = Modifier.size(4.dp)) - } - it.health?.let { - Field(stringResource(id = R.string.gls_context_health), it.toDisplayString()) - Spacer(modifier = Modifier.size(4.dp)) - } - Field( - stringResource(id = R.string.gls_context_exercise_title), - stringResource( - id = R.string.gls_context_exercise_field, - it.exerciseDuration, - it.exerciseIntensity + BooleanField( + stringResource(id = R.string.gls_details_sensor_malfunction), + it.sensorMalfunction ) - ) - Spacer(modifier = Modifier.size(4.dp)) + Spacer(modifier = Modifier.size(4.dp)) + BooleanField( + stringResource(id = R.string.gls_details_insufficient_sample), + it.sampleSizeInsufficient + ) + Spacer(modifier = Modifier.size(4.dp)) + BooleanField( + stringResource(id = R.string.gls_details_strip_insertion_error), + it.stripInsertionError + ) + Spacer(modifier = Modifier.size(4.dp)) + BooleanField( + stringResource(id = R.string.gls_details_strip_type_incorrect), + it.stripTypeIncorrect + ) + Spacer(modifier = Modifier.size(4.dp)) + BooleanField( + stringResource(id = R.string.gls_details_sensor_result_too_high), + it.sensorResultHigherThenDeviceCanProcess + ) + Spacer(modifier = Modifier.size(4.dp)) + BooleanField( + stringResource(id = R.string.gls_details_sensor_result_too_low), + it.sensorResultLowerThenDeviceCanProcess + ) + Spacer(modifier = Modifier.size(4.dp)) + BooleanField( + stringResource(id = R.string.gls_details_temperature_too_high), + it.sensorTemperatureTooHigh + ) + Spacer(modifier = Modifier.size(4.dp)) + BooleanField( + stringResource(id = R.string.gls_details_temperature_too_low), + it.sensorTemperatureTooLow + ) + Spacer(modifier = Modifier.size(4.dp)) + BooleanField( + stringResource(id = R.string.gls_details_strip_pulled_too_soon), + it.sensorReadInterrupted + ) + Spacer(modifier = Modifier.size(4.dp)) + BooleanField( + stringResource(id = R.string.gls_details_general_device_fault), + it.generalDeviceFault + ) + Spacer(modifier = Modifier.size(4.dp)) + BooleanField(stringResource(id = R.string.gls_details_time_fault), it.timeFault) + Spacer(modifier = Modifier.size(4.dp)) + } - val medicationField = String.format( - stringResource(id = R.string.gls_context_medication_field), - it.medicationQuantity, - it.medicationUnit.toDisplayString(), - it.medication?.toDisplayString() - ) - Field(stringResource(id = R.string.gls_context_medication_title), medicationField) + record.context?.let { + Divider( + color = MaterialTheme.colorScheme.secondary, + thickness = 1.dp, + modifier = Modifier.padding(vertical = 16.dp) + ) - Spacer(modifier = Modifier.size(4.dp)) - Field( - stringResource(id = R.string.gls_context_hba1c_title), - stringResource(id = R.string.gls_context_hba1c_field, it.HbA1c) + Field( + stringResource(id = R.string.gls_context_title), + stringResource(id = R.string.gls_available) + ) + Spacer(modifier = Modifier.size(4.dp)) + it.carbohydrate?.let { + Field( + stringResource(id = R.string.gls_context_carbohydrate), + it.toDisplayString() + ) + Spacer(modifier = Modifier.size(4.dp)) + } + it.meal?.let { + Field(stringResource(id = R.string.gls_context_meal), it.toDisplayString()) + Spacer(modifier = Modifier.size(4.dp)) + } + it.tester?.let { + Field( + stringResource(id = R.string.gls_context_tester), + it.toDisplayString() + ) + Spacer(modifier = Modifier.size(4.dp)) + } + it.health?.let { + Field( + stringResource(id = R.string.gls_context_health), + it.toDisplayString() + ) + Spacer(modifier = Modifier.size(4.dp)) + } + Field( + stringResource(id = R.string.gls_context_exercise_title), + stringResource( + id = R.string.gls_context_exercise_field, + it.exerciseDuration, + it.exerciseIntensity + ) + ) + Spacer(modifier = Modifier.size(4.dp)) + + val medicationField = String.format( + stringResource(id = R.string.gls_context_medication_field), + it.medicationQuantity, + it.medicationUnit.toDisplayString(), + it.medication?.toDisplayString() + ) + Field( + stringResource(id = R.string.gls_context_medication_title), + medicationField + ) + + Spacer(modifier = Modifier.size(4.dp)) + Field( + stringResource(id = R.string.gls_context_hba1c_title), + stringResource(id = R.string.gls_context_hba1c_field, it.HbA1c) + ) + Spacer(modifier = Modifier.size(4.dp)) + } ?: Field( + stringResource(id = R.string.gls_context_title), + stringResource(id = R.string.gls_unavailable) ) - Spacer(modifier = Modifier.size(4.dp)) - } ?: Field( - stringResource(id = R.string.gls_context_title), - stringResource(id = R.string.gls_unavailable) - ) + } } } } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt index cf7f4ff0..181165ad 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt @@ -109,5 +109,6 @@ internal class GLSViewModel @Inject constructor( override fun onCleared() { super.onCleared() repository.clear() + glsManager.release() } } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt index 36ab0cb3..363001a4 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt @@ -21,53 +21,52 @@ */ package no.nordicsemi.android.gls.repository -import android.annotation.SuppressLint -import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.content.Context import android.util.Log import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback -import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextDataCallback -import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementDataCallback +import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointResponse +import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextResponse +import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementResponse import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData -import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPErrorCode -import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPOpCode -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementCallback.GlucoseStatus -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.* +import no.nordicsemi.android.ble.ktx.asValidResponseFlow +import no.nordicsemi.android.ble.ktx.suspend import no.nordicsemi.android.gls.data.* import no.nordicsemi.android.service.BatteryManager +import no.nordicsemi.android.service.CloseableCoroutineScope import java.util.* import javax.inject.Inject -import javax.inject.Singleton -/** Glucose service UUID */ val GLS_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb") -/** Glucose Measurement characteristic UUID */ private val GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb") - -/** Glucose Measurement Context characteristic UUID */ -private val GM_CONTEXT_CHARACTERISTIC = - UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb") - -/** Glucose Feature characteristic UUID */ +private val GM_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb") private val GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb") - -/** Record Access Control Point characteristic UUID */ private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb") -@Singleton internal class GLSManager @Inject constructor( @ApplicationContext context: Context, private val repository: GLSRepository ) : BatteryManager(context) { + private val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null + private val exceptionHandler = CoroutineExceptionHandler { _, t-> + Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) + } + override fun onBatteryLevelChanged(batteryLevel: Int) { repository.setNewBatteryLevel(batteryLevel) } @@ -76,183 +75,124 @@ internal class GLSManager @Inject constructor( return GlucoseManagerGattCallback() } - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving notification, etc. - */ private inner class GlucoseManagerGattCallback : BatteryManagerGattCallback() { override fun initialize() { super.initialize() - // The gatt.setCharacteristicNotification(...) method is called in BleManager during - // enabling notifications or indications - // (see BleManager#internalEnableNotifications/Indications). - // However, on Samsung S3 with Android 4.3 it looks like the 2 gatt calls - // (gatt.setCharacteristicNotification(...) and gatt.writeDescriptor(...)) are called - // too quickly, or from a wrong thread, and in result the notification listener is not - // set, causing onCharacteristicChanged(...) callback never being called when a - // notification comes. Enabling them here, like below, solves the problem. - // However... the original approach works for the Battery Level CCCD, which makes it - // even weirder. - /* - gatt.setCharacteristicNotification(glucoseMeasurementCharacteristic, true); - if (glucoseMeasurementContextCharacteristic != null) { - device.setCharacteristicNotification(glucoseMeasurementContextCharacteristic, true); - } - device.setCharacteristicNotification(recordAccessControlPointCharacteristic, true); - */ - setNotificationCallback(glucoseMeasurementCharacteristic) - .with(object : GlucoseMeasurementDataCallback() { - - override fun onGlucoseMeasurementReceived( - device: BluetoothDevice, - sequenceNumber: Int, - time: Calendar, - glucoseConcentration: Float?, - unit: Int?, - type: Int?, - sampleLocation: Int?, - status: GlucoseStatus?, - contextInformationFollows: Boolean - ) { - val record = GLSRecord( - sequenceNumber = sequenceNumber, - time = time, - glucoseConcentration = glucoseConcentration ?: 0f, - unit = unit?.let { ConcentrationUnit.create(it) } - ?: ConcentrationUnit.UNIT_KGPL, - type = RecordType.createOrNull(type), - sampleLocation = SampleLocation.createOrNull(sampleLocation), - status = status - ) - - repository.addNewRecord(record) - } - }) - setNotificationCallback(glucoseMeasurementContextCharacteristic) - .with(object : GlucoseMeasurementContextDataCallback() { - - override fun onGlucoseMeasurementContextReceived( - device: BluetoothDevice, - sequenceNumber: Int, - carbohydrate: Carbohydrate?, - carbohydrateAmount: Float?, - meal: Meal?, - tester: Tester?, - health: Health?, - exerciseDuration: Int?, - exerciseIntensity: Int?, - medication: Medication?, - medicationAmount: Float?, - medicationUnit: Int?, - HbA1c: Float? - ) { - val context = MeasurementContext( - sequenceNumber = sequenceNumber, - carbohydrate = carbohydrate, - carbohydrateAmount = carbohydrateAmount ?: 0f, - meal = meal, - tester = tester, - health = health, - exerciseDuration = exerciseDuration ?: 0, - exerciseIntensity = exerciseIntensity ?: 0, - medication = medication, - medicationQuantity = medicationAmount ?: 0f, - medicationUnit = medicationUnit?.let { MedicationUnit.create(it) } - ?: MedicationUnit.UNIT_KG, - HbA1c = HbA1c ?: 0f - ) - - repository.addNewContext(context) - } - }) - setIndicationCallback(recordAccessControlPointCharacteristic) - .with(object : RecordAccessControlPointDataCallback() { - - @SuppressLint("SwitchIntDef") - override fun onRecordAccessOperationCompleted( - device: BluetoothDevice, - @RACPOpCode requestCode: Int - ) { - val status = when (requestCode) { - RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED - else -> RequestStatus.SUCCESS - } - repository.setRequestStatus(status) - } - - override fun onRecordAccessOperationCompletedWithNoRecordsFound( - device: BluetoothDevice, - @RACPOpCode requestCode: Int - ) { - repository.setRequestStatus(RequestStatus.SUCCESS) - } - - override fun onNumberOfRecordsReceived( - device: BluetoothDevice, - numberOfRecords: Int - ) { - if (numberOfRecords > 0) { - if (repository.records().isNotEmpty()) { - val sequenceNumber = repository.records().last().sequenceNumber + 1 //TODO check if correct - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo( - sequenceNumber - ) - ) - .enqueue() - } else { - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportAllStoredRecords() - ) - .enqueue() - } - } - repository.setRequestStatus(RequestStatus.SUCCESS) - } - - override fun onRecordAccessOperationError( - device: BluetoothDevice, - @RACPOpCode requestCode: Int, - @RACPErrorCode errorCode: Int - ) { - log(Log.WARN, "Record Access operation failed (error $errorCode)") - if (errorCode == RACP_ERROR_OP_CODE_NOT_SUPPORTED) { - repository.setRequestStatus(RequestStatus.NOT_SUPPORTED) - } else { - repository.setRequestStatus(RequestStatus.FAILED) - } - } - }) - enableNotifications(glucoseMeasurementCharacteristic).enqueue() - enableNotifications(glucoseMeasurementContextCharacteristic).enqueue() - enableIndications(recordAccessControlPointCharacteristic) - .fail { device: BluetoothDevice?, status: Int -> - log( - Log.WARN, - "Failed to enabled Record Access Control Point indications (error $status)" + setNotificationCallback(glucoseMeasurementCharacteristic).asValidResponseFlow() + .onEach { + val record = GLSRecord( + sequenceNumber = it.sequenceNumber, + time = it.time, + glucoseConcentration = it.glucoseConcentration ?: 0f, + unit = it.unit?.let { ConcentrationUnit.create(it) } + ?: ConcentrationUnit.UNIT_KGPL, + type = RecordType.createOrNull(it.type), + sampleLocation = SampleLocation.createOrNull(it.sampleLocation), + status = it.status ) + + repository.addNewRecord(record) + }.launchIn(scope) + + setNotificationCallback(glucoseMeasurementContextCharacteristic).asValidResponseFlow() + .onEach { + val context = MeasurementContext( + sequenceNumber = it.sequenceNumber, + carbohydrate = it.carbohydrate, + carbohydrateAmount = it.carbohydrateAmount ?: 0f, + meal = it.meal, + tester = it.tester, + health = it.health, + exerciseDuration = it.exerciseDuration ?: 0, + exerciseIntensity = it.exerciseIntensity ?: 0, + medication = it.medication, + medicationQuantity = it.medicationAmount ?: 0f, + medicationUnit = it.medicationUnit?.let { MedicationUnit.create(it) } + ?: MedicationUnit.UNIT_KG, + HbA1c = it.hbA1c ?: 0f + ) + + repository.addNewContext(context) + }.launchIn(scope) + + setIndicationCallback(recordAccessControlPointCharacteristic).asValidResponseFlow() + .onEach { + if (it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords > 0) { + onNumberOfRecordsReceived(it) + } else if(it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords == 0) { + onRecordAccessOperationCompletedWithNoRecordsFound(it) + } else if (it.isOperationCompleted && it.wereRecordsFound()) { + onRecordAccessOperationCompleted(it) + } else if (it.errorCode > 0) { + onRecordAccessOperationError(it) + } + }.launchIn(scope) + + scope.launch(exceptionHandler) { + enableNotifications(glucoseMeasurementCharacteristic).suspend() + } + scope.launch(exceptionHandler) { + enableNotifications(glucoseMeasurementContextCharacteristic).suspend() + } + scope.launch(exceptionHandler) { + enableIndications(recordAccessControlPointCharacteristic).suspend() + } + } + + private fun onRecordAccessOperationCompleted(response: RecordAccessControlPointResponse) { + val status = when (response.requestCode) { + RecordAccessControlPointDataCallback.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED + else -> RequestStatus.SUCCESS + } + repository.setRequestStatus(status) + } + + private fun onRecordAccessOperationCompletedWithNoRecordsFound(response: RecordAccessControlPointResponse) { + repository.setRequestStatus(RequestStatus.SUCCESS) + } + + private suspend fun onNumberOfRecordsReceived(response: RecordAccessControlPointResponse) { + if (response.numberOfRecords > 0) { + if (repository.records().isNotEmpty()) { + val sequenceNumber = repository.records() + .last().sequenceNumber + 1 //TODO check if correct + writeCharacteristic( + recordAccessControlPointCharacteristic, + RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() + } else { + writeCharacteristic( + recordAccessControlPointCharacteristic, + RecordAccessControlPointData.reportAllStoredRecords(), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() } - .enqueue() + } + repository.setRequestStatus(RequestStatus.SUCCESS) + } + + private fun onRecordAccessOperationError(response: RecordAccessControlPointResponse) { + log(Log.WARN, "Record Access operation failed (error ${response.errorCode})") + if (response.errorCode == RecordAccessControlPointDataCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) { + repository.setRequestStatus(RequestStatus.NOT_SUPPORTED) + } else { + repository.setRequestStatus(RequestStatus.FAILED) + } } public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { val service = gatt.getService(GLS_SERVICE_UUID) if (service != null) { glucoseMeasurementCharacteristic = service.getCharacteristic(GM_CHARACTERISTIC) - glucoseMeasurementContextCharacteristic = service.getCharacteristic( - GM_CONTEXT_CHARACTERISTIC - ) - recordAccessControlPointCharacteristic = service.getCharacteristic( - RACP_CHARACTERISTIC - ) + glucoseMeasurementContextCharacteristic = service.getCharacteristic(GM_CONTEXT_CHARACTERISTIC) + recordAccessControlPointCharacteristic = service.getCharacteristic(RACP_CHARACTERISTIC) } return glucoseMeasurementCharacteristic != null && recordAccessControlPointCharacteristic != null } - override fun onServicesInvalidated() { } + override fun onServicesInvalidated() {} override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean { super.isOptionalServiceSupported(gatt) @@ -266,9 +206,6 @@ internal class GLSManager @Inject constructor( } } - /** - * Clears the records list locally. - */ private fun clear() { repository.clearRecords() val target = bluetoothDevice @@ -277,112 +214,47 @@ internal class GLSManager @Inject constructor( } } - /** - * Sends the request to obtain the last (most recent) record from glucose device. The data will - * be returned to Glucose Measurement characteristic as a notification followed by Record Access - * Control Point indication with status code Success or other in case of error. - */ fun requestLastRecord() { if (recordAccessControlPointCharacteristic == null) return val target = bluetoothDevice ?: return clear() repository.setRequestStatus(RequestStatus.PENDING) - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportLastStoredRecord() - ).enqueue() - } - - /** - * Sends the request to obtain the first (oldest) record from glucose device. The data will be - * returned to Glucose Measurement characteristic as a notification followed by Record Access - * Control Point indication with status code Success or other in case of error. - */ - fun requestFirstRecord() { - if (recordAccessControlPointCharacteristic == null) return - val target = bluetoothDevice ?: return - clear() - repository.setRequestStatus(RequestStatus.PENDING) - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportFirstStoredRecord() - ).enqueue() - } - - /** - * Sends the request to obtain all records from glucose device. Initially we want to notify user - * about the number of the records so the 'Report Number of Stored Records' is send. The data - * will be returned to Glucose Measurement characteristic as a notification followed by - * Record Access Control Point indication with status code Success or other in case of error. - */ - fun requestAllRecords() { - if (recordAccessControlPointCharacteristic == null) return - val target = bluetoothDevice ?: return - clear() - repository.setRequestStatus(RequestStatus.PENDING) - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportNumberOfAllStoredRecords() - ).enqueue() - } - - /** - * Sends the request to obtain from the glucose device all records newer than the newest one - * from local storage. The data will be returned to Glucose Measurement characteristic as - * a notification followed by Record Access Control Point indication with status code Success - * or other in case of error. - * - * - * Refresh button will not download records older than the oldest in the local memory. - * E.g. if you have pressed Last and then Refresh, than it will try to get only newer records. - * However if there are no records, it will download all existing (using [.getAllRecords]). - */ - fun refreshRecords() { - if (recordAccessControlPointCharacteristic == null) return - val target = bluetoothDevice ?: return - if (repository.records().isEmpty()) { - requestAllRecords() - } else { - repository.setRequestStatus(RequestStatus.PENDING) - - // obtain the last sequence number - val sequenceNumber = repository.records().last().sequenceNumber + 1 //TODO check if correct + scope.launch(exceptionHandler) { writeCharacteristic( recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber) - ).enqueue() - // Info: - // Operators OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by Nordic Semiconductor Glucose Service in SDK 4.4.2. + RecordAccessControlPointData.reportLastStoredRecord(), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() } } - /** - * Sends abort operation signal to the device. - */ - fun abort() { + fun requestFirstRecord() { if (recordAccessControlPointCharacteristic == null) return - val target = bluetoothDevice ?: return - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.abortOperation() - ).enqueue() - } - - /** - * Sends the request to delete all data from the device. A Record Access Control Point - * indication with status code Success (or other in case of error) will be send. - */ - fun deleteAllRecords() { - if (recordAccessControlPointCharacteristic == null) return - val target = bluetoothDevice ?: return clear() repository.setRequestStatus(RequestStatus.PENDING) - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.deleteAllStoredRecords() - ).enqueue() + scope.launch(exceptionHandler) { + writeCharacteristic( + recordAccessControlPointCharacteristic, + RecordAccessControlPointData.reportFirstStoredRecord(), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() + } + } - val elements = listOf(1, 2, 3) - val result = elements.all { it > 3 } + fun requestAllRecords() { + if (recordAccessControlPointCharacteristic == null) return + clear() + repository.setRequestStatus(RequestStatus.PENDING) + scope.launch(exceptionHandler) { + writeCharacteristic( + recordAccessControlPointCharacteristic, + RecordAccessControlPointData.reportNumberOfAllStoredRecords(), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() + } + } + + fun release() { + scope.close() } } diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt index ba425bec..e686240c 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt @@ -4,7 +4,6 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.hrs.data.HRSRepository -import no.nordicsemi.android.service.BleManagerStatus import no.nordicsemi.android.service.ForegroundBleService import javax.inject.Inject diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt index 3d6a568c..228cc955 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt @@ -1,6 +1,5 @@ package no.nordicsemi.android.hrs.viewmodel -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -25,7 +24,6 @@ internal class HRSViewModel @Inject constructor( ) : ViewModel() { val state = repository.data.combine(repository.status) { data, status -> - Log.d("AAATESTAAA", "data: $data, status: $status") when (status) { BleManagerStatus.CONNECTING -> LoadingState BleManagerStatus.OK, diff --git a/profile_hts/build.gradle b/profile_hts/build.gradle index ee04804e..129cd874 100644 --- a/profile_hts/build.gradle +++ b/profile_hts/build.gradle @@ -7,6 +7,7 @@ dependencies { implementation project(":lib_utils") implementation libs.nordic.ble.common + implementation libs.nordic.ble.ktx implementation libs.nordic.log implementation libs.nordic.theme diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSManager.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSManager.kt index c3fd2d4d..34890245 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSManager.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSManager.kt @@ -21,13 +21,18 @@ */ package no.nordicsemi.android.hts.repository -import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.content.Context -import no.nordicsemi.android.ble.common.callback.ht.TemperatureMeasurementDataCallback -import no.nordicsemi.android.ble.common.profile.ht.TemperatureType -import no.nordicsemi.android.ble.common.profile.ht.TemperatureUnit +import android.util.Log +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.ble.common.callback.ht.TemperatureMeasurementResponse +import no.nordicsemi.android.ble.ktx.asValidResponseFlow +import no.nordicsemi.android.ble.ktx.suspend import no.nordicsemi.android.hts.data.HTSRepository import no.nordicsemi.android.service.BatteryManager import java.util.* @@ -42,22 +47,14 @@ private val HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000- */ internal class HTSManager internal constructor( context: Context, + private val scope: CoroutineScope, private val dataHolder: HTSRepository ) : BatteryManager(context) { private var htCharacteristic: BluetoothGattCharacteristic? = null - private val temperatureMeasurementDataCallback = object : TemperatureMeasurementDataCallback() { - - override fun onTemperatureMeasurementReceived( - device: BluetoothDevice, - temperature: Float, - @TemperatureUnit unit: Int, - calendar: Calendar?, - @TemperatureType type: Int? - ) { - dataHolder.setNewTemperature(temperature) - } + private val exceptionHandler = CoroutineExceptionHandler { _, t-> + Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) } override fun onBatteryLevelChanged(batteryLevel: Int) { @@ -75,9 +72,16 @@ internal class HTSManager internal constructor( private inner class HTManagerGattCallback : BatteryManagerGattCallback() { override fun initialize() { super.initialize() + setIndicationCallback(htCharacteristic) - .with(temperatureMeasurementDataCallback) - enableIndications(htCharacteristic).enqueue() + .asValidResponseFlow() + .onEach { + dataHolder.setNewTemperature(it.temperature) + }.launchIn(scope) + + scope.launch(exceptionHandler) { + enableIndications(htCharacteristic).suspend() + } } override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt index aa576f35..ccaae41e 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt @@ -4,10 +4,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.hts.data.HTSRepository -import no.nordicsemi.android.service.BleManagerStatus -import no.nordicsemi.android.service.BleServiceStatus import no.nordicsemi.android.service.ForegroundBleService -import no.nordicsemi.android.utils.exhaustive import javax.inject.Inject @AndroidEntryPoint @@ -16,7 +13,7 @@ internal class HTSService : ForegroundBleService() { @Inject lateinit var repository: HTSRepository - override val manager: HTSManager by lazy { HTSManager(this, repository) } + override val manager: HTSManager by lazy { HTSManager(this, scope, repository) } override fun onCreate() { super.onCreate() diff --git a/profile_prx/build.gradle b/profile_prx/build.gradle index 4da933f5..d669dc22 100644 --- a/profile_prx/build.gradle +++ b/profile_prx/build.gradle @@ -7,6 +7,7 @@ dependencies { implementation project(":lib_utils") implementation libs.nordic.ble.common + implementation libs.nordic.ble.ktx implementation libs.nordic.log implementation libs.nordic.ui.scanner diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXRepository.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXRepository.kt index eef232d3..6085e7ea 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXRepository.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXRepository.kt @@ -41,8 +41,14 @@ internal class PRXRepository @Inject constructor() { } fun invokeCommand(command: PRXCommand) { - _command.tryEmit(command) - _status.tryEmit(BleManagerStatus.DISCONNECTED) + if (command == Disconnect) { + _command.tryEmit(command) + _status.tryEmit(BleManagerStatus.DISCONNECTED) + } else if (_command.subscriptionCount.value > 0) { + _command.tryEmit(command) + } else { + _status.tryEmit(BleManagerStatus.DISCONNECTED) + } } fun setNewStatus(status: BleManagerStatus) { diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXManager.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXManager.kt index 255b05f8..dabaf4cd 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXManager.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXManager.kt @@ -27,51 +27,43 @@ import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattServer import android.content.Context import android.util.Log -import no.nordicsemi.android.ble.callback.FailCallback +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import no.nordicsemi.android.ble.common.callback.alert.AlertLevelDataCallback import no.nordicsemi.android.ble.common.data.alert.AlertLevelData -import no.nordicsemi.android.ble.error.GattError +import no.nordicsemi.android.ble.ktx.suspend import no.nordicsemi.android.prx.data.PRXRepository import no.nordicsemi.android.service.BatteryManager import java.util.* -/** Link Loss service UUID. */ val LINK_LOSS_SERVICE_UUID = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb") - -/** Immediate Alert service UUID. */ val PRX_SERVICE_UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb") - -/** Alert Level characteristic UUID. */ val ALERT_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A06-0000-1000-8000-00805f9b34fb") internal class PRXManager( context: Context, + private val scope: CoroutineScope, private val dataHolder: PRXRepository ) : BatteryManager(context) { - // Client characteristics. private var alertLevelCharacteristic: BluetoothGattCharacteristic? = null private var linkLossCharacteristic: BluetoothGattCharacteristic? = null - // Server characteristics. private var localAlertLevelCharacteristic: BluetoothGattCharacteristic? = null private var linkLossServerCharacteristic: BluetoothGattCharacteristic? = null - /** - * Returns true if the alert has been enabled on the proximity tag, false otherwise. - */ - /** A flag indicating whether the alarm on the connected proximity tag has been activated. */ + + private val exceptionHandler = CoroutineExceptionHandler { _, t-> + Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) + } + var isAlertEnabled = false private set - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving indication, etc. - */ private inner class ProximityManagerGattCallback : BatteryManagerGattCallback() { override fun initialize() { super.initialize() - // This callback will be called whenever local Alert Level char is written - // by a connected proximity tag. + setWriteCallback(localAlertLevelCharacteristic) .with(object : AlertLevelDataCallback() { override fun onAlertLevelChanged(device: BluetoothDevice, level: Int) { @@ -86,21 +78,13 @@ internal class PRXManager( } }) - // After connection, set the Link Loss behaviour on the tag. - writeCharacteristic(linkLossCharacteristic, AlertLevelData.highAlert(), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) - .done { device: BluetoothDevice? -> - log( - Log.INFO, - "Link loss alert level set" - ) - } - .fail { device: BluetoothDevice?, status: Int -> - log( - Log.WARN, - "Failed to set link loss level: $status" - ) - } - .enqueue() + scope.launch(exceptionHandler) { + writeCharacteristic( + linkLossCharacteristic, + AlertLevelData.highAlert(), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).suspend() + } } override fun onServerReady(server: BluetoothGattServer) { @@ -141,48 +125,22 @@ internal class PRXManager( linkLossCharacteristic = null localAlertLevelCharacteristic = null linkLossServerCharacteristic = null - // Reset the alert flag isAlertEnabled = false } } - /** - * Toggles the immediate alert on the target device. - */ - fun toggleImmediateAlert() { - writeImmediateAlert(!isAlertEnabled) - } - - /** - * Writes the HIGH ALERT or NO ALERT command to the target device. - * - * @param on true to enable the alarm on proximity tag, false to disable it. - */ fun writeImmediateAlert(on: Boolean) { - if (!isConnected()) return - writeCharacteristic( - alertLevelCharacteristic, - if (on) AlertLevelData.highAlert() else AlertLevelData.noAlert() - ) - .before { device: BluetoothDevice? -> - log( - Log.VERBOSE, - if (on) "Setting alarm to HIGH..." else "Disabling alarm..." - ) - } - .done { device: BluetoothDevice? -> - isAlertEnabled = on - dataHolder.setRemoteAlarmLevel(on) - } - .fail { device: BluetoothDevice?, status: Int -> - log( - Log.WARN, - if (status == FailCallback.REASON_NULL_ATTRIBUTE) "Alert Level characteristic not found" else GattError.parse( - status - ) - ) - } - .enqueue() + if (!isConnected) return + scope.launch(exceptionHandler) { + writeCharacteristic( + alertLevelCharacteristic, + if (on) AlertLevelData.highAlert() else AlertLevelData.noAlert(), + BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE + ).suspend() + + isAlertEnabled = on + dataHolder.setRemoteAlarmLevel(on) + } } override fun onBatteryLevelChanged(batteryLevel: Int) { diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt index 4be3b2bb..c05f324e 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt @@ -1,5 +1,6 @@ package no.nordicsemi.android.prx.repository +import android.util.Log import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -22,7 +23,7 @@ internal class PRXService : ForegroundBleService() { private var serverManager: ProximityServerManager = ProximityServerManager(this) override val manager: PRXManager by lazy { - PRXManager(this, repository).apply { + PRXManager(this, scope, repository).apply { useServer(serverManager) } } diff --git a/profile_rscs/build.gradle b/profile_rscs/build.gradle index 4da933f5..d669dc22 100644 --- a/profile_rscs/build.gradle +++ b/profile_rscs/build.gradle @@ -7,6 +7,7 @@ dependencies { implementation project(":lib_utils") implementation libs.nordic.ble.common + implementation libs.nordic.ble.ktx implementation libs.nordic.log implementation libs.nordic.ui.scanner diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSData.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSData.kt index 6f2d8ce1..e4809e30 100644 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSData.kt +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSData.kt @@ -26,10 +26,9 @@ internal data class RSCSData( return "$instantaneousCadence RPM" } - - fun displayNumberOfSteps(): String { + fun displayNumberOfSteps(): String? { if (totalDistance == null || strideLength == null) { - return "NONE" + return null } val numberOfSteps = totalDistance/strideLength return "Number of Steps $numberOfSteps" diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSManager.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSManager.kt index 7195992f..307e9f71 100644 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSManager.kt +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSManager.kt @@ -21,40 +21,35 @@ */ package no.nordicsemi.android.rscs.repository -import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.content.Context -import no.nordicsemi.android.ble.common.callback.rsc.RunningSpeedAndCadenceMeasurementDataCallback +import android.util.Log +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.ble.common.callback.rsc.RunningSpeedAndCadenceMeasurementResponse +import no.nordicsemi.android.ble.ktx.asValidResponseFlow +import no.nordicsemi.android.ble.ktx.suspend import no.nordicsemi.android.rscs.data.RSCSRepository import no.nordicsemi.android.service.BatteryManager import java.util.* -/** Running Speed and Cadence Measurement service UUID */ val RSCS_SERVICE_UUID: UUID = UUID.fromString("00001814-0000-1000-8000-00805F9B34FB") - -/** Running Speed and Cadence Measurement characteristic UUID */ private val RSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A53-0000-1000-8000-00805F9B34FB") internal class RSCSManager internal constructor( context: Context, + private val scope: CoroutineScope, private val dataHolder: RSCSRepository ) : BatteryManager(context) { private var rscMeasurementCharacteristic: BluetoothGattCharacteristic? = null - private val callback = object : RunningSpeedAndCadenceMeasurementDataCallback() { - - override fun onRSCMeasurementReceived( - device: BluetoothDevice, - running: Boolean, - instantaneousSpeed: Float, - instantaneousCadence: Int, - strideLength: Int?, - totalDistance: Long? - ) { - dataHolder.setNewData(running, instantaneousSpeed, instantaneousCadence, strideLength, totalDistance) - } + private val exceptionHandler = CoroutineExceptionHandler { _, t-> + Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) } override fun onBatteryLevelChanged(batteryLevel: Int) { @@ -65,17 +60,18 @@ internal class RSCSManager internal constructor( return RSCManagerGattCallback() } - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving indication, etc. - */ private inner class RSCManagerGattCallback : BatteryManagerGattCallback() { override fun initialize() { super.initialize() - setNotificationCallback(rscMeasurementCharacteristic) - .with(callback) - enableNotifications(rscMeasurementCharacteristic).enqueue() + setNotificationCallback(rscMeasurementCharacteristic).asValidResponseFlow() + .onEach { + dataHolder.setNewData(it.isRunning, it.instantaneousSpeed, it.instantaneousCadence, it.strideLength, it.totalDistance) + }.launchIn(scope) + + scope.launch(exceptionHandler) { + enableNotifications(rscMeasurementCharacteristic).suspend() + } } public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSService.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSService.kt index 62e16585..20a932a5 100644 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSService.kt +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSService.kt @@ -4,10 +4,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.rscs.data.RSCSRepository -import no.nordicsemi.android.service.BleManagerStatus -import no.nordicsemi.android.service.BleServiceStatus import no.nordicsemi.android.service.ForegroundBleService -import no.nordicsemi.android.utils.exhaustive import javax.inject.Inject @AndroidEntryPoint @@ -16,7 +13,7 @@ internal class RSCSService : ForegroundBleService() { @Inject lateinit var repository: RSCSRepository - override val manager: RSCSManager by lazy { RSCSManager(this, repository) } + override val manager: RSCSManager by lazy { RSCSManager(this, scope, repository) } override fun onCreate() { super.onCreate() diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/SensorsReadingView.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/SensorsReadingView.kt index 92c97ae8..e517bc67 100644 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/SensorsReadingView.kt +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/SensorsReadingView.kt @@ -26,10 +26,9 @@ internal fun SensorsReadingView(state: RSCSData) { Spacer(modifier = Modifier.height(4.dp)) KeyValueField(stringResource(id = R.string.rscs_cadence), state.displayCadence()) Spacer(modifier = Modifier.height(4.dp)) - KeyValueField( - stringResource(id = R.string.rscs_number_of_steps), - state.displayNumberOfSteps() - ) + state.displayNumberOfSteps()?.let { + KeyValueField(stringResource(id = R.string.rscs_number_of_steps), it) + } } } diff --git a/profile_uart/build.gradle b/profile_uart/build.gradle index ee04804e..129cd874 100644 --- a/profile_uart/build.gradle +++ b/profile_uart/build.gradle @@ -7,6 +7,7 @@ dependencies { implementation project(":lib_utils") implementation libs.nordic.ble.common + implementation libs.nordic.ble.ktx implementation libs.nordic.log implementation libs.nordic.theme diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTManager.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTManager.kt index 3b6bbdf5..fddde2a6 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTManager.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTManager.kt @@ -26,51 +26,53 @@ import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattService import android.content.Context import android.text.TextUtils +import android.util.Log +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import no.nordicsemi.android.ble.WriteRequest -import no.nordicsemi.android.log.LogContract +import no.nordicsemi.android.ble.ktx.asFlow +import no.nordicsemi.android.ble.ktx.suspend import no.nordicsemi.android.service.BatteryManager import no.nordicsemi.android.uart.data.UARTRepository import no.nordicsemi.android.utils.EMPTY import java.util.* -/** Nordic UART Service UUID */ val UART_SERVICE_UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") - -/** RX characteristic UUID */ private val UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") - -/** TX characteristic UUID */ private val UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") -internal class UARTManager(context: Context, private val dataHolder: UARTRepository) : BatteryManager(context) { +internal class UARTManager( + context: Context, + private val scope: CoroutineScope, + private val dataHolder: UARTRepository +) : BatteryManager(context) { private var rxCharacteristic: BluetoothGattCharacteristic? = null private var txCharacteristic: BluetoothGattCharacteristic? = null - /** - * A flag indicating whether Long Write can be used. It's set to false if the UART RX - * characteristic has only PROPERTY_WRITE_NO_RESPONSE property and no PROPERTY_WRITE. - * If you set it to false here, it will never use Long Write. - * - * TODO change this flag if you don't want to use Long Write even with Write Request. - */ + private val exceptionHandler = CoroutineExceptionHandler { _, t-> + Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) + } + private var useLongWrite = true - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving indication, etc. - */ private inner class UARTManagerGattCallback : BatteryManagerGattCallback() { override fun initialize() { - setNotificationCallback(txCharacteristic) - .with { device, data -> - val text: String = data.getStringValue(0) ?: String.EMPTY - log(LogContract.Log.Level.APPLICATION, "\"$text\" received") - dataHolder.emitNewMessage(text) - } - requestMtu(260).enqueue() - enableNotifications(txCharacteristic).enqueue() + setNotificationCallback(txCharacteristic).asFlow().onEach { + val text: String = it.getStringValue(0) ?: String.EMPTY + dataHolder.emitNewMessage(text) + } + + scope.launch(exceptionHandler) { + requestMtu(260).suspend() + } + + scope.launch(exceptionHandler) { + enableNotifications(txCharacteristic).suspend() + } } override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { @@ -113,21 +115,15 @@ internal class UARTManager(context: Context, private val dataHolder: UARTReposit } fun send(text: String) { - // Are we connected? if (rxCharacteristic == null) return if (!TextUtils.isEmpty(text)) { - val request: WriteRequest = writeCharacteristic(rxCharacteristic, text.toByteArray()) - .with { device, data -> - log( - LogContract.Log.Level.APPLICATION, - "\"" + data.getStringValue(0).toString() + "\" sent" - ) + scope.launch(exceptionHandler) { + val request: WriteRequest = writeCharacteristic(rxCharacteristic, text.toByteArray(), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) + if (!useLongWrite) { + request.split() } - if (!useLongWrite) { - // This will automatically split the long data into MTU-3-byte long packets. - request.split() + request.enqueue() } - request.enqueue() } } diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt index 04c65923..a561add3 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt @@ -3,7 +3,6 @@ package no.nordicsemi.android.uart.repository import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import no.nordicsemi.android.service.BleManagerStatus import no.nordicsemi.android.service.ForegroundBleService import no.nordicsemi.android.uart.data.DisconnectCommand import no.nordicsemi.android.uart.data.SendTextCommand @@ -17,7 +16,7 @@ internal class UARTService : ForegroundBleService() { @Inject lateinit var repository: UARTRepository - override val manager: UARTManager by lazy { UARTManager(this, repository) } + override val manager: UARTManager by lazy { UARTManager(this, scope, repository) } override fun onCreate() { super.onCreate() diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt index 0aba9fd1..38125332 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -24,6 +25,7 @@ internal fun UARTAddMacroDialog(onDismiss: () -> Unit, onEvent: (UARTViewEvent) AlertDialog( onDismissRequest = onDismiss, + containerColor = MaterialTheme.colorScheme.background, title = { Text(text = stringResource(id = R.string.uart_macro_dialog_title)) }, diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt index ffe1e17f..98bef87e 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.PlayArrow @@ -15,6 +17,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import no.nordicsemi.android.material.you.Card @@ -26,13 +29,14 @@ internal fun MacroItem(macro: UARTMacro, onEvent: (UARTViewEvent) -> Unit) { Card(backgroundColor = MaterialTheme.colorScheme.primaryContainer) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(16.dp) ) { Icon( imageVector = Icons.Default.PlayArrow, contentDescription = stringResource(id = R.string.uart_run_macro_description), modifier = Modifier - .size(40.dp) + .size(70.dp) + .padding(8.dp) + .clip(RoundedCornerShape(8.dp)) .clickable { onEvent(OnRunMacro(macro)) } ) @@ -56,7 +60,10 @@ internal fun MacroItem(macro: UARTMacro, onEvent: (UARTViewEvent) -> Unit) { imageVector = Icons.Default.Delete, contentDescription = stringResource(id = R.string.uart_delete_macro_description), modifier = Modifier - .size(32.dp) + .padding(8.dp) + .padding(end = 8.dp) + .size(40.dp) + .clip(RoundedCornerShape(8.dp)) .clickable { onEvent(OnDeleteMacro(macro)) } ) } diff --git a/settings.gradle b/settings.gradle index bdd5507b..91fae7b2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,6 +14,7 @@ dependencyResolutionManagement { alias('nordic-navigation').to('no.nordicsemi.android.common:navigation:1.0.0') alias('nordic-theme').to('no.nordicsemi.android.common:theme:1.0.0') alias('nordic-ble-common').to('no.nordicsemi.android:ble-common:2.3.1') + alias('nordic-ble-ktx').to('no.nordicsemi.android:ble-ktx:2.3.1') alias('nordic-log').to('no.nordicsemi.android:log:2.3.0') alias('nordic-scanner').to('no.nordicsemi.android.support.v18:scanner:1.6.0') alias('nordic-ui-scanner').to('no.nordicsemi.android.common:scanner:1.0.0') @@ -91,3 +92,7 @@ include ':lib_utils' if (file('../Android-Common-Libraries').exists()) { includeBuild('../Android-Common-Libraries') } + +if (file('../Android-BLE-Library').exists()) { + includeBuild('../Android-BLE-Library') +}