Improve GLS screen

This commit is contained in:
Sylwester Zieliński
2021-12-28 16:18:39 +01:00
parent b842ff6551
commit a7224cee23
9 changed files with 177 additions and 70 deletions

View File

@@ -9,6 +9,7 @@ dependencies {
implementation libs.chart
implementation libs.nordic.ble.common
implementation libs.nordic.theme
implementation libs.nordic.log

View File

@@ -3,14 +3,13 @@ package no.nordicsemi.android.gls.data
internal data class GLSData(
val records: List<GLSRecord> = 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 {

View File

@@ -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<GLSData> = _data
val data: StateFlow<GLSData> = _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))
}

View File

@@ -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,

View File

@@ -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(

View File

@@ -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
) {
if (state.requestStatus == RequestStatus.PENDING) {
CircularProgressIndicator()
} else {
WorkingMode.values().forEach {
Button(onClick = { onEvent(OnWorkingModeSelected(it)) }) {
Text(it.displayName)
Text(it.toDisplayString())
}
}
}
}
@@ -90,10 +103,44 @@ 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
@@ -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),
))) { }
}

View File

@@ -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())
}

View File

@@ -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()

View File

@@ -2,4 +2,25 @@
<resources>
<string name="gls_title">GLS</string>
<string name="gls_no_records_info">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.</string>
<string name="gls_timestamp">%1$te %1$tb %1$tY at %1$tT</string>
<string name="gls_type_reserved">Reserved for future use</string>
<string name="gls_type_capillary_whole_blood">Capillary Whole blood</string>
<string name="gls_type_capillary_plasma">Capillary Plasma</string>
<string name="gls_type_venous_whole_blood">Venous Whole blood</string>
<string name="gls_type_venous_plasma">Venous Plasma</string>
<string name="gls_type_arterial_whole_blood">Arterial Whole blood</string>
<string name="gls_type_arterial_plasma">Arterial Plasma</string>
<string name="gls_type_undetermined_whole_blood">Undetermined Whole blood</string>
<string name="gls_type_undetermined_plasma">Undetermined Plasma</string>
<string name="gls_type_interstitial_fluid">Interstitial Fluid (ISF)</string>
<string name="gls_type_control_solution">Control Solution</string>
<string name="gls_unit_mgpdl">mg/dl</string>
<string name="gls_unit_mmolpl">mmol/l</string>
<string name="gls__working_mode__all">All</string>
<string name="gls__working_mode__last">Last</string>
<string name="gls__working_mode__first">First</string>
</resources>