-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathHealthConnectRepository.kt
226 lines (197 loc) · 8.38 KB
/
HealthConnectRepository.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
package com.rafapps.taskerhealthconnect
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.Record
import androidx.health.connect.client.request.AggregateGroupByDurationRequest
import androidx.health.connect.client.request.ReadRecordsRequest
import androidx.health.connect.client.response.InsertRecordsResponse
import androidx.health.connect.client.time.TimeRangeFilter
import androidx.health.connect.client.units.BloodGlucose
import androidx.health.connect.client.units.Energy
import androidx.health.connect.client.units.Length
import androidx.health.connect.client.units.Mass
import androidx.health.connect.client.units.Percentage
import androidx.health.connect.client.units.Power
import androidx.health.connect.client.units.Pressure
import androidx.health.connect.client.units.Temperature
import androidx.health.connect.client.units.Velocity
import androidx.health.connect.client.units.Volume
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.type.TypeFactory
import org.json.JSONArray
import org.json.JSONObject
import java.time.Duration
import java.time.Instant
import kotlin.reflect.KClass
import kotlin.reflect.KVisibility
import kotlin.reflect.full.memberProperties
class HealthConnectRepository(private val context: Context) {
private val TAG = "HealthConnectRepository"
private val playStoreUri =
"https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata"
private val client by lazy { HealthConnectClient.getOrCreate(context) }
val permissions by lazy {
(singleRecordTypes + aggregateRecordTypes)
.flatMap {
listOf(
HealthPermission.getReadPermission(it),
HealthPermission.getWritePermission(it)
)
}
.toSet()
}
fun installHealthConnect() {
Log.d(TAG, "installHealthConnect")
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(playStoreUri)
setPackage("com.android.vending")
}
runCatching { context.startActivity(intent) }
}
fun isAvailable(): Boolean =
HealthConnectClient.getSdkStatus(context) == HealthConnectClient.SDK_AVAILABLE
suspend fun hasPermissions(): Boolean {
val granted = client.permissionController.getGrantedPermissions()
return granted.containsAll(permissions)
}
suspend fun getAggregateData(
startTime: Instant
): JSONArray {
Log.d(TAG, "getAggregateData: $startTime")
var dataPointSize = 0
val result = JSONArray()
val response = client.aggregateGroupByDuration(
AggregateGroupByDurationRequest(
metrics = aggregateMetricTypes.values.toSet(),
timeRangeFilter = TimeRangeFilter.after(startTime),
timeRangeSlicer = Duration.ofDays(1)
)
)
// loop through the "buckets" of data where each bucket is 1 day
response.forEach { aggregationResult ->
val data = mutableMapOf<String, Any>()
data["startTime"] = aggregationResult.startTime.toEpochMilli()
data["endTime"] = aggregationResult.endTime.toEpochMilli()
data["zoneOffset"] = aggregationResult.zoneOffset.toString()
// check for each data type in each bucket
aggregateMetricTypes.forEach { metricType ->
aggregationResult.result[metricType.value]?.let { dataPoint ->
extractDataPointValue(dataPoint)?.let {
dataPointSize++
data[metricType.key] = it
}
}
}
// put the data for that day as an object in the output array
result.put(JSONObject(data.toMap()))
}
Log.d(
TAG, "getAggregateData result size: ${response.size}," +
" dataPointSize: $dataPointSize"
)
return result
}
@Suppress("UNCHECKED_CAST")
suspend fun getData(recordClass: String, startTime: Instant): JSONArray {
Log.d(TAG, "getData: $recordClass, $startTime")
val result = JSONArray()
// convert it to a specific record type via reflection, will throw if fails
val kClass = Class.forName("androidx.health.connect.client.records.$recordClass")
.kotlin as KClass<Record>
// fetch the records for the given type
val response = client.readRecords(
ReadRecordsRequest(
recordType = kClass,
timeRangeFilter = TimeRangeFilter.after(startTime)
)
)
// process each record in the response
for (record in response.records) {
result.put(JSONObject(extractDataRecordValue(record)))
}
Log.d(TAG, "getData result size: ${response.records.size}")
return result
}
// use the actual data type to get the correct value
private fun extractDataPointValue(dataPoint: Comparable<*>): Any? {
return when (dataPoint) {
is Boolean -> dataPoint
is BloodGlucose -> dataPoint.inMilligramsPerDeciliter
is Double -> dataPoint
is Duration -> dataPoint.toMillis()
is Energy -> dataPoint.inCalories
is Length -> dataPoint.inMeters
is Instant -> dataPoint.toEpochMilli()
is Int -> dataPoint
is Long -> dataPoint
is Mass -> dataPoint.inKilograms
is Percentage -> dataPoint.value
is Power -> dataPoint.inWatts
is Pressure -> dataPoint.inMillimetersOfMercury
is String -> dataPoint
is Temperature -> dataPoint.inCelsius
is Velocity -> dataPoint.inMetersPerSecond
is Volume -> dataPoint.inLiters
else -> null
}
}
// cast to each record type to process individually
@Suppress("UNCHECKED_CAST")
private fun extractDataRecordValue(record: Record): Map<String, Any> {
val result = mutableMapOf<String, Any>()
val properties = (record::class as KClass<Record>)
.memberProperties.filter { it.visibility == KVisibility.PUBLIC }
for (prop in properties) {
val value = prop.get(record) ?: continue
val key = prop.name
when (value) {
is Comparable<*> -> extractDataPointValue(value)?.let { result[key] = it }
is List<*> -> result[key] = extractListData(value)
}
}
return result
}
// extract records where there is a list of samples
@Suppress("UNCHECKED_CAST")
private fun extractListData(samples: List<*>): List<Any> {
val result = mutableListOf<Any>()
for (sample in samples) {
if (sample == null) continue
val obj = mutableMapOf<String, Any>()
val properties = (sample::class as KClass<Any>)
.memberProperties.filter { it.visibility == KVisibility.PUBLIC }
for (prop in properties) {
val value = prop.get(sample) ?: continue
val key = prop.name
if (value is Comparable<*>) {
extractDataPointValue(value)?.let { obj[key] = it }
}
}
result.add(obj)
}
return result
}
suspend fun insertData(
recordClass: String, recordJson: String
): List<String> {
Log.d(TAG, "insertData: $recordClass")
val kClass = Class.forName("androidx.health.connect.client.records.$recordClass")
val result: InsertRecordsResponse
val jsonNode: JsonNode = objectMapper.readTree(recordJson)
if (jsonNode.isArray) {
val type =
TypeFactory.defaultInstance().constructCollectionType(List::class.java, kClass)
val record: List<Record> = objectMapper.treeToValue(jsonNode, type)
result = client.insertRecords(record)
} else {
val record: Record = objectMapper.treeToValue(jsonNode, kClass) as Record
result = client.insertRecords(listOf(record))
}
Log.d(TAG, "insertData: result size ${result.recordIdsList.size}")
return result.recordIdsList
}
}