Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Health Connect to FHIR converter module #15

Merged
merged 44 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8583dbc
Create Health Connect to FHIR converter module
vishnuravi May 22, 2024
b5d64c4
Update tests
vishnuravi May 22, 2024
e892419
Use build convention plugins
vishnuravi May 22, 2024
a6f1dbf
Move to modules folder and remove proguard rules
vishnuravi May 22, 2024
ea25dfc
Reorganize module files
vishnuravi May 22, 2024
43a9688
Support multiple codings and categories per observation
vishnuravi May 23, 2024
77b05b4
Add additional unit tests
vishnuravi May 23, 2024
9896b62
Update memory settings
vishnuravi May 23, 2024
5e24882
Improve observation creation
vishnuravi May 23, 2024
8c29c82
Increase heap size
vishnuravi May 23, 2024
aa13705
Update tests and dependencies
vishnuravi May 26, 2024
e5f2b78
Clean up manifest
vishnuravi May 26, 2024
3320580
Extract dependencies to version catalog
vishnuravi May 27, 2024
d277d90
Refactor tests to use truth library
vishnuravi May 27, 2024
c3f5871
Merge branch 'main' into healthconnectonfhir
vishnuravi Jun 3, 2024
7764a65
Create record to observation mapper
vishnuravi Jun 11, 2024
562ba6f
Merge branch 'main' into healthconnectonfhir
vishnuravi Jun 11, 2024
b55608f
Add heart rate mapping
vishnuravi Jun 11, 2024
6612e8d
Fix detekt errors
vishnuravi Jun 11, 2024
99983f8
Switch fhir dependencies
vishnuravi Jun 11, 2024
48fcdac
Update tests
vishnuravi Jun 11, 2024
fe14a1e
Remove unused import
vishnuravi Jun 11, 2024
de1302e
Fix dependency conflict
vishnuravi Jun 11, 2024
34025e9
Reduce heap size to default
vishnuravi Jun 12, 2024
8ad3d1f
Delete .DS_Store
PSchmiedmayer Jun 12, 2024
066e1ba
Alphabetize mapper functions
vishnuravi Jun 12, 2024
b914d9b
Add BodyFatRecord
vishnuravi Jun 12, 2024
e8e973c
Add OxygenSaturationRecord
vishnuravi Jun 12, 2024
4f65b0b
Add RespiratoryRateRecord
vishnuravi Jun 12, 2024
be80bce
Use effectiveDateTime instead of effectivePeriod if start and end tim…
vishnuravi Jun 13, 2024
0d2c49b
Add unique identifier from Record metadata to Observation
vishnuravi Jun 13, 2024
8c7e226
Adds issued time to observation to match HealthKitOnFHIR
vishnuravi Jun 13, 2024
178acbc
Fix detekt warning
vishnuravi Jun 13, 2024
ed133a0
Update identifier json structure to match HealthKitOnFHIR
vishnuravi Jun 13, 2024
a246c43
Switch to UCUM units and sync with HealthKitOnFHIR
vishnuravi Jun 13, 2024
66008f1
Fix detekt issues
vishnuravi Jun 13, 2024
3d9f12e
Use SI units
vishnuravi Jun 16, 2024
683594c
Add blood glucose
vishnuravi Jun 16, 2024
1c7a6c7
Update tests
vishnuravi Jun 16, 2024
1196b43
Add KDocs and improve test coverage
vishnuravi Jun 17, 2024
db9eb16
Add README
vishnuravi Jun 17, 2024
b86d51e
Update README
vishnuravi Jun 17, 2024
3a3c682
Fix dokka error
vishnuravi Jun 17, 2024
4217b3b
Update README
vishnuravi Jun 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
vishnuravi marked this conversation as resolved.
Show resolved Hide resolved
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ detekt = "1.23.6" # please adjust github action version as well in case of versi
credentialsPlayServicesAuth = "1.2.2"
dokka = "1.9.20"
espressoCore = "3.5.1"
fhirDataCapture = "1.0.0"
firebaseAuthKtx = "23.0.0"
firebaseFirestoreKtx = "25.0.0"
firebaseFunctionsKtx = "21.0.0"
firebaseStorageKtx = "21.0.0"
foundation = "1.6.7"
googleGmsGoogleServices = "4.4.2"
googleid = "1.1.0"
healthConnectClient = "1.1.0-alpha02"
hiltNavigation = "1.2.0"
hiltVersion = "2.51"
junit = "4.13.2"
Expand All @@ -46,10 +48,12 @@ android-gradle = { group = "com.android.tools.build", name = "gradle", version.r
android-tools-common = { group = "com.android.tools", name = "common", version.ref = "androidTools" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-health-connect-client = { module = "androidx.health.connect:connect-client", version.ref = "healthConnectClient" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "coreTestingVersion" }
androidx-credentials-play-services-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "credentialsPlayServicesAuth" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
android-fhir-data-capture = { module = "com.google.android.fhir:data-capture", version.ref = "fhirDataCapture" }
androidx-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleKtx" }
Expand Down
7 changes: 0 additions & 7 deletions modules/contact/src/androidTest/AndroidManifest.xml

This file was deleted.

4 changes: 1 addition & 3 deletions modules/contact/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>

</manifest>
<manifest />
1 change: 1 addition & 0 deletions modules/healthconnectonfhir/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
13 changes: 13 additions & 0 deletions modules/healthconnectonfhir/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
alias(libs.plugins.spezi.library)
alias(libs.plugins.spezi.hilt)
}

android {
namespace = "edu.stanford.spezi.modules.healthconnectonfhir"
}

dependencies {
implementation(libs.androidx.health.connect.client)
implementation(libs.android.fhir.data.capture)
}
4 changes: 4 additions & 0 deletions modules/healthconnectonfhir/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
vishnuravi marked this conversation as resolved.
Show resolved Hide resolved
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package edu.stanford.healthconnectonfhir

import androidx.health.connect.client.records.Record
import org.hl7.fhir.r4.model.Observation

interface RecordToObservationMapper {
/**
* Maps a given Health Connect record to a list of HL7 FHIR Observations
*
* @param T the type of the Health Connect record, extending from `Record`
* @param record the record to be mapped
* @return a list of `Observation` objects derived from the provided health record
*/
fun <T : Record> map(record: T): List<Observation>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package edu.stanford.healthconnectonfhir

import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
import androidx.health.connect.client.records.BloodPressureRecord
import androidx.health.connect.client.records.BodyTemperatureRecord
import androidx.health.connect.client.records.HeartRateRecord
import androidx.health.connect.client.records.HeightRecord
import androidx.health.connect.client.records.Record
import androidx.health.connect.client.records.StepsRecord
import androidx.health.connect.client.records.WeightRecord
import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Observation
import org.hl7.fhir.r4.model.Period
import org.hl7.fhir.r4.model.Quantity
import java.util.Date

class RecordToObservationMapperImpl : RecordToObservationMapper {
vishnuravi marked this conversation as resolved.
Show resolved Hide resolved
/**
* Maps a given Health Connect record to a list of HL7 FHIR Observations.
*
* @param T the type of the health record, extending from `Record`
* @param record the health record to be mapped
* @return a list of `Observation` objects derived from the provided health record
*/
override fun <T : Record> map(record: T): List<Observation> {
return when (record) {
is StepsRecord -> listOf(mapStepsRecord(record))
is WeightRecord -> listOf(mapWeightRecord(record))
is HeightRecord -> listOf(mapHeightRecord(record))
is BodyTemperatureRecord -> listOf(mapBodyTemperatureRecord(record))
is BloodPressureRecord -> listOf(mapBloodPressureRecord(record))
is ActiveCaloriesBurnedRecord -> listOf(mapActiveCaloriesBurnedRecord(record))
is HeartRateRecord -> mapHeartRateRecord(record)
else -> error("Unsupported record type ${record.javaClass.name}")

Check warning on line 35 in modules/healthconnectonfhir/src/main/java/edu/stanford/healthconnectonfhir/RecordToObservationMapperImpl.kt

View check run for this annotation

Codecov / codecov/patch

modules/healthconnectonfhir/src/main/java/edu/stanford/healthconnectonfhir/RecordToObservationMapperImpl.kt#L35

Added line #L35 was not covered by tests
}
}

private fun mapStepsRecord(record: StepsRecord) = record.createObservation(
categories = listOf(
Coding()
.setSystem("http://terminology.hl7.org/CodeSystem/observation-category")
.setCode("activity")
.setDisplay("Activity")
),
codings = listOf(
Coding()
.setSystem("http://loinc.org")
.setCode("55423-8")
.setDisplay("Number of steps")
),
unit = "steps",
valueExtractor = { count.toDouble() },
periodExtractor = { Date.from(startTime) to Date.from(endTime) }
)

private fun mapWeightRecord(record: WeightRecord) = record.createObservation(
categories = listOf(
Coding()
.setSystem("http://terminology.hl7.org/CodeSystem/observation-category")
.setCode("vital-signs")
.setDisplay("Vital Signs")
),
codings = listOf(
Coding()
.setSystem("http://loinc.org")
.setCode("29463-7")
.setDisplay("Body weight")
),
unit = "g",
valueExtractor = { weight.inGrams },
periodExtractor = { Date.from(time) to Date.from(time) }
)

private fun mapHeightRecord(record: HeightRecord) = record.createObservation(
categories = listOf(
Coding()
.setSystem("http://terminology.hl7.org/CodeSystem/observation-category")
.setCode("vital-signs")
.setDisplay("Vital Signs")
),
codings = listOf(
Coding().setSystem("http://loinc.org").setCode("8302-2").setDisplay("Body height")
),
unit = "m",
valueExtractor = { height.inMeters },
periodExtractor = { Date.from(time) to Date.from(time) }
)

private fun mapActiveCaloriesBurnedRecord(record: ActiveCaloriesBurnedRecord) = record.createObservation(
categories = listOf(
Coding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("activity").setDisplay("Activity")
),
codings = listOf(
Coding().setSystem("http://loinc.org").setCode("41981-2").setDisplay("Calories burned")
),
unit = "kcal",
valueExtractor = { energy.inCalories },
periodExtractor = { Date.from(startTime) to Date.from(endTime) }
)

private fun mapBodyTemperatureRecord(record: BodyTemperatureRecord) = record.createObservation(
categories = listOf(
Coding()
.setSystem("http://terminology.hl7.org/CodeSystem/observation-category")
.setCode("vital-signs")
.setDisplay("Vital Signs")
),
codings = listOf(
Coding().setSystem("http://loinc.org").setCode("8310-5").setDisplay("Body temperature")
),
unit = "°C",
valueExtractor = { temperature.inCelsius },
periodExtractor = { Date.from(time) to Date.from(time) }
)

private fun mapBloodPressureRecord(record: BloodPressureRecord): Observation {
val observation = Observation()
observation.status = Observation.ObservationStatus.FINAL

observation.category = listOf(
CodeableConcept().addCoding(
Coding()
.setSystem("http://terminology.hl7.org/CodeSystem/observation-category")
.setCode("vital-signs")
.setDisplay("Vital Signs")
)
)

observation.code = CodeableConcept().addCoding(
Coding()
.setSystem("http://loinc.org")
.setCode("85354-9")
.setDisplay("Blood pressure panel with all children optional")
)

val period = Period()
period.start = Date.from(record.time)
period.end = Date.from(record.time)
observation.effective = period

val systolicComponent = Observation.ObservationComponentComponent()
systolicComponent.code = CodeableConcept().addCoding(
Coding()
.setSystem("http://loinc.org")
.setCode("8480-6")
.setDisplay("Systolic blood pressure")
)
systolicComponent.value = Quantity().setValue(record.systolic.inMillimetersOfMercury).setUnit("mmHg")

val diastolicComponent = Observation.ObservationComponentComponent()
diastolicComponent.code = CodeableConcept().addCoding(
Coding()
.setSystem("http://loinc.org")
.setCode("8462-4")
.setDisplay("Diastolic blood pressure")
)
diastolicComponent.value = Quantity().setValue(record.diastolic.inMillimetersOfMercury).setUnit("mmHg")

observation.addComponent(systolicComponent)
observation.addComponent(diastolicComponent)

return observation
}

private fun mapHeartRateRecord(record: HeartRateRecord): List<Observation> {
return record.samples.map { sample ->
val observation = Observation()
observation.status = Observation.ObservationStatus.FINAL

observation.category = listOf(
CodeableConcept().addCoding(
Coding()
.setSystem("http://terminology.hl7.org/CodeSystem/observation-category")
.setCode("vital-signs")
.setDisplay("Vital Signs")
)
)

observation.code = CodeableConcept().addCoding(
Coding()
.setSystem("http://loinc.org")
.setCode("8867-4")
.setDisplay("Heart rate")
)

val period = Period()
period.start = Date.from(sample.time)
period.end = Date.from(sample.time)
observation.effective = period

observation.value = Quantity().setValue(sample.beatsPerMinute).setUnit("beats/minute")

observation
}
}

private fun <T : Record> T.createObservation(
categories: List<Coding> = listOf(),

Check warning on line 199 in modules/healthconnectonfhir/src/main/java/edu/stanford/healthconnectonfhir/RecordToObservationMapperImpl.kt

View check run for this annotation

Codecov / codecov/patch

modules/healthconnectonfhir/src/main/java/edu/stanford/healthconnectonfhir/RecordToObservationMapperImpl.kt#L198-L199

Added lines #L198 - L199 were not covered by tests
codings: List<Coding>,
unit: String,
valueExtractor: T.() -> Double,
periodExtractor: T.() -> Pair<Date, Date>,
): Observation {
return Observation().apply {
status = Observation.ObservationStatus.FINAL

category = listOf(CodeableConcept().apply {
categories.forEach { addCoding(it) }
})

code = CodeableConcept().apply {
codings.forEach { addCoding(it) }
}

effective = Period().apply {
val (start, end) = periodExtractor()
this.start = start
this.end = end
}

value = Quantity().apply {
this.value = valueExtractor().toBigDecimal()
this.unit = unit
}
}
}
}
vishnuravi marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package edu.stanford.healthconnectonfhir

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module

Check warning on line 8 in modules/healthconnectonfhir/src/main/java/edu/stanford/healthconnectonfhir/RecordToObservationModule.kt

View check run for this annotation

Codecov / codecov/patch

modules/healthconnectonfhir/src/main/java/edu/stanford/healthconnectonfhir/RecordToObservationModule.kt#L8

Added line #L8 was not covered by tests
@InstallIn(SingletonComponent::class)
abstract class RecordToObservationModule {

@Binds
abstract fun bindRecordToObservationMapper(
impl: RecordToObservationMapperImpl,
): RecordToObservationMapper
}
Loading
Loading