From 60d41868fbd95f3a694252e4d04749b1f6aad7e4 Mon Sep 17 00:00:00 2001 From: Sylwester Zielinski Date: Fri, 17 Mar 2023 10:28:47 +0100 Subject: [PATCH] Migrate CGM profile to new BLE library --- profile_cgms/build.gradle.kts | 1 + .../nordicsemi/android/cgms/data/CGMData.kt | 38 --- .../android/cgms/data/CGMManager.kt | 323 ------------------ .../nordicsemi/android/cgms/data/CGMRecord.kt | 42 --- .../android/cgms/data/CGMServiceData.kt | 18 + .../no/nordicsemi/android/cgms/data/Ext.kt | 43 --- .../android/cgms/data/RequestStatus.kt | 36 -- .../android/cgms/repository/CGMRepository.kt | 87 ++--- .../android/cgms/repository/CGMService.kt | 238 ++++++++++++- .../android/cgms/view/CGMContentView.kt | 16 +- .../nordicsemi/android/cgms/view/CGMMapper.kt | 9 +- .../nordicsemi/android/cgms/view/CGMScreen.kt | 42 +-- .../android/cgms/view/CGMViewState.kt | 11 +- .../android/cgms/viewmodel/CGMViewModel.kt | 20 +- .../view/{GLSState.kt => GLSViewState.kt} | 0 .../gls/main/viewmodel/GLSViewModel.kt | 2 +- 16 files changed, 326 insertions(+), 600 deletions(-) delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMData.kt delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMManager.kt delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRecord.kt create mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceData.kt delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/Ext.kt delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/RequestStatus.kt rename profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/{GLSState.kt => GLSViewState.kt} (100%) diff --git a/profile_cgms/build.gradle.kts b/profile_cgms/build.gradle.kts index f444d877..204279e2 100644 --- a/profile_cgms/build.gradle.kts +++ b/profile_cgms/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(libs.nordic.theme) implementation(libs.nordic.uiscanner) implementation(libs.nordic.navigation) + implementation(libs.nordic.core) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.compose.material.iconsExtended) diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMData.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMData.kt deleted file mode 100644 index a0764930..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMData.kt +++ /dev/null @@ -1,38 +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.cgms.data - -internal data class CGMData( - val records: List = emptyList(), - val batteryLevel: Int? = null, - val requestStatus: RequestStatus = RequestStatus.IDLE -) diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMManager.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMManager.kt deleted file mode 100644 index 55c1189e..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMManager.kt +++ /dev/null @@ -1,323 +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.cgms.data - -import android.bluetooth.BluetoothGatt -import android.bluetooth.BluetoothGattCharacteristic -import android.content.Context -import android.util.Log -import android.util.SparseArray -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.RecordAccessControlPointResponse -import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse -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.ktx.asValidResponseFlow -import no.nordicsemi.android.ble.ktx.suspend -import no.nordicsemi.android.ble.ktx.suspendForValidResponse -import no.nordicsemi.android.common.logger.NordicLogger -import no.nordicsemi.android.service.ConnectionObserverAdapter -import no.nordicsemi.android.utils.launchWithCatch -import java.util.* - -val CGMS_SERVICE_UUID: 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 RACP_UUID = 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 CGMManager( - context: Context, - private val scope: CoroutineScope, - private val logger: NordicLogger -) : BleManager(context) { - - private var cgmStatusCharacteristic: BluetoothGattCharacteristic? = null - private var cgmFeatureCharacteristic: BluetoothGattCharacteristic? = null - private var cgmMeasurementCharacteristic: BluetoothGattCharacteristic? = null - private var cgmSpecificOpsControlPointCharacteristic: BluetoothGattCharacteristic? = null - private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null - private val records: SparseArray = SparseArray() - private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null - - private var secured = false - - private var recordAccessRequestInProgress = false - - private var sessionStartTime: Long = 0 - - private val data = MutableStateFlow(CGMData()) - val dataHolder = ConnectionObserverAdapter() - - init { - connectionObserver = dataHolder - - data.onEach { - dataHolder.setValue(it) - }.launchIn(scope) - } - - override fun getGattCallback(): BleManagerGattCallback { - return CGMManagerGattCallback() - } - - override fun log(priority: Int, message: String) { - logger.log(priority, message) - } - - override fun getMinLogPriority(): Int { - return Log.VERBOSE - } - - private inner class CGMManagerGattCallback : BleManagerGattCallback() { - override fun initialize() { - super.initialize() - - setNotificationCallback(cgmMeasurementCharacteristic).asValidResponseFlow() - .onEach { - if (sessionStartTime == 0L && !recordAccessRequestInProgress) { - val timeOffset = it.items.minOf { it.timeOffset } - sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L - } - - it.items.map { - val timestamp = sessionStartTime + it.timeOffset * 60000L - val item = CGMRecord(it.timeOffset, it.glucoseConcentration, timestamp) - records.put(item.sequenceNumber, item) - } - - data.value = data.value.copy(records = 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 - } - } else { - when (it.requestCode) { - CGMSpecificOpsControlPointCallback.CGM_OP_CODE_START_SESSION -> - if (it.errorCode == CGMSpecificOpsControlPointCallback.CGM_ERROR_PROCEDURE_NOT_COMPLETED) { - sessionStartTime = 0 - } - CGMSpecificOpsControlPointCallback.CGM_OP_CODE_STOP_SESSION -> sessionStartTime = - 0 - } - } - }.launchIn(scope) - - 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) - - setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow() - .onEach { - data.value = data.value.copy(batteryLevel = it.batteryLevel) - }.launchIn(scope) - - enableNotifications(cgmMeasurementCharacteristic).enqueue() - enableIndications(cgmSpecificOpsControlPointCharacteristic).enqueue() - enableIndications(recordAccessControlPointCharacteristic).enqueue() - enableNotifications(batteryLevelCharacteristic).enqueue() - - scope.launchWithCatch { - val cgmResponse = readCharacteristic(cgmFeatureCharacteristic).suspendForValidResponse() - this@CGMManager.secured = cgmResponse.features.e2eCrcSupported - } - - scope.launchWithCatch { - val response = readCharacteristic(cgmStatusCharacteristic).suspendForValidResponse() - if (response.status?.sessionStopped == false) { - sessionStartTime = System.currentTimeMillis() - response.timeOffset * 60000L - } - } - - scope.launchWithCatch { - if (sessionStartTime == 0L) { - 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 - data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS) - } - } - - private fun onNoRecordsFound() { - recordAccessRequestInProgress = false - data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS) - } - - private fun onOperationCompleted(response: RecordAccessControlPointResponse) { - when (response.requestCode) { - RecordAccessControlPointCallback.RACP_OP_CODE_ABORT_OPERATION -> - data.value = data.value.copy(requestStatus = RequestStatus.ABORTED) - else -> { - recordAccessRequestInProgress = false - data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS) - } - } - } - - private fun onError(response: RecordAccessControlPointResponse) { - if (response.errorCode == RecordAccessControlPointCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) { - data.value = data.value.copy(requestStatus = RequestStatus.NOT_SUPPORTED) - } else { - data.value = data.value.copy(requestStatus = RequestStatus.FAILED) - } - } - - override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { - gatt.getService(CGMS_SERVICE_UUID)?.run { - cgmStatusCharacteristic = getCharacteristic(CGM_STATUS_UUID) - cgmFeatureCharacteristic = getCharacteristic(CGM_FEATURE_UUID) - cgmMeasurementCharacteristic = getCharacteristic(CGM_MEASUREMENT_UUID) - cgmSpecificOpsControlPointCharacteristic = getCharacteristic(CGM_OPS_CONTROL_POINT_UUID) - recordAccessControlPointCharacteristic = getCharacteristic(RACP_UUID) - } - gatt.getService(BATTERY_SERVICE_UUID)?.run { - batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - } - return cgmMeasurementCharacteristic != null - && cgmSpecificOpsControlPointCharacteristic != null - && recordAccessControlPointCharacteristic != null - && cgmStatusCharacteristic != null - && cgmFeatureCharacteristic != null - } - - override fun onServicesInvalidated() { - cgmStatusCharacteristic = null - cgmFeatureCharacteristic = null - cgmMeasurementCharacteristic = null - cgmSpecificOpsControlPointCharacteristic = null - recordAccessControlPointCharacteristic = null - batteryLevelCharacteristic = null - } - } - - private fun clear() { - records.clear() - } - - fun requestLastRecord() { - if (recordAccessControlPointCharacteristic == null) return - clear() - data.value = data.value.copy(requestStatus = RequestStatus.PENDING) - recordAccessRequestInProgress = true - scope.launchWithCatch { - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportLastStoredRecord(), - BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - ).suspend() - } - } - - fun requestFirstRecord() { - if (recordAccessControlPointCharacteristic == null) return - clear() - data.value = data.value.copy(requestStatus = RequestStatus.PENDING) - recordAccessRequestInProgress = true - scope.launchWithCatch { - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportFirstStoredRecord(), - BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - ).suspend() - } - } - - fun requestAllRecords() { - if (recordAccessControlPointCharacteristic == null) return - clear() - data.value = data.value.copy(requestStatus = RequestStatus.PENDING) - recordAccessRequestInProgress = true - scope.launchWithCatch { - writeCharacteristic( - recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportNumberOfAllStoredRecords(), - BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - ).suspend() - } - } -} diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRecord.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRecord.kt deleted file mode 100644 index 3bcef7e0..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRecord.kt +++ /dev/null @@ -1,42 +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.cgms.data - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -internal data class CGMRecord( - var sequenceNumber: Int, - var glucoseConcentration: Float, - var timestamp: Long -) : Parcelable diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceData.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceData.kt new file mode 100644 index 00000000..d3f20a28 --- /dev/null +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceData.kt @@ -0,0 +1,18 @@ +package no.nordicsemi.android.cgms.data + +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMRecord +import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus + +internal data class CGMServiceData( + val records: List = emptyList(), + val batteryLevel: Int? = null, + val connectionState: GattConnectionState? = null, + val requestStatus: RequestStatus = RequestStatus.IDLE +) + +data class CGMRecordWithSequenceNumber( + val sequenceNumber: Int, + val record: CGMRecord, + val timestamp: Long +) diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/Ext.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/Ext.kt deleted file mode 100644 index 80861747..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/Ext.kt +++ /dev/null @@ -1,43 +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.cgms.data - -import android.util.SparseArray -import androidx.core.util.keyIterator - -internal fun SparseArray.toList(): List { - val list = mutableListOf() - this.keyIterator().forEach { - list.add(get(it)) - } - return list.sortedBy { it.sequenceNumber }.toList() -} diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/RequestStatus.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/RequestStatus.kt deleted file mode 100644 index 6ded6128..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/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.cgms.data - -internal enum class RequestStatus { - IDLE, PENDING, SUCCESS, ABORTED, FAILED, NOT_SUPPORTED -} diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt index f76367d9..98117309 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt @@ -33,23 +33,19 @@ package no.nordicsemi.android.cgms.repository import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.ble.ktx.suspend -import no.nordicsemi.android.cgms.data.CGMData -import no.nordicsemi.android.cgms.data.CGMManager -import no.nordicsemi.android.common.logger.NordicLogger -import no.nordicsemi.android.common.logger.NordicLoggerFactory +import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber +import no.nordicsemi.android.cgms.data.CGMServiceCommand +import no.nordicsemi.android.cgms.data.CGMServiceData +import no.nordicsemi.android.common.core.simpleSharedFlow import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.service.BleManagerResult -import no.nordicsemi.android.service.IdleResult +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus +import no.nordicsemi.android.service.DisconnectAndStopEvent import no.nordicsemi.android.service.ServiceManager -import no.nordicsemi.android.ui.view.StringConst import javax.inject.Inject import javax.inject.Singleton @@ -58,68 +54,53 @@ class CGMRepository @Inject constructor( @ApplicationContext private val context: Context, private val serviceManager: ServiceManager, - private val loggerFactory: NordicLoggerFactory, - private val stringConst: StringConst ) { - private var manager: CGMManager? = null - private var logger: NordicLogger? = null - - private val _data = MutableStateFlow>(IdleResult()) + private val _data = MutableStateFlow(CGMServiceData()) internal val data = _data.asStateFlow() - val isRunning = data.map { it.isRunning() } - val hasBeenDisconnected = data.map { it.hasBeenDisconnected() } + private val _stopEvent = simpleSharedFlow() + internal val stopEvent = _stopEvent.asSharedFlow() + + private val _command = simpleSharedFlow() + internal val command = _command.asSharedFlow() + + val isRunning = data.map { it.connectionState == GattConnectionState.STATE_CONNECTED } + val hasRecords = data.value.records.isNotEmpty() + val highestSequenceNumber = data.value.records.maxOfOrNull { it.sequenceNumber } ?: -1 fun launch(device: ServerDevice) { serviceManager.startService(CGMService::class.java, device) } - fun start(device: ServerDevice, scope: CoroutineScope) { - val createdLogger = loggerFactory.create(stringConst.APP_NAME, "CGMS", device.address).also { - logger = it - } - val manager = CGMManager(context, scope, createdLogger) - this.manager = manager - - manager.dataHolder.status.onEach { - _data.value = it - }.launchIn(scope) - - scope.launch { - manager.start(device) - } + fun onDataReceived(data: List) { + _data.value = _data.value.copy(records = _data.value.records + data) } - private suspend fun CGMManager.start(device: ServerDevice) { -// try { -// connect(device.device) -// .useAutoConnect(false) -// .retry(3, 100) -// .suspend() -// } catch (e: Exception) { -// e.printStackTrace() -// } + internal fun onCommand(command: CGMServiceCommand) { + _command.tryEmit(command) } - fun requestAllRecords() { - manager?.requestAllRecords() + fun onConnectionStateChanged(connectionState: GattConnectionState?) { + _data.value = _data.value.copy(connectionState = connectionState) } - fun requestLastRecord() { - manager?.requestLastRecord() + fun onBatteryLevelChanged(batteryLevel: Int) { + _data.value = _data.value.copy(batteryLevel = batteryLevel) } - fun requestFirstRecord() { - manager?.requestFirstRecord() + fun onNewRequestStatus(requestStatus: RequestStatus) { + _data.value = _data.value.copy(requestStatus = requestStatus) } fun openLogger() { - NordicLogger.launch(context, logger) + TODO() + } + + fun clear() { + _data.value = _data.value.copy(records = emptyList()) } fun release() { - manager?.disconnect()?.enqueue() - logger = null - manager = null + _stopEvent.tryEmit(DisconnectAndStopEvent()) } } 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 9f53dc2d..aa308938 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 @@ -31,33 +31,263 @@ package no.nordicsemi.android.cgms.repository +import android.annotation.SuppressLint import android.content.Intent import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.ble.common.data.cgm.CGMSpecificOpsControlPointData +import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber +import no.nordicsemi.android.cgms.data.CGMServiceCommand import no.nordicsemi.android.kotlin.ble.core.ServerDevice +import no.nordicsemi.android.kotlin.ble.core.client.callback.BleGattClient +import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattCharacteristic +import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattServices +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser +import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMFeatureParser +import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMMeasurementParser +import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMSpecificOpsControlPointParser +import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMStatusParser +import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMErrorCode +import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMOpCode +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.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.kotlin.ble.profile.racp.RACPOpCode +import no.nordicsemi.android.kotlin.ble.profile.racp.RACPResponseCode import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.NotificationService +import java.util.* import javax.inject.Inject +val CGMS_SERVICE_UUID: 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 RACP_UUID = 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") + +@SuppressLint("MissingPermission") @AndroidEntryPoint internal class CGMService : NotificationService() { @Inject lateinit var repository: CGMRepository + private lateinit var client: BleGattClient + + private var secured = false + + private var recordAccessRequestInProgress = false + + private var sessionStartTime: Long = 0 + + private lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) val device = intent!!.getParcelableExtra(DEVICE_DATA)!! - repository.start(device, lifecycleScope) + startGattClient(device) - repository.hasBeenDisconnected.onEach { - if (it) stopSelf() - }.launchIn(lifecycleScope) + repository.stopEvent + .onEach { disconnect() } + .launchIn(lifecycleScope) + + repository.command + .onEach { onCommand(it) } + .launchIn(lifecycleScope) return START_REDELIVER_INTENT } + + private fun onCommand(command: CGMServiceCommand) = lifecycleScope.launch{ + when (command) { + CGMServiceCommand.REQUEST_ALL_RECORDS -> requestAllRecords() + CGMServiceCommand.REQUEST_LAST_RECORD -> requestLastRecord() + CGMServiceCommand.REQUEST_FIRST_RECORD -> requestFirstRecord() + CGMServiceCommand.DISCONNECT -> client.disconnect() + } + } + + private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { + client = device.connect(this@CGMService) + + client.connectionState + .onEach { repository.onConnectionStateChanged(it) } + .filterNotNull() + .onEach { stopIfDisconnected(it) } + .launchIn(lifecycleScope) + + client.services + .filterNotNull() + .onEach { configureGatt(it) } + .launchIn(lifecycleScope) + } + + private suspend fun configureGatt(services: BleGattServices) { + val glsService = services.findService(CGMS_SERVICE_UUID)!! + val statusCharacteristic = glsService.findCharacteristic(CGM_STATUS_UUID)!! + val featureCharacteristic = glsService.findCharacteristic(CGM_FEATURE_UUID)!! + val measurementCharacteristic = glsService.findCharacteristic(CGM_MEASUREMENT_UUID)!! + val opsControlPointCharacteristic = glsService.findCharacteristic(CGM_OPS_CONTROL_POINT_UUID)!! + recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_UUID)!! + 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) } + .launchIn(lifecycleScope) + + measurementCharacteristic.getNotifications() + .mapNotNull { CGMMeasurementParser.parse(it) } + .onEach { + if (sessionStartTime == 0L && !recordAccessRequestInProgress) { + val timeOffset = it.minOf { it.timeOffset } + sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L + } + + val result = it.map { + val timestamp = sessionStartTime + it.timeOffset * 60000L + CGMRecordWithSequenceNumber(it.timeOffset, it, timestamp) + } + + repository.onDataReceived(result) + } + .launchIn(lifecycleScope) + + opsControlPointCharacteristic.getNotifications() + .mapNotNull { CGMSpecificOpsControlPointParser.parse(it) } + .onEach { + if (it.isOperationCompleted) { + if (it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION) { + sessionStartTime = System.currentTimeMillis() + } else { + sessionStartTime = 0 + } + } else { + if (it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION && it.errorCode == CGMErrorCode.CGM_ERROR_PROCEDURE_NOT_COMPLETED) { + sessionStartTime = 0 + } else if (it.requestCode == CGMOpCode.CGM_OP_CODE_STOP_SESSION) { + sessionStartTime = 0 + } + } + } + .launchIn(lifecycleScope) + + recordAccessControlPointCharacteristic.getNotifications() + .mapNotNull { RecordAccessControlPointParser.parse(it) } + .onEach { onAccessControlPointDataReceived(it) } + .launchIn(lifecycleScope) + + val featuresEnvelope = featureCharacteristic.read().let { CGMFeatureParser.parse(it) }!! + secured = featuresEnvelope.features.e2eCrcSupported + + val statusEnvelope = statusCharacteristic.read().let { CGMStatusParser.parse(it) }!! + if (!statusEnvelope.status.sessionStopped) { + sessionStartTime = System.currentTimeMillis() - statusEnvelope.timeOffset * 60000L + } + + if (sessionStartTime == 0L) { + opsControlPointCharacteristic.write(CGMSpecificOpsControlPointData.startSession(secured).value!!) + } + } + + private fun onAccessControlPointDataReceived(data: RecordAccessControlPointData) = lifecycleScope.launch { + when (data) { + 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(requestCode: RACPOpCode) { + val status = when (requestCode) { + RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED + else -> RequestStatus.SUCCESS + } + repository.onNewRequestStatus(status) + } + + private fun onRecordAccessOperationCompletedWithNoRecordsFound() { + repository.onNewRequestStatus(RequestStatus.SUCCESS) + } + + private suspend fun onNumberOfRecordsReceived(numberOfRecords: Int) { + if (numberOfRecords > 0) { + if (repository.hasRecords) { + recordAccessControlPointCharacteristic.write( + RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(repository.highestSequenceNumber).value + ) + } else { + recordAccessControlPointCharacteristic.write( + RecordAccessControlPointInputParser.reportAllStoredRecords().value + ) + } + } + repository.onNewRequestStatus(RequestStatus.SUCCESS) + } + + private fun onRecordAccessOperationError(response: RACPResponseCode) { + if (response == RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED) { + repository.onNewRequestStatus(RequestStatus.NOT_SUPPORTED) + } else { + repository.onNewRequestStatus(RequestStatus.FAILED) + } + } + + private fun clear() { + repository.clear() + } + + private suspend fun requestLastRecord() { + clear() + repository.onNewRequestStatus(RequestStatus.PENDING) + recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportLastStoredRecord().value) + } + + private suspend fun requestFirstRecord() { + clear() + repository.onNewRequestStatus(RequestStatus.PENDING) + recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportFirstStoredRecord().value) + } + + private suspend fun requestAllRecords() { + clear() + repository.onNewRequestStatus(RequestStatus.PENDING) + recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value) + } + + private fun stopIfDisconnected(connectionState: GattConnectionState) { + if (connectionState == GattConnectionState.STATE_DISCONNECTED) { + stopSelf() + } + } + + private fun disconnect() { + client.disconnect() + } } diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt index bdfacfc0..b2449b7f 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt @@ -52,16 +52,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import no.nordicsemi.android.cgms.R -import no.nordicsemi.android.cgms.data.CGMData -import no.nordicsemi.android.cgms.data.CGMRecord +import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber import no.nordicsemi.android.cgms.data.CGMServiceCommand -import no.nordicsemi.android.cgms.data.RequestStatus +import no.nordicsemi.android.cgms.data.CGMServiceData +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 @Composable -internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) { +internal fun CGMContentView(state: CGMServiceData, onEvent: (CGMViewEvent) -> Unit) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally @@ -91,7 +91,7 @@ internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) { } @Composable -private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) { +private fun SettingsView(state: CGMServiceData, onEvent: (CGMViewEvent) -> Unit) { ScreenSection { SectionTitle(icon = Icons.Default.Settings, title = "Request items") @@ -119,7 +119,7 @@ private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) { } @Composable -private fun RecordsView(state: CGMData) { +private fun RecordsView(state: CGMServiceData) { ScreenSection { if (state.records.isEmpty()) { RecordsViewWithoutData() @@ -131,7 +131,7 @@ private fun RecordsView(state: CGMData) { } @Composable -private fun RecordsViewWithData(state: CGMData) { +private fun RecordsViewWithData(state: CGMServiceData) { Column(modifier = Modifier.fillMaxWidth()) { SectionTitle(resId = R.drawable.ic_records, title = "Records") @@ -148,7 +148,7 @@ private fun RecordsViewWithData(state: CGMData) { } @Composable -private fun RecordItem(record: CGMRecord) { +private fun RecordItem(record: CGMRecordWithSequenceNumber) { Row(verticalAlignment = Alignment.CenterVertically) { Column( modifier = Modifier diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt index 94dbf67a..f95c98af 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt @@ -34,16 +34,17 @@ package no.nordicsemi.android.cgms.view import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import no.nordicsemi.android.cgms.R -import no.nordicsemi.android.cgms.data.CGMRecord +import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber +import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMRecord import java.text.SimpleDateFormat import java.util.* -internal fun CGMRecord.formattedTime(): String { +internal fun CGMRecordWithSequenceNumber.formattedTime(): String { val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US) return timeFormat.format(Date(timestamp)) } @Composable -internal fun CGMRecord.glucoseConcentration(): String { - return stringResource(id = R.string.cgms_value_unit, glucoseConcentration) +internal fun CGMRecordWithSequenceNumber.glucoseConcentration(): String { + return stringResource(id = R.string.cgms_value_unit, record.glucoseConcentration) } diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt index 384295e0..8efabf69 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt @@ -48,15 +48,7 @@ import no.nordicsemi.android.cgms.viewmodel.CGMViewModel import no.nordicsemi.android.common.ui.scanner.view.DeviceConnectingView import no.nordicsemi.android.common.ui.scanner.view.DeviceDisconnectedView import no.nordicsemi.android.common.ui.scanner.view.Reason -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,17 +70,15 @@ fun CGMScreen() { .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 -> CGMContentView(state.result.data) { viewModel.onEvent(it) } + if (state.deviceName == null) { + DeviceConnectingView() + } else { + when (state.result?.connectionState) { + null, + GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } + GattConnectionState.STATE_DISCONNECTED, + GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) } + GattConnectionState.STATE_CONNECTED -> CGMContentView(state.result) { viewModel.onEvent(it) } } } } @@ -97,15 +87,11 @@ fun CGMScreen() { @Composable private fun AppBar(state: CGMViewState, navigateUp: () -> Unit, viewModel: CGMViewModel) { - val toolbarName = (state as? WorkingState)?.let { - (it.result as? DeviceHolder)?.deviceName() - } - - if (toolbarName == null) { - BackIconAppBar(stringResource(id = R.string.cgms_title), navigateUp) - } else { - LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) { + if (state.deviceName?.isNotBlank() == true) { + LoggerIconAppBar(state.deviceName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) { viewModel.onEvent(OpenLoggerEvent) } + } else { + BackIconAppBar(stringResource(id = R.string.cgms_title), navigateUp) } } diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt index b53cc4f6..842f35f7 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt @@ -31,10 +31,9 @@ package no.nordicsemi.android.cgms.view -import no.nordicsemi.android.cgms.data.CGMData -import no.nordicsemi.android.service.BleManagerResult +import no.nordicsemi.android.cgms.data.CGMServiceData -internal sealed class CGMViewState - -internal data class WorkingState(val result: BleManagerResult) : CGMViewState() -internal object NoDeviceState : CGMViewState() +internal data class CGMViewState( + val result: CGMServiceData? = null, + val deviceName: String? = null +) diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMViewModel.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMViewModel.kt index afca2922..ac340591 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMViewModel.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMViewModel.kt @@ -44,21 +44,19 @@ 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.cgms.data.CGMS_SERVICE_UUID import no.nordicsemi.android.cgms.data.CGMServiceCommand import no.nordicsemi.android.cgms.repository.CGMRepository +import no.nordicsemi.android.cgms.repository.CGMS_SERVICE_UUID import no.nordicsemi.android.cgms.view.CGMViewEvent import no.nordicsemi.android.cgms.view.CGMViewState import no.nordicsemi.android.cgms.view.DisconnectEvent import no.nordicsemi.android.cgms.view.NavigateUp -import no.nordicsemi.android.cgms.view.NoDeviceState import no.nordicsemi.android.cgms.view.OnWorkingModeSelected import no.nordicsemi.android.cgms.view.OpenLoggerEvent -import no.nordicsemi.android.cgms.view.WorkingState import no.nordicsemi.android.common.navigation.NavigationResult import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.service.ConnectedResult +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId import javax.inject.Inject @@ -69,7 +67,7 @@ internal class CGMViewModel @Inject constructor( private val analytics: AppAnalytics ) : ViewModel() { - private val _state = MutableStateFlow(NoDeviceState) + private val _state = MutableStateFlow(CGMViewState()) val state = _state.asStateFlow() init { @@ -80,9 +78,9 @@ internal class CGMViewModel @Inject constructor( } repository.data.onEach { - _state.value = WorkingState(it) + _state.value = _state.value.copy(result = it) - (it as? ConnectedResult)?.let { + if (it.connectionState == GattConnectionState.STATE_CONNECTED) { analytics.logEvent(ProfileConnectedEvent(Profile.CGMS)) } }.launchIn(viewModelScope) @@ -113,16 +111,10 @@ internal class CGMViewModel @Inject constructor( } private fun onCommandReceived(workingMode: CGMServiceCommand) { - when (workingMode) { - CGMServiceCommand.REQUEST_ALL_RECORDS -> repository.requestAllRecords() - CGMServiceCommand.REQUEST_LAST_RECORD -> repository.requestLastRecord() - CGMServiceCommand.REQUEST_FIRST_RECORD -> repository.requestFirstRecord() - CGMServiceCommand.DISCONNECT -> disconnect() - } + repository.onCommand(workingMode) } private fun disconnect() { repository.release() - navigationManager.navigateUp() } } 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/GLSViewState.kt similarity index 100% rename from profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSState.kt rename to profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSViewState.kt 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 c48be0b8..14feaa78 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 @@ -200,7 +200,7 @@ internal class GLSViewModel @Inject constructor( .onEach { onAccessControlPointDataReceived(it) } .launchIn(viewModelScope) - _state.value = _state.value.copy(deviceName = device.name) + _state.value = _state.value.copy(deviceName = device.name) //prevents UI from appearing before BLE connection is set up } private fun stopIfDisconnected(connectionState: GattConnectionState) {