Skip to content

Commit

Permalink
Feature heart health (#72)
Browse files Browse the repository at this point in the history
# *Heart Health*



## ⚙️ Release Notes 

**New Heart Health Data Page:**
Consists of 4 subpages:

- Symptoms
- Heart Rate
- Blood Pressure
- Weight

**Each subpage includes:**

- A chart for data visualization
- A table displaying detailed data
- Filtering options to refine data display

**New Repository:**

- A new repository has been created for data retrieval, ensuring
efficient and seamless access to the necessary data for visualization.

## ✅ Testing
as soon as the basic structure has been checked, further tests are added

## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).

---------

Signed-off-by: Basler182 <[email protected]>
Co-authored-by: Eldi Cano <[email protected]>
  • Loading branch information
Basler182 and eldcn authored Aug 8, 2024
1 parent 2732ef5 commit b2e833b
Show file tree
Hide file tree
Showing 63 changed files with 3,702 additions and 286 deletions.
8 changes: 5 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,21 @@ dependencies {
implementation(project(":core:navigation"))
implementation(project(":modules:account"))
implementation(project(":modules:education"))
implementation(project(":modules:healthconnectonfhir"))
implementation(project(":modules:onboarding"))
implementation(project(":modules:measurements"))

implementation(libs.firebase.firestore.ktx)

implementation(libs.androidx.core.i18n)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.splashscreen)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.view.model.ktx)
implementation(libs.androidx.splashscreen)

implementation(libs.hilt.navigation.compose)
implementation(libs.navigation.compose)
implementation(libs.kotlinx.serialization.json)
implementation(libs.navigation.compose)
implementation(libs.vico.compose.m3)

