mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-21 16:34:23 +01:00
Improve GLS screen
This commit is contained in:
@@ -9,6 +9,7 @@ dependencies {
|
||||
implementation libs.chart
|
||||
|
||||
implementation libs.nordic.ble.common
|
||||
implementation libs.nordic.theme
|
||||
|
||||
implementation libs.nordic.log
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
))) { }
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user