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.chart
implementation libs.nordic.ble.common implementation libs.nordic.ble.common
implementation libs.nordic.theme
implementation libs.nordic.log implementation libs.nordic.log

View File

@@ -3,14 +3,13 @@ package no.nordicsemi.android.gls.data
internal data class GLSData( internal data class GLSData(
val records: List<GLSRecord> = emptyList(), val records: List<GLSRecord> = emptyList(),
val batteryLevel: Int = 0, val batteryLevel: Int = 0,
val requestStatus: RequestStatus = RequestStatus.IDLE, val requestStatus: RequestStatus = RequestStatus.IDLE
val selectedMode: WorkingMode = WorkingMode.ALL
) )
internal enum class WorkingMode(val displayName: String) { internal enum class WorkingMode {
ALL("All"), ALL,
LAST("First"), LAST,
FIRST("Last") FIRST
} }
internal enum class RequestStatus { 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -9,7 +10,7 @@ import javax.inject.Singleton
internal class GLSDataHolder @Inject constructor() { internal class GLSDataHolder @Inject constructor() {
private val _data = MutableStateFlow(GLSData()) private val _data = MutableStateFlow(GLSData())
val data: StateFlow<GLSData> = _data val data: StateFlow<GLSData> = _data.asStateFlow()
fun addNewRecord(record: GLSRecord) { fun addNewRecord(record: GLSRecord) {
val newRecords = _data.value.records.toMutableList().apply { val newRecords = _data.value.records.toMutableList().apply {
@@ -35,10 +36,6 @@ internal class GLSDataHolder @Inject constructor() {
_data.tryEmit(_data.value.copy(records = emptyList())) _data.tryEmit(_data.value.copy(records = emptyList()))
} }
fun setNewWorkingMode(workingMode: WorkingMode) {
_data.tryEmit(_data.value.copy(selectedMode = workingMode))
}
fun setNewBatteryLevel(batteryLevel: Int) { fun setNewBatteryLevel(batteryLevel: Int) {
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) _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] */ /** Concentration unit. One of the following: [ConcentrationUnit.UNIT_KGPL], [ConcentrationUnit.UNIT_MOLPL] */
val unit: ConcentrationUnit = ConcentrationUnit.UNIT_KGPL, val unit: ConcentrationUnit = ConcentrationUnit.UNIT_KGPL,
/** The type of the record. 0 if not present */ val type: RecordType?,
val type: Int = 0,
/** The sample location. 0 if unknown */ /** The sample location. 0 if unknown */
val sampleLocation: Int = 0, val sampleLocation: Int = 0,
@@ -48,6 +47,30 @@ internal data class GLSRecord(
var context: MeasurementContext? = 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( internal data class MeasurementContext(
/** Record sequence number */ /** Record sequence number */
val sequenceNumber: Int = 0, 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.RACPErrorCode
import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPOpCode 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.GlucoseMeasurementCallback.GlucoseStatus
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Carbohydrate import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.*
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.data.Data 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.CarbohydrateId
import no.nordicsemi.android.gls.data.ConcentrationUnit 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.HealthStatus
import no.nordicsemi.android.gls.data.MeasurementContext
import no.nordicsemi.android.gls.data.MedicationId import no.nordicsemi.android.gls.data.MedicationId
import no.nordicsemi.android.gls.data.MedicationUnit 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.TestType
import no.nordicsemi.android.gls.data.TypeOfMeal import no.nordicsemi.android.gls.data.TypeOfMeal
import no.nordicsemi.android.log.LogContract import no.nordicsemi.android.log.LogContract
@@ -137,7 +130,7 @@ internal class GLSManager @Inject constructor(
glucoseConcentration = glucoseConcentration ?: 0f, glucoseConcentration = glucoseConcentration ?: 0f,
unit = unit?.let { ConcentrationUnit.create(it) } unit = unit?.let { ConcentrationUnit.create(it) }
?: ConcentrationUnit.UNIT_KGPL, ?: ConcentrationUnit.UNIT_KGPL,
type = type ?: 0, type = RecordType.createOrNull(type),
sampleLocation = sampleLocation ?: 0, sampleLocation = sampleLocation ?: 0,
status = status?.value ?: 0 status = status?.value ?: 0
) )
@@ -213,8 +206,6 @@ internal class GLSManager @Inject constructor(
device: BluetoothDevice, device: BluetoothDevice,
numberOfRecords: Int numberOfRecords: Int
) { ) {
//TODO("Probably not needed")
// mCallbacks!!.onNumberOfRecordsRequested(device, numberOfRecords)
if (numberOfRecords > 0) { if (numberOfRecords > 0) {
if (dataHolder.records().isNotEmpty()) { if (dataHolder.records().isNotEmpty()) {
val sequenceNumber = dataHolder.records().last().sequenceNumber + 1 //TODO check if correct val sequenceNumber = dataHolder.records().last().sequenceNumber + 1 //TODO check if correct
@@ -232,9 +223,8 @@ internal class GLSManager @Inject constructor(
) )
.enqueue() .enqueue()
} }
} else {
dataHolder.setRequestStatus(RequestStatus.SUCCESS)
} }
dataHolder.setRequestStatus(RequestStatus.SUCCESS)
} }
override fun onRecordAccessOperationError( override fun onRecordAccessOperationError(

View File

@@ -1,24 +1,28 @@
package no.nordicsemi.android.gls.view package no.nordicsemi.android.gls.view
import androidx.compose.foundation.layout.* 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.Icons
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.data.GLSData import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.gls.data.GLSRecord 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.data.WorkingMode
import no.nordicsemi.android.gls.viewmodel.DisconnectEvent import no.nordicsemi.android.gls.viewmodel.DisconnectEvent
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
import no.nordicsemi.android.gls.viewmodel.OnWorkingModeSelected 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.BatteryLevelView
import no.nordicsemi.android.theme.view.ScreenSection import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle import no.nordicsemi.android.theme.view.SectionTitle
@@ -26,7 +30,10 @@ import no.nordicsemi.android.theme.view.SectionTitle
@Composable @Composable
internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) { internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.height(16.dp)) 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)) 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(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
if (state.requestStatus == RequestStatus.PENDING) {
CircularProgressIndicator()
} else {
WorkingMode.values().forEach { WorkingMode.values().forEach {
Button(onClick = { onEvent(OnWorkingModeSelected(it)) }) { 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)) Spacer(modifier = Modifier.height(16.dp))
state.records.forEach { state.records.forEachIndexed { i, it ->
Text(text = String.format("Glucose concentration: %.2d", it.glucoseConcentration)) 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 @Composable
@@ -106,22 +153,9 @@ private fun RecordsViewWithoutData() {
Spacer(modifier = Modifier.height(16.dp)) 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 package no.nordicsemi.android.gls.viewmodel
import dagger.hilt.android.lifecycle.HiltViewModel 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.GLSDataHolder
import no.nordicsemi.android.gls.data.WorkingMode import no.nordicsemi.android.gls.data.WorkingMode
import no.nordicsemi.android.gls.repository.GLSManager import no.nordicsemi.android.gls.repository.GLSManager
@@ -20,26 +17,11 @@ internal class GLSViewModel @Inject constructor(
) : CloseableViewModel() { ) : CloseableViewModel() {
val state = dataHolder.data 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) { fun onEvent(event: GLSScreenViewEvent) {
when (event) { when (event) {
DisconnectEvent -> disconnect() DisconnectEvent -> disconnect()
is OnWorkingModeSelected -> dataHolder.setNewWorkingMode(event.workingMode) is OnWorkingModeSelected -> requestData(event.workingMode)
}.exhaustive }.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() { private fun disconnect() {
finish() finish()
deviceHolder.forgetDevice() deviceHolder.forgetDevice()

View File

@@ -2,4 +2,25 @@
<resources> <resources>
<string name="gls_title">GLS</string> <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_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> </resources>