@appeeky/expo-healthkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +92 -0
  3. package/README.md +453 -0
  4. package/android/build.gradle +22 -0
  5. package/android/src/main/AndroidManifest.xml +74 -0
  6. package/android/src/main/java/expo/modules/healthkit/ExpoHealthKitModule.kt +228 -0
  7. package/android/src/main/java/expo/modules/healthkit/HealthConnectMapping.kt +129 -0
  8. package/android/src/main/java/expo/modules/healthkit/HealthConnectNutrition.kt +152 -0
  9. package/android/src/main/java/expo/modules/healthkit/HealthConnectRationaleActivity.kt +62 -0
  10. package/android/src/main/java/expo/modules/healthkit/HealthConnectService.kt +1054 -0
  11. package/android/src/main/java/expo/modules/healthkit/HealthConnectUnits.kt +133 -0
  12. package/android/src/main/java/expo/modules/healthkit/HealthConnectWorkouts.kt +105 -0
  13. package/android/src/main/java/expo/modules/healthkit/HealthKitExceptions.kt +30 -0
  14. package/app.plugin.js +1 -0
  15. package/build/ExpoHealthKitModule.d.ts +39 -0
  16. package/build/ExpoHealthKitModule.d.ts.map +1 -0
  17. package/build/ExpoHealthKitModule.js +35 -0
  18. package/build/ExpoHealthKitModule.js.map +1 -0
  19. package/build/ExpoHealthKitModule.web.d.ts +39 -0
  20. package/build/ExpoHealthKitModule.web.d.ts.map +1 -0
  21. package/build/ExpoHealthKitModule.web.js +105 -0
  22. package/build/ExpoHealthKitModule.web.js.map +1 -0
  23. package/build/dates.d.ts +6 -0
  24. package/build/dates.d.ts.map +1 -0
  25. package/build/dates.js +13 -0
  26. package/build/dates.js.map +1 -0
  27. package/build/errors.d.ts +9 -0
  28. package/build/errors.d.ts.map +1 -0
  29. package/build/errors.js +18 -0
  30. package/build/errors.js.map +1 -0
  31. package/build/identifiers.d.ts +418 -0
  32. package/build/identifiers.d.ts.map +1 -0
  33. package/build/identifiers.js +402 -0
  34. package/build/identifiers.js.map +1 -0
  35. package/build/index.d.ts +454 -0
  36. package/build/index.d.ts.map +1 -0
  37. package/build/index.js +418 -0
  38. package/build/index.js.map +1 -0
  39. package/build/types.d.ts +542 -0
  40. package/build/types.d.ts.map +1 -0
  41. package/build/types.js +2 -0
  42. package/build/types.js.map +1 -0
  43. package/docs/banner.jpg +0 -0
  44. package/expo-module.config.json +9 -0
  45. package/ios/ExpoHealthKit.podspec +30 -0
  46. package/ios/ExpoHealthKitModule.swift +155 -0
  47. package/ios/HealthKitAdvancedQueries.swift +458 -0
  48. package/ios/HealthKitExceptions.swift +109 -0
  49. package/ios/HealthKitIdentifiers.swift +183 -0
  50. package/ios/HealthKitRecords.swift +214 -0
  51. package/ios/HealthKitService.swift +626 -0
  52. package/ios/PrivacyInfo.xcprivacy +14 -0
  53. package/package.json +105 -0
  54. package/plugin/build/index.d.ts +30 -0
  55. package/plugin/build/index.js +138 -0
  56. package/src/ExpoHealthKitModule.ts +116 -0
  57. package/src/ExpoHealthKitModule.web.ts +140 -0
  58. package/src/dates.ts +17 -0
  59. package/src/errors.ts +22 -0
  60. package/src/identifiers.ts +494 -0
  61. package/src/index.ts +563 -0
  62. package/src/types.ts +625 -0