androidTestImplementation(project(":core:testing"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package edu.stanford.bdh.engagehf.health

import androidx.compose.ui.test.junit4.createComposeRule
import androidx.health.connect.client.records.WeightRecord
import androidx.health.connect.client.units.Mass
import edu.stanford.bdh.engagehf.simulator.HealthPageSimulator
import org.junit.Rule
import org.junit.Test
import java.time.ZonedDateTime

class HealthPageTest {

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun `test health page root is displayed`() {
// given
setState(state = getSuccessState())

// then
healthPage {
assertIsDisplayed()
}
}

@Test
fun `test health page error message is displayed`() {
// given
val message = "Error message"

// when
setState(state = HealthUiState.Error(message))

// then
healthPage {
assertErrorMessage(message)
assertCenteredContent()
}
}

@Test
fun `test health page no data message is displayed`() {
// given
val message = "No data available"

// when
setState(state = HealthUiState.NoData(message))

// then
healthPage {
assertNoDataMessage(message)
assertCenteredContent()
}
}

@Test
fun `test health page health chart is displayed`() {
// given
setState(state = getSuccessState())

// then
healthPage {
assertHealthChartIsDisplayed()
}
}

@Test
fun `test health page health header is displayed`() {
// given
setState(state = getSuccessState())
// then
healthPage {
assertHealthHeaderIsDisplayed()
}
}

@Test
fun `test health page health progress indicator is displayed`() {
// given
setState(state = HealthUiState.Loading)
// then
healthPage {
assertHealthProgressIndicatorIsDisplayed()
assertCenteredContent()
}
}

@Test
fun `test health page health history table is displayed`() {
// given
val entryId = "entry-id"

// when
setState(state = getSuccessState(entryId = entryId))

// then
healthPage {
assertHistoryTableItemDisplayed(id = entryId)
}
}

@Test
fun `test health page health history text is displayed`() {
// given
setState(state = getSuccessState())
// then
healthPage {
assertHealthHistoryTextIsDisplayed()
}
}

@Test
fun `test health page health history text is displayed with text`() {
// given
setState(state = getSuccessState())
// then
healthPage {
assertHealthHistoryText("History")
}
}

private fun setState(state: HealthUiState) {
composeTestRule.setContent {
HealthPage(uiState = state, onAction = {})
}
}

private fun healthPage(block: HealthPageSimulator.() -> Unit) {
HealthPageSimulator(composeTestRule).apply(block)
}

private fun getSuccessState(entryId: String? = null): HealthUiState {
return HealthUiState.Success(
data = HealthUiData(
infoRowData = InfoRowData(
selectedTimeRange = TimeRange.MONTHLY,
formattedValue = "70.0 kg",
formattedDate = "Jan 2022",
isSelectedTimeRangeDropdownExpanded = false
),
records = listOf(
WeightRecord(
time = ZonedDateTime.now().toInstant(),
zoneOffset = ZonedDateTime.now().offset,
weight = @Suppress("MagicNumber") Mass.pounds(154.0)
)
),
tableData = listOf(
TableEntryData(
value = 70.0f,
formattedValues = "70.0 kg",
date = ZonedDateTime.now(),
formattedDate = "Jan 2022",
trend = 0f,
formattedTrend = "0.0 kg",
secondValue = null,
id = entryId
)
)
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package edu.stanford.bdh.engagehf.simulator

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.ComposeTestRule
import edu.stanford.bdh.engagehf.health.HealthPageTestIdentifier
import edu.stanford.spezi.core.testing.onNodeWithIdentifier

class HealthPageSimulator(
private val composeTestRule: ComposeTestRule,
) {
private val root = composeTestRule.onNodeWithIdentifier(HealthPageTestIdentifier.ROOT)

private val centeredContent =
composeTestRule.onNodeWithIdentifier(HealthPageTestIdentifier.CENTERED_CONTENT)

private val errorMessage =
composeTestRule.onNodeWithIdentifier(HealthPageTestIdentifier.ERROR_MESSAGE)

private val noDataMessage =
composeTestRule.onNodeWithIdentifier(HealthPageTestIdentifier.NO_DATA_MESSAGE)

private val healthChart =
composeTestRule.onNodeWithIdentifier(HealthPageTestIdentifier.HEALTH_CHART)

private val healthHeader =
composeTestRule.onNodeWithIdentifier(HealthPageTestIdentifier.HEALTH_HEADER)

private val healthProgressIndicator =
composeTestRule.onNodeWithIdentifier(HealthPageTestIdentifier.PROGRESS_INDICATOR)

private val healthHistoryText =
composeTestRule.onNodeWithIdentifier(HealthPageTestIdentifier.HEALTH_HISTORY_TEXT)

fun assertIsDisplayed() {
root.assertIsDisplayed()
}

fun assertErrorMessage(text: String) {
errorMessage.assertIsDisplayed().assertTextEquals(text)
}

fun assertNoDataMessage(text: String) {
noDataMessage.assertIsDisplayed().assertTextEquals(text)
}

fun assertCenteredContent() {
centeredContent.assertIsDisplayed()
}

fun assertHealthChartIsDisplayed() {
healthChart.assertIsDisplayed()
}

fun assertHealthHeaderIsDisplayed() {
healthHeader.assertIsDisplayed()
}

fun assertHealthProgressIndicatorIsDisplayed() {
healthProgressIndicator.assertIsDisplayed()
}

fun assertHistoryTableItemDisplayed(id: String) {
composeTestRule.onNodeWithIdentifier(
HealthPageTestIdentifier.HEALTH_HISTORY_TABLE_ITEM, id)
.assertIsDisplayed()
}

fun assertHealthHistoryTextIsDisplayed() {
healthHistoryText.assertIsDisplayed()
}

fun assertHealthHistoryText(text: String) {
healthHistoryText.assertIsDisplayed().assertTextEquals(text)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import edu.stanford.bdh.engagehf.bluetooth.data.mapper.BluetoothUiStateMapper
import edu.stanford.bdh.engagehf.bluetooth.data.models.Action
import edu.stanford.bdh.engagehf.bluetooth.data.models.BluetoothUiState
import edu.stanford.bdh.engagehf.bluetooth.data.models.UiState
import edu.stanford.bdh.engagehf.bluetooth.measurements.MeasurementsRepository
import edu.stanford.bdh.engagehf.education.EngageEducationRepository
import edu.stanford.bdh.engagehf.messages.MessageRepository
import edu.stanford.bdh.engagehf.messages.MessagesAction
Expand All @@ -17,7 +18,6 @@ import edu.stanford.spezi.core.bluetooth.data.model.BLEServiceState
import edu.stanford.spezi.core.logging.speziLogger
import edu.stanford.spezi.core.navigation.Navigator
import edu.stanford.spezi.modules.education.EducationNavigationEvent
import edu.stanford.spezi.modules.measurements.MeasurementsRepository
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,9 @@ class BottomSheetEvents @Inject constructor(
data object NewMeasurementAction : Event
data object DoNewMeasurement : Event
data object CloseBottomSheet : Event
data object WeightDescriptionBottomSheet : Event
data object AddWeightRecord : Event
data object AddBloodPressureRecord : Event
data object AddHeartRateRecord : Event
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ fun DoNewMeasurementBottomSheet() {
Spacer(modifier = Modifier.width(Spacings.medium))
Icon(
painter = painterResource(id = edu.stanford.spezi.core.design.R.drawable.ic_monitor_weight),
contentDescription = stringResource(R.string.weight_icon_content_description),
contentDescription = stringResource(R.string.info_icon_content_description),
modifier = Modifier
.size(Sizes.Icon.large)
.testIdentifier(DoNewMeasurementBottomSheetTestIdentifier.WEIGHT_ICON),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import edu.stanford.spezi.core.bluetooth.data.model.BLEServiceState
import edu.stanford.spezi.core.bluetooth.data.model.Measurement
import edu.stanford.spezi.core.utils.LocaleProvider
import java.time.format.DateTimeFormatter
import java.util.Locale
import javax.inject.Inject

class BluetoothUiStateMapper @Inject constructor(
Expand All @@ -35,13 +34,17 @@ class BluetoothUiStateMapper @Inject constructor(
fun mapBleServiceState(state: BLEServiceState.Scanning): BluetoothUiState.Ready {
val devices = state.sessions.map {
val summary = when (val lastMeasurement = it.measurements.lastOrNull()) {
is Measurement.BloodPressure -> "Blood Pressure: ${format(lastMeasurement.systolic)} / ${
format(
is Measurement.BloodPressure -> "Blood Pressure: ${
formatSystolicForLocale(
lastMeasurement.systolic
)
} / ${
formatDiastolicForLocale(
lastMeasurement.diastolic
)
}"

is Measurement.Weight -> "Weight: ${format(lastMeasurement.weight)}"
is Measurement.Weight -> "Weight: ${formatWeightForLocale(lastMeasurement.weight)}"
else -> "No measurement received yet"
}
DeviceUiModel(
Expand Down Expand Up @@ -224,8 +227,6 @@ class BluetoothUiStateMapper @Inject constructor(
return String.format(getDefaultLocale(), "%.0f bpm", heartRate)
}

private fun format(value: Number?): String = String.format(Locale.US, "%.2f", value)

private fun getDefaultLocale() = localeProvider.getDefaultLocale()

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package edu.stanford.spezi.modules.measurements
package edu.stanford.bdh.engagehf.bluetooth.measurements

import androidx.health.connect.client.records.BloodPressureRecord
import androidx.health.connect.client.records.HeartRateRecord
Expand Down
Loading

0 comments on commit b2e833b

Please sign in to comment.