diff --git a/gradle.properties b/gradle.properties index 7b00938f..7c49c88c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -46,7 +46,5 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official \ No newline at end of file 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 920234d6..615d69d1 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 @@ -110,10 +110,10 @@ internal class BPSViewModel @Inject constructor( } } - private fun startGattClient(blinkyDevice: ServerDevice) = viewModelScope.launch { - _state.value = _state.value.copy(deviceName = blinkyDevice.name) + private fun startGattClient(device: ServerDevice) = viewModelScope.launch { + _state.value = _state.value.copy(deviceName = device.name) - client = blinkyDevice.connect(context) + client = device.connect(context) client.connectionState .filterNotNull() 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 89c4a506..f9d0f303 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 @@ -80,8 +80,8 @@ internal class CSCService : NotificationService() { return START_REDELIVER_INTENT } - private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch { - client = blinkyDevice.connect(this@CSCService) + private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { + client = device.connect(this@CSCService) client.connectionState .onEach { repository.onConnectionStateChanged(it) } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/GLSDestination.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/GLSDestination.kt index 00e0ef92..a39603e1 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/GLSDestination.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/GLSDestination.kt @@ -33,9 +33,10 @@ package no.nordicsemi.android.gls import no.nordicsemi.android.common.navigation.createDestination import no.nordicsemi.android.common.navigation.defineDestination -import no.nordicsemi.android.gls.data.GLSRecord import no.nordicsemi.android.gls.details.view.GLSDetailsScreen +import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord +import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext -internal val GlsDetailsDestinationId = createDestination("gls-details-screen") +internal val GlsDetailsDestinationId = createDestination, Unit>("gls-details-screen") val GLSDestination = defineDestination(GlsDetailsDestinationId) { GLSDetailsScreen() } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/DataMapper.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/DataMapper.kt deleted file mode 100644 index 46657dea..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/DataMapper.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.data - -import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextResponse -import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementResponse - -internal fun GlucoseMeasurementResponse.toRecord(): GLSRecord { - return this.let { - 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 - ) - } -} - -internal fun GlucoseMeasurementContextResponse.toMeasurementContext(): MeasurementContext { - return this.let { - 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 - ) - } -} - -internal fun GLSRecord.copyWithNewContext(response: GlucoseMeasurementContextResponse): GLSRecord { - return copy(context = context) -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSManager.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSManager.kt deleted file mode 100644 index 6166ffc1..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSManager.kt +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package no.nordicsemi.android.gls.data - -import android.bluetooth.BluetoothGatt -import android.bluetooth.BluetoothGattCharacteristic -import android.content.Context -import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import no.nordicsemi.android.ble.BleManager -import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback -import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointResponse -import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse -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.ktx.asValidResponseFlow -import no.nordicsemi.android.ble.ktx.suspend -import no.nordicsemi.android.common.logger.NordicLogger -import no.nordicsemi.android.service.ConnectionObserverAdapter -import no.nordicsemi.android.utils.launchWithCatch -import java.util.* - -val GLS_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb") - -private val GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb") -private val GM_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb") -private val GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb") -private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb") - -private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -internal class GLSManager( - context: Context, - private val scope: CoroutineScope, - private val logger: NordicLogger -) : BleManager(context) { - - private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null - private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null - private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null - private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null - - private val data = MutableStateFlow(GLSServiceData()) - val dataHolder = ConnectionObserverAdapter() - - init { - connectionObserver = dataHolder - - data.onEach { - dataHolder.setValue(it) - }.launchIn(scope) - } - - override fun log(priority: Int, message: String) { - logger.log(priority, message) - } - - override fun getMinLogPriority(): Int { - return Log.VERBOSE - } - - override fun getGattCallback(): BleManagerGattCallback { - return GlucoseManagerGattCallback() - } - - private inner class GlucoseManagerGattCallback : BleManagerGattCallback() { - override fun initialize() { - super.initialize() - - setNotificationCallback(glucoseMeasurementCharacteristic).asValidResponseFlow() - .onEach { data.tryEmit(data.value.copy(records = data.value.records + it.toRecord())) } - .launchIn(scope) - - setNotificationCallback(glucoseMeasurementContextCharacteristic).asValidResponseFlow() - .onEach { - val context = it.toMeasurementContext() - data.value.records.find { context.sequenceNumber == it.sequenceNumber }?.let { - it.context = context - } - data.tryEmit(data.value) - }.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) - - setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow() - .onEach { - data.value = data.value.copy(batteryLevel = it.batteryLevel) - }.launchIn(scope) - - enableNotifications(glucoseMeasurementCharacteristic).enqueue() - enableNotifications(glucoseMeasurementContextCharacteristic).enqueue() - enableIndications(recordAccessControlPointCharacteristic).enqueue() - enableNotifications(batteryLevelCharacteristic).enqueue() - } - - private fun onRecordAccessOperationCompleted(response: RecordAccessControlPointResponse) { - val status = when (response.requestCode) { - RecordAccessControlPointDataCallback.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED - else -> RequestStatus.SUCCESS - } - data.tryEmit(data.value.copy(requestStatus = status)) - } - - private fun onRecordAccessOperationCompletedWithNoRecordsFound(response: RecordAccessControlPointResponse) { - data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS)) - } - - private suspend fun onNumberOfRecordsReceived(response: RecordAccessControlPointResponse) { - if (response.numberOfRecords > 0) { - if (data.value.records.isNotEmpty()) { - val sequenceNumber = data.value.records.last().sequenceNumber + 1 - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo( - sequenceNumber - ), - BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - ).suspend() - } else { - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportAllStoredRecords(), - BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - ).suspend() - } - } - data.tryEmit(data.value.copy(requestStatus = 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) { - data.tryEmit(data.value.copy(requestStatus = RequestStatus.NOT_SUPPORTED)) - } else { - data.tryEmit(data.value.copy(requestStatus = RequestStatus.FAILED)) - } - } - - public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { - gatt.getService(GLS_SERVICE_UUID)?.run { - glucoseMeasurementCharacteristic = getCharacteristic(GM_CHARACTERISTIC) - glucoseMeasurementContextCharacteristic = getCharacteristic(GM_CONTEXT_CHARACTERISTIC) - recordAccessControlPointCharacteristic = getCharacteristic(RACP_CHARACTERISTIC) - } - gatt.getService(BATTERY_SERVICE_UUID)?.run { - batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - } - return glucoseMeasurementCharacteristic != null && recordAccessControlPointCharacteristic != null - } - - override fun onServicesInvalidated() { - glucoseMeasurementCharacteristic = null - glucoseMeasurementContextCharacteristic = null - recordAccessControlPointCharacteristic = null - } - } - - private fun clear() { - data.tryEmit(data.value.copy(records = mapOf())) - val target = bluetoothDevice - if (target != null) { - data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS)) - } - } - - fun requestLastRecord() { - if (recordAccessControlPointCharacteristic == null) return - val target = bluetoothDevice ?: return - clear() - data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING)) - scope.launchWithCatch { - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportLastStoredRecord(), - BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - ).suspend() - } - } - - fun requestFirstRecord() { - if (recordAccessControlPointCharacteristic == null) return - clear() - data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING)) - scope.launchWithCatch { - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportFirstStoredRecord(), - BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - ).suspend() - } - } - - fun requestAllRecords() { - if (recordAccessControlPointCharacteristic == null) return - clear() - data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING)) - scope.launchWithCatch { - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportNumberOfAllStoredRecords(), - BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - ).suspend() - } - } -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRecord.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRecord.kt deleted file mode 100644 index cc4ec51c..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRecord.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package no.nordicsemi.android.gls.data - -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementCallback.GlucoseStatus -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Carbohydrate -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Health -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Meal -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Medication -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Tester -import java.util.* - -internal data class GLSRecord( - val sequenceNumber: Int = 0, - val time: Calendar? = null, - val glucoseConcentration: Float = 0f, - val unit: ConcentrationUnit = ConcentrationUnit.UNIT_KGPL, - val type: RecordType? = null, - val status: GlucoseStatus? = null, - val sampleLocation: SampleLocation? = null, - var context: MeasurementContext? = null -) - -internal enum class RecordType(val id: Int) { - CAPILLARY_WHOLE_BLOOD(1), - CAPILLARY_PLASMA(2), - VENOUS_WHOLE_BLOOD(3), - VENOUS_PLASMA(4), - ARTERIAL_WHOLE_BLOOD(5), - ARTERIAL_PLASMA(6), - UNDETERMINED_WHOLE_BLOOD(7), - UNDETERMINED_PLASMA(8), - INTERSTITIAL_FLUID(9), - CONTROL_SOLUTION(10); - - companion object { - fun create(value: Int): RecordType { - return values().firstOrNull { it.id == value.toInt() } - ?: throw IllegalArgumentException("Cannot find element for provided value.") - } - - fun createOrNull(value: Int?): RecordType? { - return values().firstOrNull { it.id == value } - } - } -} - -internal data class MeasurementContext( - val sequenceNumber: Int = 0, - val carbohydrate: Carbohydrate? = null, - val carbohydrateAmount: Float = 0f, - val meal: Meal? = null, - val tester: Tester? = null, - val health: Health? = null, - val exerciseDuration: Int = 0, - val exerciseIntensity: Int = 0, - val medication: Medication?, - val medicationQuantity: Float = 0f, - val medicationUnit: MedicationUnit = MedicationUnit.UNIT_KG, - val HbA1c: Float = 0f -) - -internal enum class ConcentrationUnit(val id: Int) { - UNIT_KGPL(0), - UNIT_MOLPL(1); - - companion object { - fun create(value: Int): ConcentrationUnit { - return values().firstOrNull { it.id == value } - ?: throw IllegalArgumentException("Cannot find element for provided value.") - } - } -} - -internal enum class MedicationUnit(val id: Int) { - UNIT_KG(0), - UNIT_L(1); - - companion object { - fun create(value: Int): MedicationUnit { - return values().firstOrNull { it.id == value } - ?: throw IllegalArgumentException("Cannot find element for provided value.") - } - } -} - -internal enum class SampleLocation(val id: Int) { - FINGER(1), - AST(2), - EARLOBE(3), - CONTROL_SOLUTION(4), - NOT_AVAILABLE(15); - - companion object { - fun createOrNull(value: Int?): SampleLocation? { - return values().firstOrNull { it.id == value } - } - } -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSServiceData.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSServiceData.kt index 0f621f9a..f5a5cb0a 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSServiceData.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSServiceData.kt @@ -32,10 +32,12 @@ package no.nordicsemi.android.gls.data import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GlucoseMeasurementContext +import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord +import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext +import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus internal data class GLSServiceData( - val records: Map = mapOf(), + val records: Map = mapOf(), val batteryLevel: Int? = null, val connectionState: GattConnectionState? = null, val requestStatus: RequestStatus = RequestStatus.IDLE diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/RequestStatus.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/RequestStatus.kt deleted file mode 100644 index f369da4a..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/RequestStatus.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.data - -internal enum class RequestStatus { - IDLE, PENDING, SUCCESS, ABORTED, FAILED, NOT_SUPPORTED -} 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 fccc7909..e0ca33e2 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 @@ -49,12 +49,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource 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.kotlin.ble.profile.gls.data.GLSRecord +import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext import no.nordicsemi.android.ui.view.ScreenSection @Composable -internal fun GLSDetailsContentView(record: GLSRecord) { +internal fun GLSDetailsContentView(record: GLSRecord, context: GLSMeasurementContext?) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.padding(16.dp)) { ScreenSection { @@ -86,24 +87,28 @@ internal fun GLSDetailsContentView(record: GLSRecord) { 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.glucoseConcentration?.let { glucoseConcentration -> + record.unit?.let { unit -> + 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, + glucoseConcentration, + unit.toDisplayString() + ), + style = MaterialTheme.typography.titleLarge + ) + } + } } record.status?.let { @@ -172,7 +177,7 @@ internal fun GLSDetailsContentView(record: GLSRecord) { Spacer(modifier = Modifier.size(4.dp)) } - record.context?.let { + context?.let { Divider( color = MaterialTheme.colorScheme.secondary, thickness = 1.dp, @@ -209,33 +214,42 @@ internal fun GLSDetailsContentView(record: GLSRecord) { ) 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 + it.exerciseDuration?.let { exerciseDuration -> + it.exerciseIntensity?.let { exerciseIntensity -> + Field( + stringResource(id = R.string.gls_context_exercise_title), + stringResource( + id = R.string.gls_context_exercise_field, + exerciseDuration, + exerciseIntensity + ) + ) + } + } + + it.medicationUnit?.let { medicationUnit -> + Spacer(modifier = Modifier.size(4.dp)) + val medicationField = String.format( + stringResource(id = R.string.gls_context_medication_field), + it.medicationQuantity, + medicationUnit.toDisplayString(), + it.medication?.toDisplayString() ) - ) - Spacer(modifier = Modifier.size(4.dp)) + Field( + stringResource(id = R.string.gls_context_medication_title), + medicationField + ) + } - 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 - ) + it.HbA1c?.let { hbA1c -> + Spacer(modifier = Modifier.size(4.dp)) + Field( + stringResource(id = R.string.gls_context_hba1c_title), + stringResource(id = R.string.gls_context_hba1c_field, hbA1c) + ) + } 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) diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsMappers.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsMappers.kt index 9d3401ca..63a6ddbb 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsMappers.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsMappers.kt @@ -33,15 +33,15 @@ package no.nordicsemi.android.gls.details.view import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Carbohydrate -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Health -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Meal -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Medication -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Tester import no.nordicsemi.android.gls.R -import no.nordicsemi.android.gls.data.ConcentrationUnit -import no.nordicsemi.android.gls.data.MedicationUnit -import no.nordicsemi.android.gls.data.SampleLocation +import no.nordicsemi.android.kotlin.ble.profile.gls.data.Carbohydrate +import no.nordicsemi.android.kotlin.ble.profile.gls.data.ConcentrationUnit +import no.nordicsemi.android.kotlin.ble.profile.gls.data.Health +import no.nordicsemi.android.kotlin.ble.profile.gls.data.Meal +import no.nordicsemi.android.kotlin.ble.profile.gls.data.Medication +import no.nordicsemi.android.kotlin.ble.profile.gls.data.MedicationUnit +import no.nordicsemi.android.kotlin.ble.profile.gls.data.SampleLocation +import no.nordicsemi.android.kotlin.ble.profile.gls.data.Tester @Composable internal fun SampleLocation.toDisplayString(): String { @@ -65,8 +65,8 @@ internal fun ConcentrationUnit.toDisplayString(): String { @Composable internal fun MedicationUnit.toDisplayString(): String { return when (this) { - MedicationUnit.UNIT_KG -> stringResource(id = R.string.gls_sample_location_kg) - MedicationUnit.UNIT_L -> stringResource(id = R.string.gls_sample_location_l) + MedicationUnit.UNIT_MG -> stringResource(id = R.string.gls_sample_location_kg) + MedicationUnit.UNIT_ML -> stringResource(id = R.string.gls_sample_location_l) } } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsScreen.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsScreen.kt index 07791fac..0a668199 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsScreen.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsScreen.kt @@ -50,6 +50,6 @@ internal fun GLSDetailsScreen() { viewModel.navigateBack() } - GLSDetailsContentView(record) + GLSDetailsContentView(record.first, record.second) } } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSContentView.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSContentView.kt index 958e8100..f8c9830b 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSContentView.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSContentView.kt @@ -58,10 +58,10 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.data.GLSServiceData -import no.nordicsemi.android.gls.data.GLSRecord -import no.nordicsemi.android.gls.data.RequestStatus import no.nordicsemi.android.gls.data.WorkingMode import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel +import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord +import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus import no.nordicsemi.android.ui.view.BatteryLevelView import no.nordicsemi.android.ui.view.ScreenSection import no.nordicsemi.android.ui.view.SectionTitle @@ -139,7 +139,7 @@ private fun RecordsViewWithData(state: GLSServiceData) { Spacer(modifier = Modifier.height(16.dp)) - state.records.forEachIndexed { i, it -> + state.records.keys.forEachIndexed { i, it -> RecordItem(it) if (i < state.records.size - 1) { @@ -184,13 +184,12 @@ private fun RecordItem(record: GLSRecord) { style = MaterialTheme.typography.bodySmall ) - Text( - text = glucoseConcentrationDisplayValue( - record.glucoseConcentration, - record.unit - ), - style = MaterialTheme.typography.labelLarge, - ) + record.glucoseConcentration?.let { glucoseConcentration -> record.unit?.let { unit -> + Text( + text = glucoseConcentrationDisplayValue(glucoseConcentration, unit), + style = MaterialTheme.typography.labelLarge, + ) + } } } } } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSMapper.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSMapper.kt index 356531e8..0e5e8658 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSMapper.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSMapper.kt @@ -34,9 +34,9 @@ package no.nordicsemi.android.gls.main.view import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import no.nordicsemi.android.gls.R -import no.nordicsemi.android.gls.data.ConcentrationUnit -import no.nordicsemi.android.gls.data.RecordType import no.nordicsemi.android.gls.data.WorkingMode +import no.nordicsemi.android.kotlin.ble.profile.gls.data.ConcentrationUnit +import no.nordicsemi.android.kotlin.ble.profile.gls.data.RecordType @Composable internal fun RecordType?.toDisplayString(): String { diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreen.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreen.kt index 0f6e956e..7637ad12 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreen.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreen.kt @@ -48,15 +48,7 @@ import no.nordicsemi.android.common.ui.scanner.view.DeviceDisconnectedView import no.nordicsemi.android.common.ui.scanner.view.Reason import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel -import no.nordicsemi.android.service.ConnectedResult -import no.nordicsemi.android.service.ConnectingResult -import no.nordicsemi.android.service.DeviceHolder -import no.nordicsemi.android.service.DisconnectedResult -import no.nordicsemi.android.service.IdleResult -import no.nordicsemi.android.service.LinkLossResult -import no.nordicsemi.android.service.MissingServiceResult -import no.nordicsemi.android.service.SuccessResult -import no.nordicsemi.android.service.UnknownErrorResult +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.ui.view.BackIconAppBar import no.nordicsemi.android.ui.view.LoggerIconAppBar import no.nordicsemi.android.ui.view.NavigateUpButton @@ -78,19 +70,15 @@ fun GLSScreen() { .padding(16.dp) .verticalScroll(rememberScrollState()) ) { - when (state) { - NoDeviceState -> DeviceConnectingView() - is WorkingState -> when (state.result) { - is IdleResult, - is ConnectingResult -> DeviceConnectingView { NavigateUpButton(navigateUp) } - is ConnectedResult -> DeviceConnectingView { NavigateUpButton(navigateUp) } - is DisconnectedResult -> DeviceDisconnectedView(Reason.USER) { NavigateUpButton(navigateUp) } - is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS) { NavigateUpButton(navigateUp) } - is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE) { - NavigateUpButton(navigateUp) - } - is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) } - is SuccessResult -> GLSContentView(state.result.data) { viewModel.onEvent(it) } + if (state.deviceName == null) { + DeviceConnectingView() + } else { + when (state.glsServiceData.connectionState) { + null, + GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } + GattConnectionState.STATE_DISCONNECTED, + GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) } + GattConnectionState.STATE_CONNECTED -> GLSContentView(state.glsServiceData) { viewModel.onEvent(it) } } } } @@ -99,14 +87,10 @@ fun GLSScreen() { @Composable private fun AppBar(state: GLSViewState, navigateUp: () -> Unit, viewModel: GLSViewModel) { - val toolbarName = (state as? WorkingState)?.let { - (it.result as? DeviceHolder)?.deviceName() - } - - if (toolbarName == null) { + if (state.deviceName == null) { BackIconAppBar(stringResource(id = R.string.gls_title), navigateUp) } else { - LoggerIconAppBar(toolbarName, { + LoggerIconAppBar(state.deviceName, { viewModel.onEvent(DisconnectEvent) }, { viewModel.onEvent(DisconnectEvent) }) { viewModel.onEvent(OpenLoggerEvent) diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreenViewEvent.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreenViewEvent.kt index 1a964db9..2ca44d61 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreenViewEvent.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreenViewEvent.kt @@ -31,8 +31,8 @@ package no.nordicsemi.android.gls.main.view -import no.nordicsemi.android.gls.data.GLSRecord import no.nordicsemi.android.gls.data.WorkingMode +import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord internal sealed class GLSScreenViewEvent diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSState.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSState.kt index 646eb79b..b77e792c 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSState.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSState.kt @@ -32,13 +32,20 @@ package no.nordicsemi.android.gls.main.view import no.nordicsemi.android.gls.data.GLSServiceData -import no.nordicsemi.android.gls.data.RequestStatus +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext +import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord +import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus internal data class GLSViewState( val glsServiceData: GLSServiceData = GLSServiceData(), val deviceName: String? = null ) { + fun copyWithNewConnectionState(connectionState: GattConnectionState): GLSViewState { + return copy(glsServiceData = glsServiceData.copy(connectionState = connectionState)) + } + fun copyAndClear(): GLSViewState { return copy(glsServiceData = glsServiceData.copy(records = mapOf(), requestStatus = RequestStatus.IDLE)) } @@ -46,4 +53,24 @@ internal data class GLSViewState( fun copyWithNewRequestStatus(requestStatus: RequestStatus): GLSViewState { return copy(glsServiceData = glsServiceData.copy(requestStatus = requestStatus)) } + + fun copyWithNewBatteryLevel(batteryLevel: Int): GLSViewState { + return copy(glsServiceData = glsServiceData.copy(batteryLevel = batteryLevel)) + } + + //todo optimise + fun copyWithNewRecord(record: GLSRecord): GLSViewState { + val records = glsServiceData.records.toMutableMap() + records[record] = null + return copy(glsServiceData = glsServiceData.copy(records = records.toMap())) + } + + //todo optimise + fun copyWithNewContext(context: GLSMeasurementContext): GLSViewState { + val records = glsServiceData.records.toMutableMap() + return records.keys.firstOrNull { it.sequenceNumber == context.sequenceNumber }?.let { + records[it] = context + copy(glsServiceData = glsServiceData.copy(records = records.toMap())) + } ?: this + } } 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 0a83ecca..c48be0b8 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 @@ -48,13 +48,9 @@ import kotlinx.coroutines.launch import no.nordicsemi.android.analytics.AppAnalytics import no.nordicsemi.android.analytics.Profile import no.nordicsemi.android.analytics.ProfileConnectedEvent -import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback -import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointResponse import no.nordicsemi.android.common.navigation.NavigationResult import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.gls.GlsDetailsDestinationId -import no.nordicsemi.android.gls.data.GLS_SERVICE_UUID -import no.nordicsemi.android.gls.data.RequestStatus import no.nordicsemi.android.gls.data.WorkingMode import no.nordicsemi.android.gls.main.view.DisconnectEvent import no.nordicsemi.android.gls.main.view.GLSScreenViewEvent @@ -72,10 +68,13 @@ import no.nordicsemi.android.kotlin.ble.profile.gls.GlucoseMeasurementContextPar import no.nordicsemi.android.kotlin.ble.profile.gls.GlucoseMeasurementParser import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointInputParser import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointParser +import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord import no.nordicsemi.android.kotlin.ble.profile.gls.data.NumberOfRecordsData import no.nordicsemi.android.kotlin.ble.profile.gls.data.RecordAccessControlPointData +import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus import no.nordicsemi.android.kotlin.ble.profile.gls.data.ResponseData -import no.nordicsemi.android.service.ConnectedResult +import no.nordicsemi.android.kotlin.ble.profile.racp.RACPOpCode +import no.nordicsemi.android.kotlin.ble.profile.racp.RACPResponseCode import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId import java.util.* import javax.inject.Inject @@ -102,12 +101,14 @@ internal class GLSViewModel @Inject constructor( private lateinit var client: BleGattClient private lateinit var glucoseMeasurementCharacteristic: BleGattCharacteristic - private lateinit var glucoseMeasurementContextCharacteristic: BleGattCharacteristic private lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic private val _state = MutableStateFlow(GLSViewState()) val state = _state.asStateFlow() + private val highestSequenceNumber + get() = state.value.glsServiceData.records.keys.maxByOrNull { it.sequenceNumber }?.sequenceNumber ?: -1 + init { navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(GLS_SERVICE_UUID)) @@ -125,16 +126,20 @@ internal class GLSViewModel @Inject constructor( fun onEvent(event: GLSScreenViewEvent) { when (event) { - OpenLoggerEvent -> repository.openLogger() + OpenLoggerEvent -> TODO() DisconnectEvent -> navigationManager.navigateUp() - is OnWorkingModeSelected -> repository.requestMode(event.workingMode) - is OnGLSRecordClick -> navigationManager.navigateTo(GlsDetailsDestinationId, event.record) + is OnWorkingModeSelected -> onEvent(event) + is OnGLSRecordClick -> navigateToDetails(event.record) DisconnectEvent -> navigationManager.navigateUp() } } + private fun navigateToDetails(record: GLSRecord) { + val context = state.value.glsServiceData.records[record] + navigationManager.navigateTo(GlsDetailsDestinationId, record to context) + } + private fun onDeviceSelected(device: ServerDevice) { - _state.value = _state.value.copy(deviceName = device.name) startGattClient(device) } @@ -146,100 +151,98 @@ internal class GLSViewModel @Inject constructor( } } - private fun connectDevice(device: ServerDevice) { - repository.downloadData(viewModelScope, device).onEach { - _state.value = WorkingState(it) - - (it as? ConnectedResult)?.let { - analytics.logEvent(ProfileConnectedEvent(Profile.GLS)) - } - }.launchIn(viewModelScope) - } - - private fun startGattClient(blinkyDevice: ServerDevice) = viewModelScope.launch { - client = blinkyDevice.connect(context) + private fun startGattClient(device: ServerDevice) = viewModelScope.launch { + client = device.connect(context) client.connectionState - .onEach { _state.value = _state.value.copy() } .filterNotNull() + .onEach { _state.value = _state.value.copyWithNewConnectionState(it) } .onEach { stopIfDisconnected(it) } + .onEach { logAnalytics(it) } .launchIn(viewModelScope) client.services .filterNotNull() - .onEach { configureGatt(it) } + .onEach { configureGatt(it, device) } .launchIn(viewModelScope) } - private suspend fun configureGatt(services: BleGattServices) { + private fun logAnalytics(connectionState: GattConnectionState) { + if (connectionState == GattConnectionState.STATE_CONNECTED) { + analytics.logEvent(ProfileConnectedEvent(Profile.GLS)) + } + } + + private suspend fun configureGatt(services: BleGattServices, device: ServerDevice) { val glsService = services.findService(GLS_SERVICE_UUID)!! glucoseMeasurementCharacteristic = glsService.findCharacteristic(GM_CHARACTERISTIC)!! - glucoseMeasurementContextCharacteristic = glsService.findCharacteristic(GM_CONTEXT_CHARACTERISTIC)!! recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!! val batteryService = services.findService(BATTERY_SERVICE_UUID)!! val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!! batteryLevelCharacteristic.getNotifications() .mapNotNull { BatteryLevelParser.parse(it) } - .onEach { repository.onBatteryLevelChanged(it) } + .onEach { _state.value = _state.value.copyWithNewBatteryLevel(it) } .launchIn(viewModelScope) glucoseMeasurementCharacteristic.getNotifications() .mapNotNull { GlucoseMeasurementParser.parse(it) } - .onEach { } + .onEach { _state.value = _state.value.copyWithNewRecord(it) } .launchIn(viewModelScope) - glucoseMeasurementContextCharacteristic.getNotifications() - .mapNotNull { GlucoseMeasurementContextParser.parse(it) } - .onEach { } - .launchIn(viewModelScope) + glsService.findCharacteristic(GM_CONTEXT_CHARACTERISTIC)?.getNotifications() + ?.mapNotNull { GlucoseMeasurementContextParser.parse(it) } + ?.onEach { _state.value = _state.value.copyWithNewContext(it) } + ?.launchIn(viewModelScope) recordAccessControlPointCharacteristic.getNotifications() .mapNotNull { RecordAccessControlPointParser.parse(it) } .onEach { onAccessControlPointDataReceived(it) } .launchIn(viewModelScope) + + _state.value = _state.value.copy(deviceName = device.name) } private fun stopIfDisconnected(connectionState: GattConnectionState) { if (connectionState == GattConnectionState.STATE_DISCONNECTED) { - stopSelf() + navigationManager.navigateUp() } } - private fun onAccessControlPointDataReceived(data: RecordAccessControlPointData) { + private fun onAccessControlPointDataReceived(data: RecordAccessControlPointData) = viewModelScope.launch { when (data) { - is NumberOfRecordsData -> if () - is ResponseData -> TODO() - } - 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) + is NumberOfRecordsData -> onNumberOfRecordsReceived(data.numberOfRecords) + is ResponseData -> when (data.responseCode) { + RACPResponseCode.RACP_RESPONSE_SUCCESS -> onRecordAccessOperationCompleted(data.requestCode) + RACPResponseCode.RACP_ERROR_NO_RECORDS_FOUND -> onRecordAccessOperationCompletedWithNoRecordsFound() + RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED, + RACPResponseCode.RACP_ERROR_INVALID_OPERATOR, + RACPResponseCode.RACP_ERROR_OPERATOR_NOT_SUPPORTED, + RACPResponseCode.RACP_ERROR_INVALID_OPERAND, + RACPResponseCode.RACP_ERROR_ABORT_UNSUCCESSFUL, + RACPResponseCode.RACP_ERROR_PROCEDURE_NOT_COMPLETED, + RACPResponseCode.RACP_ERROR_OPERAND_NOT_SUPPORTED -> onRecordAccessOperationError(data.responseCode) + } } } - private fun onRecordAccessOperationCompleted(response: RecordAccessControlPointResponse) { - val status = when (response.requestCode) { - RecordAccessControlPointDataCallback.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED + private fun onRecordAccessOperationCompleted(requestCode: RACPOpCode) { + val status = when (requestCode) { + RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED else -> RequestStatus.SUCCESS } _state.value = _state.value.copyWithNewRequestStatus(status) } - private fun onRecordAccessOperationCompletedWithNoRecordsFound(response: RecordAccessControlPointResponse) { + private fun onRecordAccessOperationCompletedWithNoRecordsFound() { _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.SUCCESS) } - private suspend fun onNumberOfRecordsReceived(response: RecordAccessControlPointResponse) { - if (response.numberOfRecords > 0) { - if (data.value.records.isNotEmpty()) { - val sequenceNumber = data.value.records.last().sequenceNumber + 1 + private suspend fun onNumberOfRecordsReceived(numberOfRecords: Int) { + if (numberOfRecords > 0) { + if (state.value.glsServiceData.records.isNotEmpty()) { recordAccessControlPointCharacteristic.write( - RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber).value + RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(highestSequenceNumber).value ) } else { recordAccessControlPointCharacteristic.write( @@ -250,8 +253,8 @@ internal class GLSViewModel @Inject constructor( _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.SUCCESS) } - private fun onRecordAccessOperationError(response: RecordAccessControlPointResponse) { - if (response.errorCode == RecordAccessControlPointDataCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) { + private fun onRecordAccessOperationError(response: RACPResponseCode) { + if (response == RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED) { _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.NOT_SUPPORTED) } else { _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.FAILED) @@ -262,21 +265,21 @@ internal class GLSViewModel @Inject constructor( _state.value = _state.value.copyAndClear() } - suspend fun requestLastRecord() { + private suspend fun requestLastRecord() { + clear() + _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING) recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportLastStoredRecord().value) - clear() - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING) } - suspend fun requestFirstRecord() { + private suspend fun requestFirstRecord() { + clear() + _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING) recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportFirstStoredRecord().value) - clear() - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING) } - suspend fun requestAllRecords() { - recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value) + private suspend fun requestAllRecords() { clear() _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING) + recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value) } } 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 e267a7dc..137cfc4e 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 @@ -82,8 +82,8 @@ internal class HRSService : NotificationService() { return START_REDELIVER_INTENT } - private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch { - client = blinkyDevice.connect(this@HRSService) + private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { + client = device.connect(this@HRSService) client.connectionState .onEach { repository.onConnectionStateChanged(it) } 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 bd308176..f6bd271b 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 @@ -80,8 +80,8 @@ internal class HTSService : NotificationService() { return START_REDELIVER_INTENT } - private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch { - client = blinkyDevice.connect(this@HTSService) + private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { + client = device.connect(this@HTSService) client.connectionState .onEach { repository.onConnectionStateChanged(it) } 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 f1236945..7c907373 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 @@ -80,8 +80,8 @@ internal class RSCSService : NotificationService() { return START_REDELIVER_INTENT } - private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch { - client = blinkyDevice.connect(this@RSCSService) + private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { + client = device.connect(this@RSCSService) client.connectionState .onEach { repository.onConnectionStateChanged(it) }