@@ -0,0 +1,228 @@
1
+ package expo.modules.healthkit
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
6
+ import androidx.health.connect.client.PermissionController
7
+ import expo.modules.interfaces.permissions.PermissionsStatus
8
+ import expo.modules.kotlin.functions.Coroutine
9
+ import expo.modules.kotlin.modules.Module
10
+ import expo.modules.kotlin.modules.ModuleDefinition
11
+ import kotlinx.coroutines.CancellableContinuation
12
+ import kotlinx.coroutines.Dispatchers
13
+ import kotlinx.coroutines.suspendCancellableCoroutine
14
+ import kotlinx.coroutines.withContext
15
+ import kotlin.coroutines.resume
16
+ import kotlin.coroutines.resumeWithException
17
+
18
+ class ExpoHealthKitModule : Module() {
19
+ private lateinit var service: HealthConnectService
20
+ private var permissionContinuation: CancellableContinuation<Set<String>>? = null
21
+ private val permissionContract = PermissionController.createRequestPermissionResultContract()
22
+
23
+ override fun definition() = ModuleDefinition {
24
+ Name("ExpoHealthKit")
25
+
26
+ Events("onUpdate")
27
+
28
+ OnCreate {
29
+ val context = appContext.reactContext ?: appContext.throwingActivity.applicationContext
30
+ service = HealthConnectService(context) { permissions ->
31
+ requestHealthPermissions(permissions)
32
+ }
33
+ }
34
+
35
+ OnActivityResult { _, payload ->
36
+ if (payload.requestCode != REQUEST_PERMISSIONS) {
37
+ return@OnActivityResult
38
+ }
39
+ val granted = permissionContract.parseResult(payload.resultCode, payload.data)
40
+ val continuation = permissionContinuation
41
+ permissionContinuation = null
42
+ continuation?.resume(granted)
43
+ }
44
+
45
+ Function("isHealthDataAvailable") {
46
+ HealthConnectService.isAvailable(appContext.reactContext)
47
+ }
48
+
49
+ AsyncFunction("requestAuthorization") Coroutine { options: Map<String, Any?> ->
50
+ service.requestAuthorization(options)
51
+ }
52
+
53
+ AsyncFunction("getAuthorizationStatus") Coroutine { identifier: String ->
54
+ service.authorizationStatus(identifier)
55
+ }
56
+
57
+ AsyncFunction("getRequestStatusForAuthorization") Coroutine { options: Map<String, Any?> ->
58
+ service.requestStatusForAuthorization(options)
59
+ }
60
+
61
+ AsyncFunction("queryQuantitySamples") Coroutine { options: Map<String, Any?> ->
62
+ service.queryQuantitySamples(options)
63
+ }
64
+
65
+ AsyncFunction("queryCategorySamples") Coroutine { options: Map<String, Any?> ->
66
+ service.queryCategorySamples(options)
67
+ }
68
+
69
+ AsyncFunction("queryWorkouts") Coroutine { options: Map<String, Any?> ->
70
+ service.queryWorkouts(options)
71
+ }
72
+
73
+ AsyncFunction("queryElectrocardiograms") { _: Map<String, Any?> ->
74
+ service.unsupported("Electrocardiograms")
75
+ }
76
+
77
+ AsyncFunction("queryActivitySummaries") { _: Map<String, Any?> ->
78
+ service.unsupported("Activity rings")
79
+ }
80
+
81
+ AsyncFunction("queryClinicalRecords") { _: Map<String, Any?> ->
82
+ service.unsupported("Clinical records")
83
+ }
84
+
85
+ AsyncFunction("queryAudiograms") { _: Map<String, Any?> ->
86
+ service.unsupported("Audiograms")
87
+ }
88
+
89
+ AsyncFunction("queryWorkoutRoute") { _: Map<String, Any?> ->
90
+ service.unsupported("Workout GPS routes")
91
+ }
92
+
93
+ AsyncFunction("queryCorrelations") Coroutine { options: Map<String, Any?> ->
94
+ service.queryCorrelations(options)
95
+ }
96
+
97
+ AsyncFunction("queryHeartbeatSeries") { _: Map<String, Any?> ->
98
+ service.unsupported("Heartbeat series")
99
+ }
100
+
101
+ AsyncFunction("queryStatistics") Coroutine { options: Map<String, Any?> ->
102
+ service.queryStatistics(options)
103
+ }
104
+
105
+ AsyncFunction("queryStatisticsCollection") Coroutine { options: Map<String, Any?> ->
106
+ service.queryStatisticsCollection(options)
107
+ }
108
+
109
+ AsyncFunction("queryAnchored") Coroutine { options: Map<String, Any?> ->
110
+ service.queryAnchored(options)
111
+ }
112
+
113
+ AsyncFunction("saveQuantitySample") Coroutine { sample: Map<String, Any?> ->
114
+ service.saveQuantitySample(sample)
115
+ }
116
+
117
+ AsyncFunction("saveCategorySample") Coroutine { sample: Map<String, Any?> ->
118
+ service.saveCategorySample(sample)
119
+ }
120
+
121
+ AsyncFunction("saveWorkout") Coroutine { workout: Map<String, Any?> ->
122
+ service.saveWorkout(workout)
123
+ }
124
+
125
+ AsyncFunction("saveCorrelation") Coroutine { correlation: Map<String, Any?> ->
126
+ service.saveCorrelation(correlation)
127
+ }
128
+
129
+ AsyncFunction("deleteObjects") Coroutine { options: Map<String, Any?> ->
130
+ service.deleteObjects(options)
131
+ }
132
+
133
+ AsyncFunction("getBiologicalSex") {
134
+ service.unsupported("Biological sex")
135
+ }
136
+
137
+ AsyncFunction("getBloodType") {
138
+ service.unsupported("Blood type")
139
+ }
140
+
141
+ AsyncFunction("getDateOfBirth") {
142
+ service.unsupported("Date of birth")
143
+ }
144
+
145
+ AsyncFunction("getFitzpatrickSkinType") {
146
+ service.unsupported("Fitzpatrick skin type")
147
+ }
148
+
149
+ AsyncFunction("getWheelchairUse") {
150
+ service.unsupported("Wheelchair use")
151
+ }
152
+
153
+ AsyncFunction("enableBackgroundDelivery") { _: String, _: Int ->
154
+ service.unsupported("Background delivery")
155
+ }
156
+
157
+ AsyncFunction("disableBackgroundDelivery") { _: String ->
158
+ service.unsupported("Background delivery")
159
+ }
160
+
161
+ AsyncFunction("disableAllBackgroundDelivery") {
162
+ service.unsupported("Background delivery")
163
+ }
164
+
165
+ AsyncFunction("observeTypes") { _: List<String> ->
166
+ service.unsupported("Observer queries")
167
+ }
168
+
169
+ AsyncFunction("clearObserverQueries") {
170
+ Unit
171
+ }
172
+ }
173
+
174
+ private suspend fun requestHealthPermissions(permissions: Set<String>): Set<String> {
175
+ val activity: Activity = appContext.throwingActivity
176
+ val intent = permissionContract.createIntent(activity, permissions)
177
+ return withContext(Dispatchers.Main) {
178
+ if (intent.action == RequestMultiplePermissions.ACTION_REQUEST_PERMISSIONS) {
179
+ val platformPermissions =
180
+ intent.getStringArrayExtra(RequestMultiplePermissions.EXTRA_PERMISSIONS)?.toSet()
181
+ ?: permissions
182
+ requestPlatformPermissions(platformPermissions)
183
+ } else {
184
+ requestHealthConnectActivityPermissions(activity, intent)
185
+ }
186
+ }
187
+ }
188
+
189
+ private suspend fun requestPlatformPermissions(permissions: Set<String>): Set<String> {
190
+ val manager = appContext.permissions ?: throw HealthKitUnavailableException()
191
+ return suspendCancellableCoroutine { continuation ->
192
+ manager.askForPermissions(
193
+ { responses ->
194
+ if (continuation.isActive) {
195
+ continuation.resume(
196
+ responses.filter { it.value.status == PermissionsStatus.GRANTED }.keys.toSet()
197
+ )
198
+ }
199
+ },
200
+ *permissions.toTypedArray()
201
+ )
202
+ }
203
+ }
204
+
205
+ private suspend fun requestHealthConnectActivityPermissions(
206
+ activity: Activity,
207
+ intent: Intent
208
+ ): Set<String> =
209
+ suspendCancellableCoroutine { continuation ->
210
+ permissionContinuation?.cancel()
211
+ permissionContinuation = continuation
212
+ continuation.invokeOnCancellation {
213
+ if (permissionContinuation === continuation) {
214
+ permissionContinuation = null
215
+ }
216
+ }
217
+ try {
218
+ activity.startActivityForResult(intent, REQUEST_PERMISSIONS)
219
+ } catch (error: Exception) {
220
+ permissionContinuation = null
221
+ continuation.resumeWithException(error)
222
+ }
223
+ }
224
+
225
+ companion object {
226
+ private const val REQUEST_PERMISSIONS = 0x484B
227
+ }
228
+ }
@@ -0,0 +1,129 @@
1
+ package expo.modules.healthkit
2
+
3
+ import androidx.health.connect.client.permission.HealthPermission
4
+ import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
5
+ import androidx.health.connect.client.records.BloodGlucoseRecord
6
+ import androidx.health.connect.client.records.BloodPressureRecord
7
+ import androidx.health.connect.client.records.BodyFatRecord
8
+ import androidx.health.connect.client.records.BodyTemperatureRecord
9
+ import androidx.health.connect.client.records.DistanceRecord
10
+ import androidx.health.connect.client.records.ExerciseSessionRecord
11
+ import androidx.health.connect.client.records.FloorsClimbedRecord
12
+ import androidx.health.connect.client.records.HeartRateRecord
13
+ import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
14
+ import androidx.health.connect.client.records.HeightRecord
15
+ import androidx.health.connect.client.records.HydrationRecord
16
+ import androidx.health.connect.client.records.LeanBodyMassRecord
17
+ import androidx.health.connect.client.records.NutritionRecord
18
+ import androidx.health.connect.client.records.OxygenSaturationRecord
19
+ import androidx.health.connect.client.records.Record
20
+ import androidx.health.connect.client.records.RespiratoryRateRecord
21
+ import androidx.health.connect.client.records.RestingHeartRateRecord
22
+ import androidx.health.connect.client.records.SleepSessionRecord
23
+ import androidx.health.connect.client.records.StepsRecord
24
+ import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
25
+ import androidx.health.connect.client.records.Vo2MaxRecord
26
+ import androidx.health.connect.client.records.WeightRecord
27
+ import androidx.health.connect.client.records.WheelchairPushesRecord
28
+ import kotlin.reflect.KClass
29
+
30
+ internal object HealthConnectMapping {
31
+ const val STEPS = "HKQuantityTypeIdentifierStepCount"
32
+ const val DISTANCE_WALKING = "HKQuantityTypeIdentifierDistanceWalkingRunning"
33
+ const val DISTANCE_CYCLING = "HKQuantityTypeIdentifierDistanceCycling"
34
+ const val DISTANCE_SWIMMING = "HKQuantityTypeIdentifierDistanceSwimming"
35
+ const val DISTANCE_WHEELCHAIR = "HKQuantityTypeIdentifierDistanceWheelchair"
36
+ const val ACTIVE_ENERGY = "HKQuantityTypeIdentifierActiveEnergyBurned"
37
+ const val BASAL_ENERGY = "HKQuantityTypeIdentifierBasalEnergyBurned"
38
+ const val FLOORS = "HKQuantityTypeIdentifierFlightsClimbed"
39
+ const val PUSH_COUNT = "HKQuantityTypeIdentifierPushCount"
40
+ const val VO2_MAX = "HKQuantityTypeIdentifierVO2Max"
41
+ const val HEART_RATE = "HKQuantityTypeIdentifierHeartRate"
42
+ const val RESTING_HEART_RATE = "HKQuantityTypeIdentifierRestingHeartRate"
43
+ const val HRV = "HKQuantityTypeIdentifierHeartRateVariabilitySDNN"
44
+ const val OXYGEN = "HKQuantityTypeIdentifierOxygenSaturation"
45
+ const val RESPIRATORY = "HKQuantityTypeIdentifierRespiratoryRate"
46
+ const val BODY_TEMP = "HKQuantityTypeIdentifierBodyTemperature"
47
+ const val GLUCOSE = "HKQuantityTypeIdentifierBloodGlucose"
48
+ const val SYSTOLIC = "HKQuantityTypeIdentifierBloodPressureSystolic"
49
+ const val DIASTOLIC = "HKQuantityTypeIdentifierBloodPressureDiastolic"
50
+ const val HEIGHT = "HKQuantityTypeIdentifierHeight"
51
+ const val WEIGHT = "HKQuantityTypeIdentifierBodyMass"
52
+ const val LEAN_MASS = "HKQuantityTypeIdentifierLeanBodyMass"
53
+ const val BODY_FAT = "HKQuantityTypeIdentifierBodyFatPercentage"
54
+ const val WATER = "HKQuantityTypeIdentifierDietaryWater"
55
+ const val SLEEP = "HKCategoryTypeIdentifierSleepAnalysis"
56
+ const val WORKOUT = "HKWorkoutTypeIdentifier"
57
+ const val BLOOD_PRESSURE = "HKCorrelationTypeIdentifierBloodPressure"
58
+ const val FOOD = "HKCorrelationTypeIdentifierFood"
59
+
60
+ fun recordClass(identifier: String): KClass<out Record>? = when (identifier) {
61
+ STEPS -> StepsRecord::class
62
+ DISTANCE_WALKING, DISTANCE_CYCLING, DISTANCE_SWIMMING, DISTANCE_WHEELCHAIR -> DistanceRecord::class
63
+ ACTIVE_ENERGY -> ActiveCaloriesBurnedRecord::class
64
+ BASAL_ENERGY -> TotalCaloriesBurnedRecord::class
65
+ FLOORS -> FloorsClimbedRecord::class
66
+ PUSH_COUNT -> WheelchairPushesRecord::class
67
+ VO2_MAX -> Vo2MaxRecord::class
68
+ HEART_RATE -> HeartRateRecord::class
69
+ RESTING_HEART_RATE -> RestingHeartRateRecord::class
70
+ HRV -> HeartRateVariabilityRmssdRecord::class
71
+ OXYGEN -> OxygenSaturationRecord::class
72
+ RESPIRATORY -> RespiratoryRateRecord::class
73
+ BODY_TEMP -> BodyTemperatureRecord::class
74
+ GLUCOSE -> BloodGlucoseRecord::class
75
+ SYSTOLIC, DIASTOLIC, BLOOD_PRESSURE -> BloodPressureRecord::class
76
+ HEIGHT -> HeightRecord::class
77
+ WEIGHT -> WeightRecord::class
78
+ LEAN_MASS -> LeanBodyMassRecord::class
79
+ BODY_FAT -> BodyFatRecord::class
80
+ WATER -> HydrationRecord::class
81
+ SLEEP -> SleepSessionRecord::class
82
+ WORKOUT -> ExerciseSessionRecord::class
83
+ FOOD -> NutritionRecord::class
84
+ else -> if (isDietary(identifier)) NutritionRecord::class else null
85
+ }
86
+
87
+ fun extraRecordClasses(identifier: String): List<KClass<out Record>> =
88
+ if (identifier == BASAL_ENERGY) listOf(ActiveCaloriesBurnedRecord::class) else emptyList()
89
+
90
+ fun permissions(identifiers: List<String>, write: Boolean): Set<String> {
91
+ val classes = linkedSetOf<KClass<out Record>>()
92
+ for (identifier in identifiers) {
93
+ val primary = recordClass(identifier) ?: continue
94
+ classes += primary
95
+ classes += extraRecordClasses(identifier)
96
+ }
97
+ return classes.mapTo(mutableSetOf()) { record ->
98
+ if (write) HealthPermission.getWritePermission(record) else HealthPermission.getReadPermission(record)
99
+ }
100
+ }
101
+
102
+ fun quantityKind(identifier: String): QuantityKind = when (identifier) {
103
+ STEPS, PUSH_COUNT, FLOORS -> if (identifier == FLOORS) QuantityKind.FLOORS else QuantityKind.COUNT
104
+ HEART_RATE, RESTING_HEART_RATE -> QuantityKind.BPM
105
+ HRV -> QuantityKind.HRV
106
+ VO2_MAX -> QuantityKind.VO2
107
+ ACTIVE_ENERGY, BASAL_ENERGY, DIETARY_ENERGY -> QuantityKind.ENERGY
108
+ OXYGEN, BODY_FAT -> QuantityKind.PERCENT
109
+ RESPIRATORY -> QuantityKind.COUNT
110
+ BODY_TEMP -> QuantityKind.TEMPERATURE
111
+ GLUCOSE -> QuantityKind.GLUCOSE
112
+ SYSTOLIC, DIASTOLIC -> QuantityKind.PRESSURE
113
+ HEIGHT, DISTANCE_WALKING, DISTANCE_CYCLING, DISTANCE_SWIMMING, DISTANCE_WHEELCHAIR -> QuantityKind.LENGTH
114
+ WEIGHT, LEAN_MASS -> QuantityKind.MASS
115
+ WATER -> QuantityKind.VOLUME
116
+ else -> if (identifier == DIETARY_ENERGY) QuantityKind.ENERGY else if (isDietary(identifier)) QuantityKind.MASS else QuantityKind.COUNT
117
+ }
118
+
119
+ fun isCumulative(identifier: String): Boolean = when (identifier) {
120
+ STEPS, DISTANCE_WALKING, DISTANCE_CYCLING, DISTANCE_SWIMMING, DISTANCE_WHEELCHAIR,
121
+ ACTIVE_ENERGY, BASAL_ENERGY, FLOORS, PUSH_COUNT, WATER -> true
122
+ else -> isDietary(identifier)
123
+ }
124
+
125
+ fun isDietary(identifier: String): Boolean =
126
+ identifier.startsWith("HKQuantityTypeIdentifierDietary")
127
+
128
+ const val DIETARY_ENERGY = "HKQuantityTypeIdentifierDietaryEnergyConsumed"
129
+ }
@@ -0,0 +1,152 @@
1
+ package expo.modules.healthkit
2
+
3
+ import androidx.health.connect.client.records.NutritionRecord
4
+ import androidx.health.connect.client.records.metadata.Metadata
5
+ import androidx.health.connect.client.units.Energy
6
+ import androidx.health.connect.client.units.Mass
7
+ import java.time.Instant
8
+ import java.time.ZoneOffset
9
+
10
+ internal object HealthConnectNutrition {
11
+ fun canonicalValue(record: NutritionRecord, identifier: String): Double? = when (identifier) {
12
+ HealthConnectMapping.DIETARY_ENERGY -> record.energy?.inKilocalories
13
+ "HKQuantityTypeIdentifierDietaryProtein" -> record.protein?.inKilograms
14
+ "HKQuantityTypeIdentifierDietaryCarbohydrates" -> record.totalCarbohydrate?.inKilograms
15
+ "HKQuantityTypeIdentifierDietaryFatTotal" -> record.totalFat?.inKilograms
16
+ "HKQuantityTypeIdentifierDietaryFatSaturated" -> record.saturatedFat?.inKilograms
17
+ "HKQuantityTypeIdentifierDietaryFatMonounsaturated" -> record.monounsaturatedFat?.inKilograms
18
+ "HKQuantityTypeIdentifierDietaryFatPolyunsaturated" -> record.polyunsaturatedFat?.inKilograms
19
+ "HKQuantityTypeIdentifierDietaryCholesterol" -> record.cholesterol?.inKilograms
20
+ "HKQuantityTypeIdentifierDietarySodium" -> record.sodium?.inKilograms
21
+ "HKQuantityTypeIdentifierDietarySugar" -> record.sugar?.inKilograms
22
+ "HKQuantityTypeIdentifierDietaryFiber" -> record.dietaryFiber?.inKilograms
23
+ "HKQuantityTypeIdentifierDietaryCaffeine" -> record.caffeine?.inKilograms
24
+ "HKQuantityTypeIdentifierDietaryCalcium" -> record.calcium?.inKilograms
25
+ "HKQuantityTypeIdentifierDietaryIron" -> record.iron?.inKilograms
26
+ "HKQuantityTypeIdentifierDietaryPotassium" -> record.potassium?.inKilograms
27
+ "HKQuantityTypeIdentifierDietaryVitaminA" -> record.vitaminA?.inKilograms
28
+ "HKQuantityTypeIdentifierDietaryVitaminC" -> record.vitaminC?.inKilograms
29
+ "HKQuantityTypeIdentifierDietaryVitaminD" -> record.vitaminD?.inKilograms
30
+ "HKQuantityTypeIdentifierDietaryVitaminE" -> record.vitaminE?.inKilograms
31
+ "HKQuantityTypeIdentifierDietaryVitaminK" -> record.vitaminK?.inKilograms
32
+ "HKQuantityTypeIdentifierDietaryVitaminB6" -> record.vitaminB6?.inKilograms
33
+ "HKQuantityTypeIdentifierDietaryVitaminB12" -> record.vitaminB12?.inKilograms
34
+ "HKQuantityTypeIdentifierDietaryFolate" -> record.folate?.inKilograms
35
+ "HKQuantityTypeIdentifierDietaryNiacin" -> record.niacin?.inKilograms
36
+ "HKQuantityTypeIdentifierDietaryRiboflavin" -> record.riboflavin?.inKilograms
37
+ "HKQuantityTypeIdentifierDietaryThiamin" -> record.thiamin?.inKilograms
38
+ "HKQuantityTypeIdentifierDietaryMagnesium" -> record.magnesium?.inKilograms
39
+ "HKQuantityTypeIdentifierDietaryManganese" -> record.manganese?.inKilograms
40
+ "HKQuantityTypeIdentifierDietaryPhosphorus" -> record.phosphorus?.inKilograms
41
+ "HKQuantityTypeIdentifierDietarySelenium" -> record.selenium?.inKilograms
42
+ "HKQuantityTypeIdentifierDietaryZinc" -> record.zinc?.inKilograms
43
+ "HKQuantityTypeIdentifierDietaryIodine" -> record.iodine?.inKilograms
44
+ "HKQuantityTypeIdentifierDietaryChloride" -> record.chloride?.inKilograms
45
+ "HKQuantityTypeIdentifierDietaryBiotin" -> record.biotin?.inKilograms
46
+ "HKQuantityTypeIdentifierDietaryPantothenicAcid" -> record.pantothenicAcid?.inKilograms
47
+ "HKQuantityTypeIdentifierDietaryMolybdenum" -> record.molybdenum?.inKilograms
48
+ "HKQuantityTypeIdentifierDietaryCopper" -> record.copper?.inKilograms
49
+ "HKQuantityTypeIdentifierDietaryChromium" -> record.chromium?.inKilograms
50
+ else -> null
51
+ }
52
+
53
+ fun presentFields(record: NutritionRecord): List<Pair<String, Double>> {
54
+ val identifiers = listOf(
55
+ HealthConnectMapping.DIETARY_ENERGY,
56
+ "HKQuantityTypeIdentifierDietaryProtein",
57
+ "HKQuantityTypeIdentifierDietaryCarbohydrates",
58
+ "HKQuantityTypeIdentifierDietaryFatTotal",
59
+ "HKQuantityTypeIdentifierDietaryFatSaturated",
60
+ "HKQuantityTypeIdentifierDietaryFatMonounsaturated",
61
+ "HKQuantityTypeIdentifierDietaryFatPolyunsaturated",
62
+ "HKQuantityTypeIdentifierDietaryCholesterol",
63
+ "HKQuantityTypeIdentifierDietarySodium",
64
+ "HKQuantityTypeIdentifierDietarySugar",
65
+ "HKQuantityTypeIdentifierDietaryFiber",
66
+ "HKQuantityTypeIdentifierDietaryCaffeine",
67
+ "HKQuantityTypeIdentifierDietaryCalcium",
68
+ "HKQuantityTypeIdentifierDietaryIron",
69
+ "HKQuantityTypeIdentifierDietaryPotassium",
70
+ "HKQuantityTypeIdentifierDietaryVitaminA",
71
+ "HKQuantityTypeIdentifierDietaryVitaminC",
72
+ "HKQuantityTypeIdentifierDietaryVitaminD",
73
+ "HKQuantityTypeIdentifierDietaryVitaminE",
74
+ "HKQuantityTypeIdentifierDietaryVitaminK",
75
+ "HKQuantityTypeIdentifierDietaryVitaminB6",
76
+ "HKQuantityTypeIdentifierDietaryVitaminB12",
77
+ "HKQuantityTypeIdentifierDietaryFolate",
78
+ "HKQuantityTypeIdentifierDietaryNiacin",
79
+ "HKQuantityTypeIdentifierDietaryRiboflavin",
80
+ "HKQuantityTypeIdentifierDietaryThiamin",
81
+ "HKQuantityTypeIdentifierDietaryMagnesium",
82
+ "HKQuantityTypeIdentifierDietaryManganese",
83
+ "HKQuantityTypeIdentifierDietaryPhosphorus",
84
+ "HKQuantityTypeIdentifierDietarySelenium",
85
+ "HKQuantityTypeIdentifierDietaryZinc",
86
+ "HKQuantityTypeIdentifierDietaryIodine",
87
+ "HKQuantityTypeIdentifierDietaryChloride",
88
+ "HKQuantityTypeIdentifierDietaryBiotin",
89
+ "HKQuantityTypeIdentifierDietaryPantothenicAcid",
90
+ "HKQuantityTypeIdentifierDietaryMolybdenum",
91
+ "HKQuantityTypeIdentifierDietaryCopper",
92
+ "HKQuantityTypeIdentifierDietaryChromium"
93
+ )
94
+ return identifiers.mapNotNull { id -> canonicalValue(record, id)?.let { id to it } }
95
+ }
96
+
97
+ fun build(
98
+ start: Instant,
99
+ end: Instant,
100
+ startOffset: ZoneOffset?,
101
+ endOffset: ZoneOffset?,
102
+ metadata: Metadata,
103
+ values: Map<String, Double>
104
+ ): NutritionRecord {
105
+ fun mass(id: String) = values[id]?.let { Mass.kilograms(it) }
106
+ return NutritionRecord(
107
+ startTime = start,
108
+ startZoneOffset = startOffset,
109
+ endTime = end,
110
+ endZoneOffset = endOffset,
111
+ energy = values[HealthConnectMapping.DIETARY_ENERGY]?.let { Energy.kilocalories(it) },
112
+ protein = mass("HKQuantityTypeIdentifierDietaryProtein"),
113
+ totalCarbohydrate = mass("HKQuantityTypeIdentifierDietaryCarbohydrates"),
114
+ totalFat = mass("HKQuantityTypeIdentifierDietaryFatTotal"),
115
+ saturatedFat = mass("HKQuantityTypeIdentifierDietaryFatSaturated"),
116
+ monounsaturatedFat = mass("HKQuantityTypeIdentifierDietaryFatMonounsaturated"),
117
+ polyunsaturatedFat = mass("HKQuantityTypeIdentifierDietaryFatPolyunsaturated"),
118
+ cholesterol = mass("HKQuantityTypeIdentifierDietaryCholesterol"),
119
+ sodium = mass("HKQuantityTypeIdentifierDietarySodium"),
120
+ sugar = mass("HKQuantityTypeIdentifierDietarySugar"),
121
+ dietaryFiber = mass("HKQuantityTypeIdentifierDietaryFiber"),
122
+ caffeine = mass("HKQuantityTypeIdentifierDietaryCaffeine"),
123
+ calcium = mass("HKQuantityTypeIdentifierDietaryCalcium"),
124
+ iron = mass("HKQuantityTypeIdentifierDietaryIron"),
125
+ potassium = mass("HKQuantityTypeIdentifierDietaryPotassium"),
126
+ vitaminA = mass("HKQuantityTypeIdentifierDietaryVitaminA"),
127
+ vitaminC = mass("HKQuantityTypeIdentifierDietaryVitaminC"),
128
+ vitaminD = mass("HKQuantityTypeIdentifierDietaryVitaminD"),
129
+ vitaminE = mass("HKQuantityTypeIdentifierDietaryVitaminE"),
130
+ vitaminK = mass("HKQuantityTypeIdentifierDietaryVitaminK"),
131
+ vitaminB6 = mass("HKQuantityTypeIdentifierDietaryVitaminB6"),
132
+ vitaminB12 = mass("HKQuantityTypeIdentifierDietaryVitaminB12"),
133
+ folate = mass("HKQuantityTypeIdentifierDietaryFolate"),
134
+ niacin = mass("HKQuantityTypeIdentifierDietaryNiacin"),
135
+ riboflavin = mass("HKQuantityTypeIdentifierDietaryRiboflavin"),
136
+ thiamin = mass("HKQuantityTypeIdentifierDietaryThiamin"),
137
+ magnesium = mass("HKQuantityTypeIdentifierDietaryMagnesium"),
138
+ manganese = mass("HKQuantityTypeIdentifierDietaryManganese"),
139
+ phosphorus = mass("HKQuantityTypeIdentifierDietaryPhosphorus"),
140
+ selenium = mass("HKQuantityTypeIdentifierDietarySelenium"),
141
+ zinc = mass("HKQuantityTypeIdentifierDietaryZinc"),
142
+ iodine = mass("HKQuantityTypeIdentifierDietaryIodine"),
143
+ chloride = mass("HKQuantityTypeIdentifierDietaryChloride"),
144
+ biotin = mass("HKQuantityTypeIdentifierDietaryBiotin"),
145
+ pantothenicAcid = mass("HKQuantityTypeIdentifierDietaryPantothenicAcid"),
146
+ molybdenum = mass("HKQuantityTypeIdentifierDietaryMolybdenum"),
147
+ copper = mass("HKQuantityTypeIdentifierDietaryCopper"),
148
+ chromium = mass("HKQuantityTypeIdentifierDietaryChromium"),
149
+ metadata = metadata
150
+ )
151
+ }
152
+ }
@@ -0,0 +1,62 @@
1
+ package expo.modules.healthkit
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.net.Uri
6
+ import android.os.Bundle
7
+ import android.widget.Button
8
+ import android.widget.LinearLayout
9
+ import android.widget.TextView
10
+
11
+ class HealthConnectRationaleActivity : Activity() {
12
+ override fun onCreate(savedInstanceState: Bundle?) {
13
+ super.onCreate(savedInstanceState)
14
+
15
+ val padding = (20 * resources.displayMetrics.density).toInt()
16
+ val layout = LinearLayout(this).apply {
17
+ orientation = LinearLayout.VERTICAL
18
+ setPadding(padding, padding, padding, padding)
19
+ }
20
+
21
+ val title = TextView(this).apply {
22
+ text = "Health Connect"
23
+ textSize = 20f
24
+ }
25
+ val body = TextView(this).apply {
26
+ text =
27
+ "This app reads and writes the health types you approve on the Health Connect permission screen. Data stays on the device unless the app uploads it."
28
+ textSize = 16f
29
+ }
30
+ layout.addView(title)
31
+ layout.addView(body)
32
+
33
+ val privacyUrl = privacyPolicyUrl()
34
+ if (!privacyUrl.isNullOrBlank()) {
35
+ val privacy = Button(this).apply {
36
+ text = "Privacy policy"
37
+ setOnClickListener {
38
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(privacyUrl)))
39
+ }
40
+ }
41
+ layout.addView(privacy)
42
+ }
43
+
44
+ val close = Button(this).apply {
45
+ text = "Close"
46
+ setOnClickListener { finish() }
47
+ }
48
+ layout.addView(close)
49
+ setContentView(layout)
50
+ }
51
+
52
+ private fun privacyPolicyUrl(): String? = try {
53
+ val appInfo = packageManager.getApplicationInfo(packageName, android.content.pm.PackageManager.GET_META_DATA)
54
+ appInfo.metaData?.getString(META_PRIVACY_POLICY)
55
+ } catch (_: Exception) {
56
+ null
57
+ }
58
+
59
+ companion object {
60
+ const val META_PRIVACY_POLICY = "expo.modules.healthkit.HEALTH_CONNECT_PRIVACY_POLICY_URL"
61
+ }
62
+ }