@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 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&lt;<a href="#readsamplesresult">ReadSamplesResult</a>&gt;</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&lt;<a href="#queryworkoutsresult">QueryWorkoutsResult</a>&gt;</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
- #### ReadSamplesResult
402
+ #### QueryWorkoutsResult
403
403
 
404
- | Prop | Type |
405
- | ------------- | --------------------------- |
406
- | **`samples`** | <code>HealthSample[]</code> |
404
+ | Prop | Type |
405
+ | -------------- | ---------------------- |
406
+ | **`workouts`** | <code>Workout[]</code> |
407
407
 
408
408
 
409
- #### HealthSample
409
+ #### Workout
410
410
 
411
- | Prop | Type |
412
- | ---------------- | --------------------------------------------------------- |
413
- | **`dataType`** | <code><a href="#healthdatatype">HealthDataType</a></code> |
414
- | **`value`** | <code>number</code> |
415
- | **`unit`** | <code><a href="#healthunit">HealthUnit</a></code> |
416
- | **`startDate`** | <code>string</code> |
417
- | **`endDate`** | <code>string</code> |
418
- | **`sourceName`** | <code>string</code> |
419
- | **`sourceId`** | <code>string</code> |
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>&lt;string, string&gt;</code> | Additional metadata (if available). |
420
420
 
421
421
 
422
- #### QueryOptions
422
+ #### QueryWorkoutsOptions
423
423
 
424
- | Prop | Type | Description |
425
- | --------------- | --------------------------------------------------------- | ------------------------------------------------------------------ |
426
- | **`dataType`** | <code><a href="#healthdatatype">HealthDataType</a></code> | The type of data to retrieve from the health store. |
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 samples to return (defaults to 100). |
430
- | **`ascending`** | <code>boolean</code> | Return results sorted ascending by start date (defaults to false). |
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><a href="#healthdatatype">HealthDataType</a> | 'sleep' | 'hydration'</code>
512
+ <code>'workouts' | 'sleep' | 'hydration'</code>
512
513
 
513
514
 
514
- #### HealthDataType
515
+ #### WorkoutType
515
516
 
516
- <code>'steps'</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
- #### HealthUnit
520
+ #### Record
521
+
522
+ Construct a type with a set of properties K of type T
520
523
 
521
- <code>'count'</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.READ_STEPS" />
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(readTypes: Collection<HealthDataType>, includeSleep: Boolean = false, includeHydration: Boolean = false): Set<String> = buildSet {
27
- readTypes.forEach { add(it.readPermission) }
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
- readTypes: Collection<HealthDataType>,
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
- readTypes.forEach { type ->
50
- if (granted.contains(type.readPermission)) {
51
- readAuthorized.put(type.identifier)
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(type.identifier)
54
+ readDenied.put("workouts")
54
55
  }
55
56
  }
56
57
 
@@ -86,83 +87,87 @@ class HealthManager {
86
87
  }
87
88
 
88
89
 
89
- suspend fun readSamples(
90
- client: HealthConnectClient,
91
- dataType: HealthDataType,
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
- val array = JSArray()
116
- limited.forEach { array.put(it.second) }
117
- return array
98
+ private fun zoneOffset(instant: Instant): ZoneOffset? {
99
+ return ZoneId.systemDefault().rules.getOffset(instant)
118
100
  }
119
101
 
120
- private suspend fun <T : Record> readRecords(
102
+ suspend fun queryWorkouts(
121
103
  client: HealthConnectClient,
122
- recordClass: kotlin.reflect.KClass<T>,
104
+ workoutType: String?,
123
105
  startTime: Instant,
124
106
  endTime: Instant,
125
107
  limit: Int,
126
- consumer: (record: T) -> Unit
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 = recordClass,
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
- consumer(record)
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 createSamplePayload(
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
- val dataOrigin = metadata.dataOrigin
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 dataTypes: List<HealthDataType>,
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 pendingReadTypes: List<HealthDataType> = emptyList()
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.dataTypes,
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.dataTypes,
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.dataTypes,
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
- pendingReadTypes = readAuth.dataTypes
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 readTypes = pendingReadTypes
116
+ val includeWorkouts = pendingIncludeWorkouts
117
117
  val includeSleep = pendingIncludeSleep
118
118
  val includeHydration = pendingIncludeHydration
119
- pendingReadTypes = emptyList()
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, readTypes, includeSleep, includeHydration)
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.dataTypes,
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
- val result = mutableListOf<HealthDataType>()
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(result, includeSleep, includeHydration)
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
+ }