From a7224cee2323499a72a7c3ed9ef3f431d0d1e76c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylwester=20Zieli=C5=84ski?= Date: Tue, 28 Dec 2021 16:18:39 +0100 Subject: [PATCH] Improve GLS screen --- profile_gls/build.gradle | 1 + .../no/nordicsemi/android/gls/data/GLSData.kt | 11 ++- .../android/gls/data/GLSDataHolder.kt | 7 +- .../nordicsemi/android/gls/data/GLSRecord.kt | 27 +++++- .../android/gls/repository/GLSManager.kt | 18 +--- .../android/gls/view/GLSContentView.kt | 82 +++++++++++++------ .../nordicsemi/android/gls/view/GLSMapper.kt | 52 ++++++++++++ .../android/gls/viewmodel/GLSViewModel.kt | 28 ++----- profile_gls/src/main/res/values/strings.xml | 21 +++++ 9 files changed, 177 insertions(+), 70 deletions(-) create mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSMapper.kt diff --git a/profile_gls/build.gradle b/profile_gls/build.gradle index 662e3f13..16b24950 100644 --- a/profile_gls/build.gradle +++ b/profile_gls/build.gradle @@ -9,6 +9,7 @@ dependencies { implementation libs.chart implementation libs.nordic.ble.common + implementation libs.nordic.theme implementation libs.nordic.log diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt index 3d56c71f..4c7a06d1 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt @@ -3,14 +3,13 @@ package no.nordicsemi.android.gls.data internal data class GLSData( val records: List = emptyList(), val batteryLevel: Int = 0, - val requestStatus: RequestStatus = RequestStatus.IDLE, - val selectedMode: WorkingMode = WorkingMode.ALL + val requestStatus: RequestStatus = RequestStatus.IDLE ) -internal enum class WorkingMode(val displayName: String) { - ALL("All"), - LAST("First"), - FIRST("Last") +internal enum class WorkingMode { + ALL, + LAST, + FIRST } internal enum class RequestStatus { diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSDataHolder.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSDataHolder.kt index a0c489c0..d0b5b1c6 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSDataHolder.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSDataHolder.kt @@ -2,6 +2,7 @@ package no.nordicsemi.android.gls.data import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject import javax.inject.Singleton @@ -9,7 +10,7 @@ import javax.inject.Singleton internal class GLSDataHolder @Inject constructor() { private val _data = MutableStateFlow(GLSData()) - val data: StateFlow = _data + val data: StateFlow = _data.asStateFlow() fun addNewRecord(record: GLSRecord) { val newRecords = _data.value.records.toMutableList().apply { @@ -35,10 +36,6 @@ internal class GLSDataHolder @Inject constructor() { _data.tryEmit(_data.value.copy(records = emptyList())) } - fun setNewWorkingMode(workingMode: WorkingMode) { - _data.tryEmit(_data.value.copy(selectedMode = workingMode)) - } - fun setNewBatteryLevel(batteryLevel: Int) { _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) } 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 index c4ed474e..de4e68b9 100644 --- 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 @@ -36,8 +36,7 @@ internal data class GLSRecord( /** Concentration unit. One of the following: [ConcentrationUnit.UNIT_KGPL], [ConcentrationUnit.UNIT_MOLPL] */ val unit: ConcentrationUnit = ConcentrationUnit.UNIT_KGPL, - /** The type of the record. 0 if not present */ - val type: Int = 0, + val type: RecordType?, /** The sample location. 0 if unknown */ val sampleLocation: Int = 0, @@ -48,6 +47,30 @@ internal data class GLSRecord( 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( /** Record sequence number */ val sequenceNumber: Int = 0, diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt index 4387d4dd..f127e293 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt @@ -35,21 +35,14 @@ import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPErrorCode import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPOpCode import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementCallback.GlucoseStatus -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.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.ble.common.profile.glucose.GlucoseMeasurementContextCallback.* import no.nordicsemi.android.ble.data.Data +import no.nordicsemi.android.gls.data.* import no.nordicsemi.android.gls.data.CarbohydrateId import no.nordicsemi.android.gls.data.ConcentrationUnit -import no.nordicsemi.android.gls.data.GLSDataHolder -import no.nordicsemi.android.gls.data.GLSRecord import no.nordicsemi.android.gls.data.HealthStatus -import no.nordicsemi.android.gls.data.MeasurementContext import no.nordicsemi.android.gls.data.MedicationId import no.nordicsemi.android.gls.data.MedicationUnit -import no.nordicsemi.android.gls.data.RequestStatus import no.nordicsemi.android.gls.data.TestType import no.nordicsemi.android.gls.data.TypeOfMeal import no.nordicsemi.android.log.LogContract @@ -137,7 +130,7 @@ internal class GLSManager @Inject constructor( glucoseConcentration = glucoseConcentration ?: 0f, unit = unit?.let { ConcentrationUnit.create(it) } ?: ConcentrationUnit.UNIT_KGPL, - type = type ?: 0, + type = RecordType.createOrNull(type), sampleLocation = sampleLocation ?: 0, status = status?.value ?: 0 ) @@ -213,8 +206,6 @@ internal class GLSManager @Inject constructor( device: BluetoothDevice, numberOfRecords: Int ) { - //TODO("Probably not needed") -// mCallbacks!!.onNumberOfRecordsRequested(device, numberOfRecords) if (numberOfRecords > 0) { if (dataHolder.records().isNotEmpty()) { val sequenceNumber = dataHolder.records().last().sequenceNumber + 1 //TODO check if correct @@ -232,9 +223,8 @@ internal class GLSManager @Inject constructor( ) .enqueue() } - } else { - dataHolder.setRequestStatus(RequestStatus.SUCCESS) } + dataHolder.setRequestStatus(RequestStatus.SUCCESS) } override fun onRecordAccessOperationError( diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt index d05560da..c2cae599 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt @@ -1,24 +1,28 @@ package no.nordicsemi.android.gls.view import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.data.GLSData 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.viewmodel.DisconnectEvent import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent import no.nordicsemi.android.gls.viewmodel.OnWorkingModeSelected +import no.nordicsemi.android.material.you.CircularProgressIndicator import no.nordicsemi.android.theme.view.BatteryLevelView import no.nordicsemi.android.theme.view.ScreenSection import no.nordicsemi.android.theme.view.SectionTitle @@ -26,7 +30,10 @@ import no.nordicsemi.android.theme.view.SectionTitle @Composable internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) { Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.height(16.dp)) @@ -48,6 +55,8 @@ internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Uni ) { Text(text = stringResource(id = R.string.disconnect)) } + + Spacer(modifier = Modifier.height(16.dp)) } } @@ -62,9 +71,13 @@ private fun SettingsView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { - WorkingMode.values().forEach { - Button(onClick = { onEvent(OnWorkingModeSelected(it)) }) { - Text(it.displayName) + if (state.requestStatus == RequestStatus.PENDING) { + CircularProgressIndicator() + } else { + WorkingMode.values().forEach { + Button(onClick = { onEvent(OnWorkingModeSelected(it)) }) { + Text(it.toDisplayString()) + } } } } @@ -90,12 +103,46 @@ private fun RecordsViewWithData(state: GLSData) { Spacer(modifier = Modifier.height(16.dp)) - state.records.forEach { - Text(text = String.format("Glucose concentration: %.2d", it.glucoseConcentration)) + state.records.forEachIndexed { i, it -> + RecordItem(it) + + if (i < state.records.size-1) { + Spacer(modifier = Modifier.padding(8.dp)) + } } } } +@Composable +private fun RecordItem(record: GLSRecord) { + Row(verticalAlignment = Alignment.CenterVertically) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + record.time?.let { + Text( + text = stringResource(R.string.gls_timestamp, it), + style = MaterialTheme.typography.labelLarge + ) + } + + Text( + text = record.type.toDisplayString(), + style = MaterialTheme.typography.bodyMedium + ) + } + + Spacer(modifier = Modifier.padding(16.dp)) + + Text( + text = glucoseConcentrationDisplayValue(record.glucoseConcentration, record.unit), + style = MaterialTheme.typography.titleMedium, + ) + } +} + @Composable private fun RecordsViewWithoutData() { Column( @@ -106,22 +153,9 @@ private fun RecordsViewWithoutData() { Spacer(modifier = Modifier.height(16.dp)) - Text(text = stringResource(id = R.string.gls_no_records_info)) + Text( + text = stringResource(id = R.string.gls_no_records_info), + style = MaterialTheme.typography.bodyMedium + ) } } - -@Preview -@Composable -private fun GLSContentView_NoData_Preview() { - GLSContentView(GLSData()) { } -} - -@Preview -@Composable -private fun GLSContentView_WithData_Preview() { - GLSContentView(GLSData(records = listOf( - GLSRecord(glucoseConcentration = 10f), - GLSRecord(glucoseConcentration = 15f), - GLSRecord(glucoseConcentration = 20f), - ))) { } -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSMapper.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSMapper.kt new file mode 100644 index 00000000..e159dba5 --- /dev/null +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSMapper.kt @@ -0,0 +1,52 @@ +package no.nordicsemi.android.gls.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 + +@Composable +internal fun RecordType?.toDisplayString(): String { + return when (this) { + RecordType.CAPILLARY_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_capillary_whole_blood) + RecordType.CAPILLARY_PLASMA -> stringResource(id = R.string.gls_type_capillary_plasma) + RecordType.VENOUS_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_venous_whole_blood) + RecordType.VENOUS_PLASMA -> stringResource(id = R.string.gls_type_venous_plasma) + RecordType.ARTERIAL_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_arterial_whole_blood) + RecordType.ARTERIAL_PLASMA -> stringResource(id = R.string.gls_type_arterial_plasma) + RecordType.UNDETERMINED_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_undetermined_whole_blood) + RecordType.UNDETERMINED_PLASMA -> stringResource(id = R.string.gls_type_undetermined_plasma) + RecordType.INTERSTITIAL_FLUID -> stringResource(id = R.string.gls_type_interstitial_fluid) + RecordType.CONTROL_SOLUTION -> stringResource(id = R.string.gls_type_control_solution) + null -> stringResource(id = R.string.gls_type_reserved) + } +} + +@Composable +internal fun ConcentrationUnit.toDisplayString(): String { + return when (this) { + //TODO("Check unit_kgpl --> mgpdl") + ConcentrationUnit.UNIT_KGPL -> stringResource(id = R.string.gls_unit_mgpdl) + ConcentrationUnit.UNIT_MOLPL -> stringResource(id = R.string.gls_unit_mmolpl) + } +} + +@Composable +internal fun WorkingMode.toDisplayString(): String { + return when (this) { + WorkingMode.ALL -> stringResource(id = R.string.gls__working_mode__all) + WorkingMode.LAST -> stringResource(id = R.string.gls__working_mode__last) + WorkingMode.FIRST -> stringResource(id = R.string.gls__working_mode__first) + } +} + +@Composable +internal fun glucoseConcentrationDisplayValue(value: Float, unit: ConcentrationUnit): String { + val result = when (unit) { + ConcentrationUnit.UNIT_KGPL -> value * 100000.0f + ConcentrationUnit.UNIT_MOLPL -> value * 1000.0f + } + return String.format("%.2f %s", result, unit.toDisplayString()) +} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt index f8a5364c..b16b0c95 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt @@ -1,9 +1,6 @@ package no.nordicsemi.android.gls.viewmodel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.gls.data.GLSDataHolder import no.nordicsemi.android.gls.data.WorkingMode import no.nordicsemi.android.gls.repository.GLSManager @@ -20,26 +17,11 @@ internal class GLSViewModel @Inject constructor( ) : CloseableViewModel() { val state = dataHolder.data - private var lastSelectedMode = state.value.selectedMode - - init { - dataHolder.data.onEach { - if (lastSelectedMode == it.selectedMode) { - return@onEach - } - lastSelectedMode = it.selectedMode - when (it.selectedMode) { - WorkingMode.ALL -> glsManager.requestAllRecords() - WorkingMode.LAST -> glsManager.requestLastRecord() - WorkingMode.FIRST -> glsManager.requestFirstRecord() - }.exhaustive - }.launchIn(GlobalScope) - } fun onEvent(event: GLSScreenViewEvent) { when (event) { DisconnectEvent -> disconnect() - is OnWorkingModeSelected -> dataHolder.setNewWorkingMode(event.workingMode) + is OnWorkingModeSelected -> requestData(event.workingMode) }.exhaustive } @@ -52,6 +34,14 @@ internal class GLSViewModel @Inject constructor( } } + private fun requestData(mode: WorkingMode) { + when (mode) { + WorkingMode.ALL -> glsManager.requestAllRecords() + WorkingMode.LAST -> glsManager.requestLastRecord() + WorkingMode.FIRST -> glsManager.requestFirstRecord() + }.exhaustive + } + private fun disconnect() { finish() deviceHolder.forgetDevice() diff --git a/profile_gls/src/main/res/values/strings.xml b/profile_gls/src/main/res/values/strings.xml index a7a5aafe..3971fc26 100644 --- a/profile_gls/src/main/res/values/strings.xml +++ b/profile_gls/src/main/res/values/strings.xml @@ -2,4 +2,25 @@ GLS There is no data available. This peripheral downloads data only when requested. Please select what kind of data you want to download by clicking one from the above buttons. + + %1$te %1$tb %1$tY at %1$tT + + Reserved for future use + Capillary Whole blood + Capillary Plasma + Venous Whole blood + Venous Plasma + Arterial Whole blood + Arterial Plasma + Undetermined Whole blood + Undetermined Plasma + Interstitial Fluid (ISF) + Control Solution + + mg/dl + mmol/l + + All + Last + First