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 14 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 @@ -6,9 +6,11 @@ androidTools = "31.4.0"
appcompat = "1.6.1"
compileSdk = "34"
composeBom = "2024.05.00"
healthConnectClient = "1.1.0-alpha02"
coreKtx = "1.13.1"
coreTestingVersion = "2.2.0"
coroutinesVersion = "1.8.0"
fhirDataCapture = "1.0.0"
dokka = "1.9.20"
espressoCore = "3.5.1"
hiltNavigation = "1.2.0"
Expand All @@ -31,9 +33,11 @@ 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-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-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleKtx" }
androidx-lifecycle-view-model-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleKtx" }
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
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
12 changes: 12 additions & 0 deletions modules/healthconnectonfhir/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
alias(libs.plugins.spezi.library)
}

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

private fun <T: Record> T.createObservation(
vishnuravi marked this conversation as resolved.
Show resolved Hide resolved
categories: List<Coding> = listOf(),
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
}
}
}

fun StepsRecord.toObservation(): Observation {
return this.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) }
)
}

fun WeightRecord.toObservation(): Observation {
return this.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) }
)
}

fun HeightRecord.toObservation(): Observation {
return this.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) }
)
}

fun ActiveCaloriesBurnedRecord.toObservation(): Observation {
return this.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) }
)
}

fun BodyTemperatureRecord.toObservation(): Observation {
return 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) }
)
}

fun BloodPressureRecord.toObservation(): 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(time)
period.end = Date.from(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(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(diastolic.inMillimetersOfMercury).setUnit("mmHg")

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

return observation
}

fun HeartRateRecord.toObservations(): List<Observation> {
return 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
}
}
vishnuravi marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading