@asiriindatissa/capacitor-health 9.0.3 → 9.0.4
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/README.md +48 -52
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/java/app/capgo/plugin/health/HealthManager.kt +63 -71
- package/android/src/main/java/app/capgo/plugin/health/HealthPlugin.kt +51 -64
- package/android/src/main/java/app/capgo/plugin/health/WorkoutType.kt +64 -0
- package/dist/docs.json +163 -79
- package/dist/esm/definitions.d.ts +29 -16
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +2 -2
- package/dist/esm/web.js +3 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +3 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +3 -3
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
- package/android/src/main/java/app/capgo/plugin/health/HealthDataType.kt +0 -24
package/README.md
CHANGED
|
@@ -213,10 +213,10 @@ const { sleepSessions } = await Health.querySleep({
|
|
|
213
213
|
* [`isAvailable()`](#isavailable)
|
|
214
214
|
* [`requestAuthorization(...)`](#requestauthorization)
|
|
215
215
|
* [`checkAuthorization(...)`](#checkauthorization)
|
|
216
|
-
* [`readSamples(...)`](#readsamples)
|
|
217
216
|
* [`getPluginVersion()`](#getpluginversion)
|
|
218
217
|
* [`openHealthConnectSettings()`](#openhealthconnectsettings)
|
|
219
218
|
* [`showPrivacyPolicy()`](#showprivacypolicy)
|
|
219
|
+
* [`queryWorkouts(...)`](#queryworkouts)
|
|
220
220
|
* [`querySleep(...)`](#querysleep)
|
|
221
221
|
* [`queryHydration(...)`](#queryhydration)
|
|
222
222
|
* [Interfaces](#interfaces)
|
|
@@ -274,23 +274,6 @@ Checks authorization status for the provided data types without prompting the us
|
|
|
274
274
|
--------------------
|
|
275
275
|
|
|
276
276
|
|
|
277
|
-
### readSamples(...)
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
readSamples(options: QueryOptions) => Promise<ReadSamplesResult>
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
Reads samples for the given data type within the specified time frame.
|
|
284
|
-
|
|
285
|
-
| Param | Type |
|
|
286
|
-
| ------------- | ----------------------------------------------------- |
|
|
287
|
-
| **`options`** | <code><a href="#queryoptions">QueryOptions</a></code> |
|
|
288
|
-
|
|
289
|
-
**Returns:** <code>Promise<<a href="#readsamplesresult">ReadSamplesResult</a>></code>
|
|
290
|
-
|
|
291
|
-
--------------------
|
|
292
|
-
|
|
293
|
-
|
|
294
277
|
### getPluginVersion()
|
|
295
278
|
|
|
296
279
|
```typescript
|
|
@@ -336,6 +319,23 @@ or by placing an HTML file at www/privacypolicy.html in your assets.
|
|
|
336
319
|
--------------------
|
|
337
320
|
|
|
338
321
|
|
|
322
|
+
### queryWorkouts(...)
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
queryWorkouts(options: QueryWorkoutsOptions) => Promise<QueryWorkoutsResult>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Queries workout sessions from the native health store on Android (Health Connect).
|
|
329
|
+
|
|
330
|
+
| Param | Type | Description |
|
|
331
|
+
| ------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
332
|
+
| **`options`** | <code><a href="#queryworkoutsoptions">QueryWorkoutsOptions</a></code> | Query options including optional workout type filter, date range, limit, and sort order |
|
|
333
|
+
|
|
334
|
+
**Returns:** <code>Promise<<a href="#queryworkoutsresult">QueryWorkoutsResult</a>></code>
|
|
335
|
+
|
|
336
|
+
--------------------
|
|
337
|
+
|
|
338
|
+
|
|
339
339
|
### querySleep(...)
|
|
340
340
|
|
|
341
341
|
```typescript
|
|
@@ -399,35 +399,35 @@ Queries hydration records from the native health store on Android (Health Connec
|
|
|
399
399
|
| **`read`** | <code>ReadAuthorizationType[]</code> | Data types that should be readable after authorization. |
|
|
400
400
|
|
|
401
401
|
|
|
402
|
-
####
|
|
402
|
+
#### QueryWorkoutsResult
|
|
403
403
|
|
|
404
|
-
| Prop
|
|
405
|
-
|
|
|
406
|
-
| **`
|
|
404
|
+
| Prop | Type |
|
|
405
|
+
| -------------- | ---------------------- |
|
|
406
|
+
| **`workouts`** | <code>Workout[]</code> |
|
|
407
407
|
|
|
408
408
|
|
|
409
|
-
####
|
|
409
|
+
#### Workout
|
|
410
410
|
|
|
411
|
-
| Prop
|
|
412
|
-
|
|
|
413
|
-
| **`
|
|
414
|
-
| **`
|
|
415
|
-
| **`
|
|
416
|
-
| **`
|
|
417
|
-
| **`
|
|
418
|
-
| **`
|
|
419
|
-
| **`
|
|
411
|
+
| Prop | Type | Description |
|
|
412
|
+
| ----------------- | --------------------------------------------------------------- | -------------------------------------- |
|
|
413
|
+
| **`workoutType`** | <code><a href="#workouttype">WorkoutType</a></code> | The type of workout. |
|
|
414
|
+
| **`duration`** | <code>number</code> | Duration of the workout in seconds. |
|
|
415
|
+
| **`startDate`** | <code>string</code> | ISO 8601 start date of the workout. |
|
|
416
|
+
| **`endDate`** | <code>string</code> | ISO 8601 end date of the workout. |
|
|
417
|
+
| **`sourceName`** | <code>string</code> | Source name that recorded the workout. |
|
|
418
|
+
| **`sourceId`** | <code>string</code> | Source bundle identifier. |
|
|
419
|
+
| **`metadata`** | <code><a href="#record">Record</a><string, string></code> | Additional metadata (if available). |
|
|
420
420
|
|
|
421
421
|
|
|
422
|
-
####
|
|
422
|
+
#### QueryWorkoutsOptions
|
|
423
423
|
|
|
424
|
-
| Prop
|
|
425
|
-
|
|
|
426
|
-
| **`
|
|
427
|
-
| **`startDate`**
|
|
428
|
-
| **`endDate`**
|
|
429
|
-
| **`limit`**
|
|
430
|
-
| **`ascending`**
|
|
424
|
+
| Prop | Type | Description |
|
|
425
|
+
| ----------------- | --------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
426
|
+
| **`workoutType`** | <code><a href="#workouttype">WorkoutType</a></code> | Optional workout type filter. If omitted, all workout types are returned. |
|
|
427
|
+
| **`startDate`** | <code>string</code> | Inclusive ISO 8601 start date (defaults to now - 1 day). |
|
|
428
|
+
| **`endDate`** | <code>string</code> | Exclusive ISO 8601 end date (defaults to now). |
|
|
429
|
+
| **`limit`** | <code>number</code> | Maximum number of workouts to return (defaults to 100). |
|
|
430
|
+
| **`ascending`** | <code>boolean</code> | Return results sorted ascending by start date (defaults to false). |
|
|
431
431
|
|
|
432
432
|
|
|
433
433
|
#### QuerySleepResult
|
|
@@ -505,33 +505,29 @@ Queries hydration records from the native health store on Android (Health Connec
|
|
|
505
505
|
#### ReadAuthorizationType
|
|
506
506
|
|
|
507
507
|
Data types that can be requested for read authorization.
|
|
508
|
+
Includes 'workouts' for querying workout sessions via queryWorkouts().
|
|
508
509
|
Includes 'sleep' for querying sleep sessions via querySleep().
|
|
509
510
|
Includes 'hydration' for querying hydration records via queryHydration().
|
|
510
511
|
|
|
511
|
-
<code
|
|
512
|
+
<code>'workouts' | 'sleep' | 'hydration'</code>
|
|
512
513
|
|
|
513
514
|
|
|
514
|
-
####
|
|
515
|
+
#### WorkoutType
|
|
515
516
|
|
|
516
|
-
<code>'
|
|
517
|
+
<code>'running' | 'cycling' | 'walking' | 'swimming' | 'yoga' | 'strengthTraining' | 'hiking' | 'tennis' | 'basketball' | 'soccer' | 'americanFootball' | 'baseball' | 'crossTraining' | 'elliptical' | 'rowing' | 'stairClimbing' | 'traditionalStrengthTraining' | 'waterFitness' | 'waterPolo' | 'waterSports' | 'wrestling' | 'other'</code>
|
|
517
518
|
|
|
518
519
|
|
|
519
|
-
####
|
|
520
|
+
#### Record
|
|
521
|
+
|
|
522
|
+
Construct a type with a set of properties K of type T
|
|
520
523
|
|
|
521
|
-
<code>
|
|
524
|
+
<code>{
|
|
522
525
|
[P in K]: T;
|
|
523
526
|
}</code>
|
|
524
527
|
|
|
525
528
|
|
|
526
529
|
#### SleepStage
|
|
527
530
|
|
|
528
531
|
<code>'unknown' | 'awake' | 'sleeping' | 'outOfBed' | 'awakeInBed' | 'light' | 'deep' | 'rem'</code>
|
|
529
532
|
|
|
530
|
-
|
|
531
|
-
#### Record
|
|
532
|
-
|
|
533
|
-
Construct a type with a set of properties K of type T
|
|
534
|
-
|
|
535
|
-
<code>{
|
|
536
533
|
[P in K]: T;
|
|
537
534
|
}</code>
|
|
538
|
-
|
|
539
535
|
</docgen-api>
|
|
540
536
|
|
|
541
537
|
### Credits:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
2
|
<uses-permission android:name="android.permission.health.READ_SLEEP" />
|
|
3
3
|
<uses-permission android:name="android.permission.health.READ_HYDRATION" />
|
|
4
|
-
<uses-permission android:name="android.permission.health.
|
|
4
|
+
<uses-permission android:name="android.permission.health.READ_EXERCISE" />
|
|
5
|
+
|
|
5
6
|
<!-- Query for Health Connect availability -->
|
|
6
7
|
<queries>
|
|
7
8
|
<package android:name="com.google.android.apps.healthdata" />
|
|
@@ -2,19 +2,15 @@ package app.capgo.plugin.health
|
|
|
2
2
|
|
|
3
3
|
import androidx.health.connect.client.HealthConnectClient
|
|
4
4
|
import androidx.health.connect.client.permission.HealthPermission
|
|
5
|
+
import androidx.health.connect.client.records.ExerciseSessionRecord
|
|
5
6
|
import androidx.health.connect.client.records.HydrationRecord
|
|
6
7
|
import androidx.health.connect.client.records.SleepSessionRecord
|
|
7
|
-
import androidx.health.connect.client.records.StepsRecord
|
|
8
|
-
import androidx.health.connect.client.records.Record
|
|
9
8
|
import androidx.health.connect.client.request.ReadRecordsRequest
|
|
10
9
|
import androidx.health.connect.client.time.TimeRangeFilter
|
|
11
|
-
import androidx.health.connect.client.records.metadata.Metadata
|
|
12
10
|
import java.time.Duration
|
|
13
11
|
import com.getcapacitor.JSArray
|
|
14
12
|
import com.getcapacitor.JSObject
|
|
15
13
|
import java.time.Instant
|
|
16
|
-
import java.time.ZoneId
|
|
17
|
-
import java.time.ZoneOffset
|
|
18
14
|
import java.time.format.DateTimeFormatter
|
|
19
15
|
import kotlin.math.min
|
|
20
16
|
import kotlin.collections.buildSet
|
|
@@ -23,8 +19,11 @@ class HealthManager {
|
|
|
23
19
|
|
|
24
20
|
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_INSTANT
|
|
25
21
|
|
|
26
|
-
fun permissionsFor(
|
|
27
|
-
|
|
22
|
+
fun permissionsFor(includeWorkouts: Boolean = false, includeSleep: Boolean = false, includeHydration: Boolean = false): Set<String> = buildSet {
|
|
23
|
+
// Include workout read permission if explicitly requested
|
|
24
|
+
if (includeWorkouts) {
|
|
25
|
+
add(HealthPermission.getReadPermission(ExerciseSessionRecord::class))
|
|
26
|
+
}
|
|
28
27
|
// Include sleep read permission if explicitly requested
|
|
29
28
|
if (includeSleep) {
|
|
30
29
|
add(HealthPermission.getReadPermission(SleepSessionRecord::class))
|
|
@@ -37,7 +36,7 @@ class HealthManager {
|
|
|
37
36
|
|
|
38
37
|
suspend fun authorizationStatus(
|
|
39
38
|
client: HealthConnectClient,
|
|
40
|
-
|
|
39
|
+
includeWorkouts: Boolean = false,
|
|
41
40
|
includeSleep: Boolean = false,
|
|
42
41
|
includeHydration: Boolean = false
|
|
43
42
|
): JSObject {
|
|
@@ -46,11 +45,13 @@ class HealthManager {
|
|
|
46
45
|
val readAuthorized = JSArray()
|
|
47
46
|
val readDenied = JSArray()
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
// Check workout permission if requested
|
|
49
|
+
if (includeWorkouts) {
|
|
50
|
+
val workoutPermission = HealthPermission.getReadPermission(ExerciseSessionRecord::class)
|
|
51
|
+
if (granted.contains(workoutPermission)) {
|
|
52
|
+
readAuthorized.put("workouts")
|
|
52
53
|
} else {
|
|
53
|
-
readDenied.put(
|
|
54
|
+
readDenied.put("workouts")
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -86,83 +87,87 @@ class HealthManager {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
startTime: Instant,
|
|
93
|
-
endTime: Instant,
|
|
94
|
-
limit: Int,
|
|
95
|
-
ascending: Boolean
|
|
96
|
-
): JSArray {
|
|
97
|
-
val samples = mutableListOf<Pair<Instant, JSObject>>()
|
|
98
|
-
when (dataType) {
|
|
99
|
-
HealthDataType.STEPS -> readRecords(client, StepsRecord::class, startTime, endTime, limit) { record ->
|
|
100
|
-
val payload = createSamplePayload(
|
|
101
|
-
dataType,
|
|
102
|
-
record.startTime,
|
|
103
|
-
record.endTime,
|
|
104
|
-
record.count.toDouble(),
|
|
105
|
-
record.metadata
|
|
106
|
-
)
|
|
107
|
-
samples.add(record.startTime to payload)
|
|
108
|
-
}
|
|
90
|
+
fun parseInstant(value: String?, defaultInstant: Instant): Instant {
|
|
91
|
+
if (value.isNullOrBlank()) {
|
|
92
|
+
return defaultInstant
|
|
109
93
|
}
|
|
94
|
+
return Instant.parse(value)
|
|
95
|
+
}
|
|
110
96
|
|
|
111
|
-
val sorted = samples.sortedBy { it.first }
|
|
112
|
-
val ordered = if (ascending) sorted else sorted.asReversed()
|
|
113
|
-
val limited = if (limit > 0) ordered.take(limit) else ordered
|
|
114
97
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return array
|
|
98
|
+
private fun zoneOffset(instant: Instant): ZoneOffset? {
|
|
99
|
+
return ZoneId.systemDefault().rules.getOffset(instant)
|
|
118
100
|
}
|
|
119
101
|
|
|
120
|
-
|
|
102
|
+
suspend fun queryWorkouts(
|
|
121
103
|
client: HealthConnectClient,
|
|
122
|
-
|
|
104
|
+
workoutType: String?,
|
|
123
105
|
startTime: Instant,
|
|
124
106
|
endTime: Instant,
|
|
125
107
|
limit: Int,
|
|
126
|
-
|
|
127
|
-
) {
|
|
108
|
+
ascending: Boolean
|
|
109
|
+
): JSArray {
|
|
110
|
+
val workouts = mutableListOf<Pair<Instant, JSObject>>()
|
|
111
|
+
|
|
128
112
|
var pageToken: String? = null
|
|
129
113
|
val pageSize = if (limit > 0) min(limit, MAX_PAGE_SIZE) else DEFAULT_PAGE_SIZE
|
|
130
114
|
var fetched = 0
|
|
131
115
|
|
|
116
|
+
val exerciseTypeFilter = WorkoutType.fromString(workoutType)
|
|
117
|
+
|
|
132
118
|
do {
|
|
133
119
|
val request = ReadRecordsRequest(
|
|
134
|
-
recordType =
|
|
120
|
+
recordType = ExerciseSessionRecord::class,
|
|
135
121
|
timeRangeFilter = TimeRangeFilter.between(startTime, endTime),
|
|
136
122
|
pageSize = pageSize,
|
|
137
123
|
pageToken = pageToken
|
|
138
124
|
)
|
|
139
125
|
val response = client.readRecords(request)
|
|
126
|
+
|
|
140
127
|
response.records.forEach { record ->
|
|
141
|
-
|
|
128
|
+
val session = record as ExerciseSessionRecord
|
|
129
|
+
|
|
130
|
+
// Filter by exercise type if specified
|
|
131
|
+
if (exerciseTypeFilter != null && session.exerciseType != exerciseTypeFilter) {
|
|
132
|
+
return@forEach
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
val payload = createWorkoutPayload(session)
|
|
136
|
+
workouts.add(session.startTime to payload)
|
|
142
137
|
}
|
|
138
|
+
|
|
143
139
|
fetched += response.records.size
|
|
144
140
|
pageToken = response.pageToken
|
|
145
141
|
} while (pageToken != null && (limit <= 0 || fetched < limit))
|
|
142
|
+
|
|
143
|
+
val sorted = workouts.sortedBy { it.first }
|
|
144
|
+
val ordered = if (ascending) sorted else sorted.asReversed()
|
|
145
|
+
val limited = if (limit > 0) ordered.take(limit) else ordered
|
|
146
|
+
|
|
147
|
+
val array = JSArray()
|
|
148
|
+
limited.forEach { array.put(it.second) }
|
|
149
|
+
return array
|
|
146
150
|
}
|
|
147
151
|
|
|
148
|
-
private fun
|
|
149
|
-
dataType: HealthDataType,
|
|
150
|
-
startTime: Instant,
|
|
151
|
-
endTime: Instant,
|
|
152
|
-
value: Double,
|
|
153
|
-
metadata: Metadata
|
|
154
|
-
): JSObject {
|
|
152
|
+
private fun createWorkoutPayload(session: ExerciseSessionRecord): JSObject {
|
|
155
153
|
val payload = JSObject()
|
|
156
|
-
payload.put("dataType", dataType.identifier)
|
|
157
|
-
payload.put("value", value)
|
|
158
|
-
payload.put("unit", dataType.unit)
|
|
159
|
-
payload.put("startDate", formatter.format(startTime))
|
|
160
|
-
payload.put("endDate", formatter.format(endTime))
|
|
161
154
|
|
|
162
|
-
|
|
155
|
+
// Workout type
|
|
156
|
+
payload.put("workoutType", WorkoutType.toWorkoutTypeString(session.exerciseType))
|
|
157
|
+
|
|
158
|
+
// Duration in seconds
|
|
159
|
+
val durationSeconds = Duration.between(session.startTime, session.endTime).seconds.toInt()
|
|
160
|
+
payload.put("duration", durationSeconds)
|
|
161
|
+
|
|
162
|
+
// Start and end dates
|
|
163
|
+
payload.put("startDate", formatter.format(session.startTime))
|
|
164
|
+
payload.put("endDate", formatter.format(session.endTime))
|
|
165
|
+
|
|
166
|
+
// Source information
|
|
167
|
+
val dataOrigin = session.metadata.dataOrigin
|
|
163
168
|
payload.put("sourceId", dataOrigin.packageName)
|
|
164
169
|
payload.put("sourceName", dataOrigin.packageName)
|
|
165
|
-
metadata.device?.let { device ->
|
|
170
|
+
session.metadata.device?.let { device ->
|
|
166
171
|
val manufacturer = device.manufacturer?.takeIf { it.isNotBlank() }
|
|
167
172
|
val model = device.model?.takeIf { it.isNotBlank() }
|
|
168
173
|
val label = listOfNotNull(manufacturer, model).joinToString(" ").trim()
|
|
@@ -174,19 +179,6 @@ class HealthManager {
|
|
|
174
179
|
return payload
|
|
175
180
|
}
|
|
176
181
|
|
|
177
|
-
fun parseInstant(value: String?, defaultInstant: Instant): Instant {
|
|
178
|
-
if (value.isNullOrBlank()) {
|
|
179
|
-
return defaultInstant
|
|
180
|
-
}
|
|
181
|
-
return Instant.parse(value)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
private fun zoneOffset(instant: Instant): ZoneOffset? {
|
|
186
|
-
return ZoneId.systemDefault().rules.getOffset(instant)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
182
|
suspend fun querySleep(
|
|
191
183
|
client: HealthConnectClient,
|
|
192
184
|
startTime: Instant,
|
|
@@ -23,7 +23,7 @@ import kotlinx.coroutines.cancel
|
|
|
23
23
|
import kotlinx.coroutines.launch
|
|
24
24
|
|
|
25
25
|
data class ReadAuthorizationTypes(
|
|
26
|
-
val
|
|
26
|
+
val includeWorkouts: Boolean,
|
|
27
27
|
val includeSleep: Boolean,
|
|
28
28
|
val includeHydration: Boolean
|
|
29
29
|
)
|
|
@@ -36,7 +36,7 @@ class HealthPlugin : Plugin() {
|
|
|
36
36
|
private val permissionContract = PermissionController.createRequestPermissionResultContract()
|
|
37
37
|
|
|
38
38
|
// Store pending request data for callback
|
|
39
|
-
private var
|
|
39
|
+
private var pendingIncludeWorkouts: Boolean = false
|
|
40
40
|
private var pendingIncludeSleep: Boolean = false
|
|
41
41
|
private var pendingIncludeHydration: Boolean = false
|
|
42
42
|
|
|
@@ -63,7 +63,7 @@ class HealthPlugin : Plugin() {
|
|
|
63
63
|
pluginScope.launch {
|
|
64
64
|
val client = getClientOrReject(call) ?: return@launch
|
|
65
65
|
val permissions = manager.permissionsFor(
|
|
66
|
-
readAuth.
|
|
66
|
+
readAuth.includeWorkouts,
|
|
67
67
|
readAuth.includeSleep,
|
|
68
68
|
readAuth.includeHydration
|
|
69
69
|
)
|
|
@@ -71,7 +71,7 @@ class HealthPlugin : Plugin() {
|
|
|
71
71
|
if (permissions.isEmpty()) {
|
|
72
72
|
val status = manager.authorizationStatus(
|
|
73
73
|
client,
|
|
74
|
-
readAuth.
|
|
74
|
+
readAuth.includeWorkouts,
|
|
75
75
|
readAuth.includeSleep,
|
|
76
76
|
readAuth.includeHydration
|
|
77
77
|
)
|
|
@@ -83,7 +83,7 @@ class HealthPlugin : Plugin() {
|
|
|
83
83
|
if (granted.containsAll(permissions)) {
|
|
84
84
|
val status = manager.authorizationStatus(
|
|
85
85
|
client,
|
|
86
|
-
readAuth.
|
|
86
|
+
readAuth.includeWorkouts,
|
|
87
87
|
readAuth.includeSleep,
|
|
88
88
|
readAuth.includeHydration
|
|
89
89
|
)
|
|
@@ -92,7 +92,7 @@ class HealthPlugin : Plugin() {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// Store types for callback
|
|
95
|
-
|
|
95
|
+
pendingIncludeWorkouts = readAuth.includeWorkouts
|
|
96
96
|
pendingIncludeSleep = readAuth.includeSleep
|
|
97
97
|
pendingIncludeHydration = readAuth.includeHydration
|
|
98
98
|
|
|
@@ -113,16 +113,16 @@ class HealthPlugin : Plugin() {
|
|
|
113
113
|
return
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
val
|
|
116
|
+
val includeWorkouts = pendingIncludeWorkouts
|
|
117
117
|
val includeSleep = pendingIncludeSleep
|
|
118
118
|
val includeHydration = pendingIncludeHydration
|
|
119
|
-
|
|
119
|
+
pendingIncludeWorkouts = false
|
|
120
120
|
pendingIncludeSleep = false
|
|
121
121
|
pendingIncludeHydration = false
|
|
122
122
|
|
|
123
123
|
pluginScope.launch {
|
|
124
124
|
val client = getClientOrReject(call) ?: return@launch
|
|
125
|
-
val status = manager.authorizationStatus(client,
|
|
125
|
+
val status = manager.authorizationStatus(client, includeWorkouts, includeSleep, includeHydration)
|
|
126
126
|
call.resolve(status)
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -140,7 +140,7 @@ class HealthPlugin : Plugin() {
|
|
|
140
140
|
val client = getClientOrReject(call) ?: return@launch
|
|
141
141
|
val status = manager.authorizationStatus(
|
|
142
142
|
client,
|
|
143
|
-
readAuth.
|
|
143
|
+
readAuth.includeWorkouts,
|
|
144
144
|
readAuth.includeSleep,
|
|
145
145
|
readAuth.includeHydration
|
|
146
146
|
)
|
|
@@ -148,72 +148,22 @@ class HealthPlugin : Plugin() {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
@PluginMethod
|
|
152
|
-
fun readSamples(call: PluginCall) {
|
|
153
|
-
val identifier = call.getString("dataType")
|
|
154
|
-
if (identifier.isNullOrBlank()) {
|
|
155
|
-
call.reject("dataType is required")
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
val dataType = HealthDataType.from(identifier)
|
|
160
|
-
if (dataType == null) {
|
|
161
|
-
call.reject("Unsupported data type: $identifier")
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
val limit = (call.getInt("limit") ?: DEFAULT_LIMIT).coerceAtLeast(0)
|
|
166
|
-
val ascending = call.getBoolean("ascending") ?: false
|
|
167
|
-
|
|
168
|
-
val startInstant = try {
|
|
169
|
-
manager.parseInstant(call.getString("startDate"), Instant.now().minus(DEFAULT_PAST_DURATION))
|
|
170
|
-
} catch (e: DateTimeParseException) {
|
|
171
|
-
call.reject(e.message, null, e)
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
val endInstant = try {
|
|
176
|
-
manager.parseInstant(call.getString("endDate"), Instant.now())
|
|
177
|
-
} catch (e: DateTimeParseException) {
|
|
178
|
-
call.reject(e.message, null, e)
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (endInstant.isBefore(startInstant)) {
|
|
183
|
-
call.reject("endDate must be greater than or equal to startDate")
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
pluginScope.launch {
|
|
188
|
-
val client = getClientOrReject(call) ?: return@launch
|
|
189
|
-
try {
|
|
190
|
-
val samples = manager.readSamples(client, dataType, startInstant, endInstant, limit, ascending)
|
|
191
|
-
val result = JSObject().apply { put("samples", samples) }
|
|
192
|
-
call.resolve(result)
|
|
193
|
-
} catch (e: Exception) {
|
|
194
|
-
call.reject(e.message ?: "Failed to read samples.", null, e)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
151
|
|
|
199
152
|
private fun parseReadAuthorizationTypes(call: PluginCall, key: String): ReadAuthorizationTypes {
|
|
200
153
|
val array = call.getArray(key) ?: JSArray()
|
|
201
|
-
|
|
154
|
+
var includeWorkouts = false
|
|
202
155
|
var includeSleep = false
|
|
203
156
|
var includeHydration = false
|
|
204
157
|
for (i in 0 until array.length()) {
|
|
205
158
|
val identifier = array.optString(i, null) ?: continue
|
|
206
159
|
when (identifier) {
|
|
160
|
+
"workouts" -> includeWorkouts = true
|
|
207
161
|
"sleep" -> includeSleep = true
|
|
208
162
|
"hydration" -> includeHydration = true
|
|
209
|
-
else ->
|
|
210
|
-
val dataType = HealthDataType.from(identifier)
|
|
211
|
-
?: throw IllegalArgumentException("Unsupported data type: $identifier")
|
|
212
|
-
result.add(dataType)
|
|
213
|
-
}
|
|
163
|
+
else -> throw IllegalArgumentException("Unsupported data type: $identifier")
|
|
214
164
|
}
|
|
215
165
|
}
|
|
216
|
-
return ReadAuthorizationTypes(
|
|
166
|
+
return ReadAuthorizationTypes(includeWorkouts, includeSleep, includeHydration)
|
|
217
167
|
}
|
|
218
168
|
|
|
219
169
|
private fun getClientOrReject(call: PluginCall): HealthConnectClient? {
|
|
@@ -278,6 +228,43 @@ class HealthPlugin : Plugin() {
|
|
|
278
228
|
}
|
|
279
229
|
}
|
|
280
230
|
|
|
231
|
+
@PluginMethod
|
|
232
|
+
fun queryWorkouts(call: PluginCall) {
|
|
233
|
+
val workoutType = call.getString("workoutType")
|
|
234
|
+
val limit = (call.getInt("limit") ?: DEFAULT_LIMIT).coerceAtLeast(0)
|
|
235
|
+
val ascending = call.getBoolean("ascending") ?: false
|
|
236
|
+
|
|
237
|
+
val startInstant = try {
|
|
238
|
+
manager.parseInstant(call.getString("startDate"), Instant.now().minus(DEFAULT_PAST_DURATION))
|
|
239
|
+
} catch (e: DateTimeParseException) {
|
|
240
|
+
call.reject(e.message, null, e)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
val endInstant = try {
|
|
245
|
+
manager.parseInstant(call.getString("endDate"), Instant.now())
|
|
246
|
+
} catch (e: DateTimeParseException) {
|
|
247
|
+
call.reject(e.message, null, e)
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (endInstant.isBefore(startInstant)) {
|
|
252
|
+
call.reject("endDate must be greater than or equal to startDate")
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
pluginScope.launch {
|
|
257
|
+
val client = getClientOrReject(call) ?: return@launch
|
|
258
|
+
try {
|
|
259
|
+
val workouts = manager.queryWorkouts(client, workoutType, startInstant, endInstant, limit, ascending)
|
|
260
|
+
val result = JSObject().apply { put("workouts", workouts) }
|
|
261
|
+
call.resolve(result)
|
|
262
|
+
} catch (e: Exception) {
|
|
263
|
+
call.reject(e.message ?: "Failed to query workouts.", null, e)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
281
268
|
@PluginMethod
|
|
282
269
|
fun querySleep(call: PluginCall) {
|
|
283
270
|
val limit = (call.getInt("limit") ?: DEFAULT_LIMIT).coerceAtLeast(0)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package app.capgo.plugin.health
|
|
2
|
+
|
|
3
|
+
import androidx.health.connect.client.records.ExerciseSessionRecord
|
|
4
|
+
|
|
5
|
+
object WorkoutType {
|
|
6
|
+
fun fromString(type: String?): Int? {
|
|
7
|
+
if (type.isNullOrBlank()) return null
|
|
8
|
+
|
|
9
|
+
return when (type) {
|
|
10
|
+
"running" -> ExerciseSessionRecord.EXERCISE_TYPE_RUNNING
|
|
11
|
+
"cycling" -> ExerciseSessionRecord.EXERCISE_TYPE_BIKING
|
|
12
|
+
"walking" -> ExerciseSessionRecord.EXERCISE_TYPE_WALKING
|
|
13
|
+
"swimming" -> ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_POOL
|
|
14
|
+
"yoga" -> ExerciseSessionRecord.EXERCISE_TYPE_YOGA
|
|
15
|
+
"strengthTraining" -> ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING
|
|
16
|
+
"hiking" -> ExerciseSessionRecord.EXERCISE_TYPE_HIKING
|
|
17
|
+
"tennis" -> ExerciseSessionRecord.EXERCISE_TYPE_TENNIS
|
|
18
|
+
"basketball" -> ExerciseSessionRecord.EXERCISE_TYPE_BASKETBALL
|
|
19
|
+
"soccer" -> ExerciseSessionRecord.EXERCISE_TYPE_SOCCER
|
|
20
|
+
"americanFootball" -> ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_AMERICAN
|
|
21
|
+
"baseball" -> ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL
|
|
22
|
+
"crossTraining" -> ExerciseSessionRecord.EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING
|
|
23
|
+
"elliptical" -> ExerciseSessionRecord.EXERCISE_TYPE_ELLIPTICAL
|
|
24
|
+
"rowing" -> ExerciseSessionRecord.EXERCISE_TYPE_ROWING
|
|
25
|
+
"stairClimbing" -> ExerciseSessionRecord.EXERCISE_TYPE_STAIR_CLIMBING
|
|
26
|
+
"traditionalStrengthTraining" -> ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING
|
|
27
|
+
"waterFitness" -> ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_POOL
|
|
28
|
+
"waterPolo" -> ExerciseSessionRecord.EXERCISE_TYPE_WATER_POLO
|
|
29
|
+
"waterSports" -> ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_OPEN_WATER
|
|
30
|
+
"wrestling" -> ExerciseSessionRecord.EXERCISE_TYPE_MARTIAL_ARTS
|
|
31
|
+
"other" -> ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT
|
|
32
|
+
else -> null
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fun toWorkoutTypeString(exerciseType: Int): String {
|
|
37
|
+
return when (exerciseType) {
|
|
38
|
+
ExerciseSessionRecord.EXERCISE_TYPE_RUNNING -> "running"
|
|
39
|
+
ExerciseSessionRecord.EXERCISE_TYPE_BIKING -> "cycling"
|
|
40
|
+
ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY -> "cycling"
|
|
41
|
+
ExerciseSessionRecord.EXERCISE_TYPE_WALKING -> "walking"
|
|
42
|
+
ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_POOL -> "swimming"
|
|
43
|
+
ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_OPEN_WATER -> "swimming"
|
|
44
|
+
ExerciseSessionRecord.EXERCISE_TYPE_YOGA -> "yoga"
|
|
45
|
+
ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING -> "strengthTraining"
|
|
46
|
+
ExerciseSessionRecord.EXERCISE_TYPE_HIKING -> "hiking"
|
|
47
|
+
ExerciseSessionRecord.EXERCISE_TYPE_TENNIS -> "tennis"
|
|
48
|
+
ExerciseSessionRecord.EXERCISE_TYPE_BASKETBALL -> "basketball"
|
|
49
|
+
ExerciseSessionRecord.EXERCISE_TYPE_SOCCER -> "soccer"
|
|
50
|
+
ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_AMERICAN -> "americanFootball"
|
|
51
|
+
ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL -> "baseball"
|
|
52
|
+
ExerciseSessionRecord.EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING -> "crossTraining"
|
|
53
|
+
ExerciseSessionRecord.EXERCISE_TYPE_ELLIPTICAL -> "elliptical"
|
|
54
|
+
ExerciseSessionRecord.EXERCISE_TYPE_ROWING -> "rowing"
|
|
55
|
+
ExerciseSessionRecord.EXERCISE_TYPE_ROWING_MACHINE -> "rowing"
|
|
56
|
+
ExerciseSessionRecord.EXERCISE_TYPE_STAIR_CLIMBING -> "stairClimbing"
|
|
57
|
+
ExerciseSessionRecord.EXERCISE_TYPE_STAIR_CLIMBING_MACHINE -> "stairClimbing"
|
|
58
|
+
ExerciseSessionRecord.EXERCISE_TYPE_WATER_POLO -> "waterPolo"
|
|
59
|
+
ExerciseSessionRecord.EXERCISE_TYPE_MARTIAL_ARTS -> "wrestling"
|
|
60
|
+
ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT -> "other"
|
|
61
|
+
else -> "other"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|