@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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +92 -0
- package/README.md +453 -0
- package/android/build.gradle +22 -0
- package/android/src/main/AndroidManifest.xml +74 -0
- package/android/src/main/java/expo/modules/healthkit/ExpoHealthKitModule.kt +228 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectMapping.kt +129 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectNutrition.kt +152 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectRationaleActivity.kt +62 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectService.kt +1054 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectUnits.kt +133 -0
- package/android/src/main/java/expo/modules/healthkit/HealthConnectWorkouts.kt +105 -0
- package/android/src/main/java/expo/modules/healthkit/HealthKitExceptions.kt +30 -0
- package/app.plugin.js +1 -0
- package/build/ExpoHealthKitModule.d.ts +39 -0
- package/build/ExpoHealthKitModule.d.ts.map +1 -0
- package/build/ExpoHealthKitModule.js +35 -0
- package/build/ExpoHealthKitModule.js.map +1 -0
- package/build/ExpoHealthKitModule.web.d.ts +39 -0
- package/build/ExpoHealthKitModule.web.d.ts.map +1 -0
- package/build/ExpoHealthKitModule.web.js +105 -0
- package/build/ExpoHealthKitModule.web.js.map +1 -0
- package/build/dates.d.ts +6 -0
- package/build/dates.d.ts.map +1 -0
- package/build/dates.js +13 -0
- package/build/dates.js.map +1 -0
- package/build/errors.d.ts +9 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +18 -0
- package/build/errors.js.map +1 -0
- package/build/identifiers.d.ts +418 -0
- package/build/identifiers.d.ts.map +1 -0
- package/build/identifiers.js +402 -0
- package/build/identifiers.js.map +1 -0
- package/build/index.d.ts +454 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +418 -0
- package/build/index.js.map +1 -0
- package/build/types.d.ts +542 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/docs/banner.jpg +0 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoHealthKit.podspec +30 -0
- package/ios/ExpoHealthKitModule.swift +155 -0
- package/ios/HealthKitAdvancedQueries.swift +458 -0
- package/ios/HealthKitExceptions.swift +109 -0
- package/ios/HealthKitIdentifiers.swift +183 -0
- package/ios/HealthKitRecords.swift +214 -0
- package/ios/HealthKitService.swift +626 -0
- package/ios/PrivacyInfo.xcprivacy +14 -0
- package/package.json +105 -0
- package/plugin/build/index.d.ts +30 -0
- package/plugin/build/index.js +138 -0
- package/src/ExpoHealthKitModule.ts +116 -0
- package/src/ExpoHealthKitModule.web.ts +140 -0
- package/src/dates.ts +17 -0
- package/src/errors.ts +22 -0
- package/src/identifiers.ts +494 -0
- package/src/index.ts +563 -0
- 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
|
+
}
|