@asiriindatissa/capacitor-health 8.4.2 → 8.4.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
@@ -162,6 +162,52 @@ const { workouts } = await Health.queryWorkouts({
162
162
  - Workout energy and distance data are aggregated from separate Health Connect records during the workout time period. If you don't request permissions for `calories`/`totalCalories` and `distance`, these fields will be missing from workout results.
163
163
  - If `totalEnergyBurned` or `totalDistance` are missing despite having permissions, it means no calorie or distance data was recorded during that workout period in Health Connect.
164
164
 
165
+ ### Sleep
166
+
167
+ To query sleep sessions, you need to request read permission for `'sleep'`:
168
+
169
+ ```ts
170
+ // Request permission to read sleep data
171
+ await Health.requestAuthorization({
172
+ read: ['sleep'],
173
+ write: [],
174
+ });
175
+
176
+ // Query recent sleep sessions
177
+ const { sleepSessions } = await Health.querySleep({
178
+ startDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
179
+ endDate: new Date().toISOString(),
180
+ limit: 10,
181
+ });
182
+
183
+ // Each sleep session includes:
184
+ // - duration (in seconds), startDate, endDate (always present)
185
+ // - title (optional, if provided by the source app)
186
+ // - stages (optional array of sleep stage records if available)
187
+ // - sourceName, sourceId (source app information)
188
+
189
+ // Sleep stages can be: 'unknown', 'awake', 'sleeping', 'outOfBed',
190
+ // 'awakeInBed', 'light', 'deep', 'rem'
191
+ ```
192
+
193
+ **Supported Sleep Stages:**
194
+
195
+ | Stage | Description |
196
+ | ------------ | ---------------------------------------------- |
197
+ | `unknown` | Unspecified or unknown if the user is sleeping |
198
+ | `awake` | The user is awake within a sleep cycle |
199
+ | `sleeping` | Generic or non-granular sleep description |
200
+ | `outOfBed` | The user gets out of bed during sleep session |
201
+ | `awakeInBed` | The user is awake in bed |
202
+ | `light` | Light sleep cycle |
203
+ | `deep` | Deep sleep cycle |
204
+ | `rem` | REM sleep cycle |
205
+
206
+ **Note:**
207
+
208
+ - `'sleep'` is a special read-only permission type. You cannot write sleep data with this plugin.
209
+ - Not all sleep sessions include detailed sleep stages. Some apps may only record the overall sleep duration.
210
+
165
211
  ## API
166
212
 
167
213
  <docgen-index>
@@ -175,6 +221,8 @@ const { workouts } = await Health.queryWorkouts({
175
221
  * [`openHealthConnectSettings()`](#openhealthconnectsettings)
176
222
  * [`showPrivacyPolicy()`](#showprivacypolicy)
177
223
  * [`queryWorkouts(...)`](#queryworkouts)
224
+ * [`querySleep(...)`](#querysleep)
225
+ * [`queryHydration(...)`](#queryhydration)
178
226
  * [Interfaces](#interfaces)
179
227
  * [Type Aliases](#type-aliases)
180
228
 
@@ -324,6 +372,40 @@ Queries workout sessions from the native health store on Android (Health Connect
324
372
  --------------------
325
373
 
326
374
 
375
+ ### querySleep(...)
376
+
377
+ ```typescript
378
+ querySleep(options: QuerySleepOptions) => Promise<QuerySleepResult>
379
+ ```
380
+
381
+ Queries sleep sessions from the native health store on Android (Health Connect).
382
+
383
+ | Param | Type | Description |
384
+ | ------------- | --------------------------------------------------------------- | --------------------------------------------------------- |
385
+ | **`options`** | <code><a href="#querysleepoptions">QuerySleepOptions</a></code> | Query options including date range, limit, and sort order |
386
+
387
+ **Returns:** <code>Promise&lt;<a href="#querysleepresult">QuerySleepResult</a>&gt;</code>
388
+
389
+ --------------------
390
+
391
+
392
+ ### queryHydration(...)
393
+
394
+ ```typescript
395
+ queryHydration(options: QueryHydrationOptions) => Promise<QueryHydrationResult>
396
+ ```
397
+
398
+ Queries hydration records from the native health store on Android (Health Connect).
399
+
400
+ | Param | Type | Description |
401
+ | ------------- | ----------------------------------------------------------------------- | --------------------------------------------------------- |
402
+ | **`options`** | <code><a href="#queryhydrationoptions">QueryHydrationOptions</a></code> | Query options including date range, limit, and sort order |
403
+
404
+ **Returns:** <code>Promise&lt;<a href="#queryhydrationresult">QueryHydrationResult</a>&gt;</code>
405
+
406
+ --------------------
407
+
408
+
327
409
  ### Interfaces
328
410
 
329
411
 
@@ -430,6 +512,75 @@ Queries workout sessions from the native health store on Android (Health Connect
430
512
  | **`ascending`** | <code>boolean</code> | Return results sorted ascending by start date (defaults to false). |
431
513
 
432
514
 
515
+ #### QuerySleepResult
516
+
517
+ | Prop | Type |
518
+ | ------------------- | --------------------------- |
519
+ | **`sleepSessions`** | <code>SleepSession[]</code> |
520
+
521
+
522
+ #### SleepSession
523
+
524
+ | Prop | Type | Description |
525
+ | ---------------- | --------------------------------------------------------------- | -------------------------------------------------------- |
526
+ | **`title`** | <code>string</code> | Title/name of the sleep session (if available). |
527
+ | **`duration`** | <code>number</code> | Duration of the sleep session in seconds. |
528
+ | **`startDate`** | <code>string</code> | ISO 8601 start date of the sleep session. |
529
+ | **`endDate`** | <code>string</code> | ISO 8601 end date of the sleep session. |
530
+ | **`stages`** | <code>SleepStageRecord[]</code> | Array of sleep stages during the session (if available). |
531
+ | **`sourceName`** | <code>string</code> | Source name that recorded the sleep session. |
532
+ | **`sourceId`** | <code>string</code> | Source bundle identifier. |
533
+ | **`metadata`** | <code><a href="#record">Record</a>&lt;string, string&gt;</code> | Additional metadata (if available). |
534
+
535
+
536
+ #### SleepStageRecord
537
+
538
+ | Prop | Type | Description |
539
+ | --------------- | ------------------------------------------------- | ---------------------------------------- |
540
+ | **`stage`** | <code><a href="#sleepstage">SleepStage</a></code> | The sleep stage type. |
541
+ | **`startDate`** | <code>string</code> | ISO 8601 start date of this sleep stage. |
542
+ | **`endDate`** | <code>string</code> | ISO 8601 end date of this sleep stage. |
543
+
544
+
545
+ #### QuerySleepOptions
546
+
547
+ | Prop | Type | Description |
548
+ | --------------- | -------------------- | ------------------------------------------------------------------ |
549
+ | **`startDate`** | <code>string</code> | Inclusive ISO 8601 start date (defaults to now - 1 day). |
550
+ | **`endDate`** | <code>string</code> | Exclusive ISO 8601 end date (defaults to now). |
551
+ | **`limit`** | <code>number</code> | Maximum number of sleep sessions to return (defaults to 100). |
552
+ | **`ascending`** | <code>boolean</code> | Return results sorted ascending by start date (defaults to false). |
553
+
554
+
555
+ #### QueryHydrationResult
556
+
557
+ | Prop | Type |
558
+ | ---------------------- | ------------------------------ |
559
+ | **`hydrationRecords`** | <code>HydrationRecord[]</code> |
560
+
561
+
562
+ #### HydrationRecord
563
+
564
+ | Prop | Type | Description |
565
+ | ---------------- | --------------------------------------------------------------- | -------------------------------------------- |
566
+ | **`volume`** | <code>number</code> | Volume of water consumed in liters. |
567
+ | **`startDate`** | <code>string</code> | ISO 8601 start date of the hydration record. |
568
+ | **`endDate`** | <code>string</code> | ISO 8601 end date of the hydration record. |
569
+ | **`sourceName`** | <code>string</code> | Source name that recorded the hydration. |
570
+ | **`sourceId`** | <code>string</code> | Source bundle identifier. |
571
+ | **`metadata`** | <code><a href="#record">Record</a>&lt;string, string&gt;</code> | Additional metadata (if available). |
572
+
573
+
574
+ #### QueryHydrationOptions
575
+
576
+ | Prop | Type | Description |
577
+ | --------------- | -------------------- | ------------------------------------------------------------------ |
578
+ | **`startDate`** | <code>string</code> | Inclusive ISO 8601 start date (defaults to now - 1 day). |
579
+ | **`endDate`** | <code>string</code> | Exclusive ISO 8601 end date (defaults to now). |
580
+ | **`limit`** | <code>number</code> | Maximum number of hydration records to return (defaults to 100). |
581
+ | **`ascending`** | <code>boolean</code> | Return results sorted ascending by start date (defaults to false). |
582
+
583
+
433
584
  ### Type Aliases
434
585
 
435
586
 
@@ -437,8 +588,10 @@ Queries workout sessions from the native health store on Android (Health Connect
437
588
 
438
589
  Data types that can be requested for read authorization.
439
590
  Includes 'workouts' for querying workout sessions via queryWorkouts().
591
+ Includes 'sleep' for querying sleep sessions via querySleep().
592
+ Includes 'hydration' for querying hydration records via queryHydration().
440
593
 
441
- <code><a href="#healthdatatype">HealthDataType</a> | 'workouts'</code>
594
+ <code><a href="#healthdatatype">HealthDataType</a> | 'workouts' | 'sleep' | 'hydration'</code>
442
595
 
443
596
 
444
597
  #### HealthDataType
@@ -462,6 +615,11 @@ Construct a type with a set of properties K of type T
462
615
 
463
616
  <code>'running' | 'cycling' | 'walking' | 'swimming' | 'yoga' | 'strengthTraining' | 'hiking' | 'tennis' | 'basketball' | 'soccer' | 'americanFootball' | 'baseball' | 'crossTraining' | 'elliptical' | 'rowing' | 'stairClimbing' | 'traditionalStrengthTraining' | 'waterFitness' | 'waterPolo' | 'waterSports' | 'wrestling' | 'other'</code>
464
617
 
618
+
619
+ #### SleepStage
620
+
621
+ <code>'unknown' | 'awake' | 'sleeping' | 'outOfBed' | 'awakeInBed' | 'light' | 'deep' | 'rem'</code>
622
+
465
623
  </docgen-api>
466
624
 
467
625
  ### Credits:
@@ -13,6 +13,10 @@
13
13
  <uses-permission android:name="android.permission.health.READ_HEIGHT" />
14
14
  <uses-permission android:name="android.permission.health.WRITE_HEIGHT" />
15
15
  <uses-permission android:name="android.permission.health.READ_EXERCISE" />
16
+ <uses-permission android:name="android.permission.health.READ_SLEEP" />
17
+ <uses-permission android:name="android.permission.health.WRITE_SLEEP" />
18
+ <uses-permission android:name="android.permission.health.READ_HYDRATION" />
19
+ <uses-permission android:name="android.permission.health.WRITE_HYDRATION" />
16
20
 
17
21
  <!-- Query for Health Connect availability -->
18
22
  <queries>
@@ -8,7 +8,9 @@ import androidx.health.connect.client.records.DistanceRecord
8
8
  import androidx.health.connect.client.records.ExerciseSessionRecord
9
9
  import androidx.health.connect.client.records.HeartRateRecord
10
10
  import androidx.health.connect.client.records.HeightRecord
11
+ import androidx.health.connect.client.records.HydrationRecord
11
12
  import androidx.health.connect.client.records.Record
13
+ import androidx.health.connect.client.records.SleepSessionRecord
12
14
  import androidx.health.connect.client.records.StepsRecord
13
15
  import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
14
16
  import androidx.health.connect.client.records.WeightRecord
@@ -17,6 +19,7 @@ import androidx.health.connect.client.time.TimeRangeFilter
17
19
  import androidx.health.connect.client.units.Energy
18
20
  import androidx.health.connect.client.units.Length
19
21
  import androidx.health.connect.client.units.Mass
22
+ import androidx.health.connect.client.units.Volume
20
23
  import androidx.health.connect.client.records.metadata.Metadata
21
24
  import java.time.Duration
22
25
  import com.getcapacitor.JSArray
@@ -32,20 +35,30 @@ class HealthManager {
32
35
 
33
36
  private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_INSTANT
34
37
 
35
- fun permissionsFor(readTypes: Collection<HealthDataType>, writeTypes: Collection<HealthDataType>, includeWorkouts: Boolean = false): Set<String> = buildSet {
38
+ fun permissionsFor(readTypes: Collection<HealthDataType>, writeTypes: Collection<HealthDataType>, includeWorkouts: Boolean = false, includeSleep: Boolean = false, includeHydration: Boolean = false): Set<String> = buildSet {
36
39
  readTypes.forEach { add(it.readPermission) }
37
40
  writeTypes.forEach { add(it.writePermission) }
38
41
  // Include workout read permission if explicitly requested
39
42
  if (includeWorkouts) {
40
43
  add(HealthPermission.getReadPermission(ExerciseSessionRecord::class))
41
44
  }
45
+ // Include sleep read permission if explicitly requested
46
+ if (includeSleep) {
47
+ add(HealthPermission.getReadPermission(SleepSessionRecord::class))
48
+ }
49
+ // Include hydration read permission if explicitly requested
50
+ if (includeHydration) {
51
+ add(HealthPermission.getReadPermission(HydrationRecord::class))
52
+ }
42
53
  }
43
54
 
44
55
  suspend fun authorizationStatus(
45
56
  client: HealthConnectClient,
46
57
  readTypes: Collection<HealthDataType>,
47
58
  writeTypes: Collection<HealthDataType>,
48
- includeWorkouts: Boolean = false
59
+ includeWorkouts: Boolean = false,
60
+ includeSleep: Boolean = false,
61
+ includeHydration: Boolean = false
49
62
  ): JSObject {
50
63
  val granted = client.permissionController.getGrantedPermissions()
51
64
 
@@ -69,6 +82,26 @@ class HealthManager {
69
82
  }
70
83
  }
71
84
 
85
+ // Check sleep permission if requested
86
+ if (includeSleep) {
87
+ val sleepPermission = HealthPermission.getReadPermission(SleepSessionRecord::class)
88
+ if (granted.contains(sleepPermission)) {
89
+ readAuthorized.put("sleep")
90
+ } else {
91
+ readDenied.put("sleep")
92
+ }
93
+ }
94
+
95
+ // Check hydration permission if requested
96
+ if (includeHydration) {
97
+ val hydrationPermission = HealthPermission.getReadPermission(HydrationRecord::class)
98
+ if (granted.contains(hydrationPermission)) {
99
+ readAuthorized.put("hydration")
100
+ } else {
101
+ readDenied.put("hydration")
102
+ }
103
+ }
104
+
72
105
  val writeAuthorized = JSArray()
73
106
  val writeDenied = JSArray()
74
107
  writeTypes.forEach { type ->
@@ -501,6 +534,188 @@ class HealthManager {
501
534
  return payload
502
535
  }
503
536
 
537
+ suspend fun querySleep(
538
+ client: HealthConnectClient,
539
+ startTime: Instant,
540
+ endTime: Instant,
541
+ limit: Int,
542
+ ascending: Boolean
543
+ ): JSArray {
544
+ val sleepSessions = mutableListOf<Pair<Instant, JSObject>>()
545
+
546
+ var pageToken: String? = null
547
+ val pageSize = if (limit > 0) min(limit, MAX_PAGE_SIZE) else DEFAULT_PAGE_SIZE
548
+ var fetched = 0
549
+
550
+ do {
551
+ val request = ReadRecordsRequest(
552
+ recordType = SleepSessionRecord::class,
553
+ timeRangeFilter = TimeRangeFilter.between(startTime, endTime),
554
+ pageSize = pageSize,
555
+ pageToken = pageToken
556
+ )
557
+ val response = client.readRecords(request)
558
+
559
+ response.records.forEach { record ->
560
+ val session = record as SleepSessionRecord
561
+ val payload = createSleepPayload(session)
562
+ sleepSessions.add(session.startTime to payload)
563
+ }
564
+
565
+ fetched += response.records.size
566
+ pageToken = response.pageToken
567
+ } while (pageToken != null && (limit <= 0 || fetched < limit))
568
+
569
+ val sorted = sleepSessions.sortedBy { it.first }
570
+ val ordered = if (ascending) sorted else sorted.asReversed()
571
+ val limited = if (limit > 0) ordered.take(limit) else ordered
572
+
573
+ val array = JSArray()
574
+ limited.forEach { array.put(it.second) }
575
+ return array
576
+ }
577
+
578
+ private fun createSleepPayload(session: SleepSessionRecord): JSObject {
579
+ val payload = JSObject()
580
+
581
+ // Title if available
582
+ session.title?.let { title ->
583
+ if (title.isNotBlank()) {
584
+ payload.put("title", title)
585
+ }
586
+ }
587
+
588
+ // Duration in seconds
589
+ val durationSeconds = Duration.between(session.startTime, session.endTime).seconds.toInt()
590
+ payload.put("duration", durationSeconds)
591
+
592
+ // Start and end dates
593
+ payload.put("startDate", formatter.format(session.startTime))
594
+ payload.put("endDate", formatter.format(session.endTime))
595
+
596
+ // Sleep stages if available
597
+ if (session.stages.isNotEmpty()) {
598
+ val stagesArray = JSArray()
599
+ session.stages.forEach { stage ->
600
+ val stageObject = JSObject()
601
+ stageObject.put("stage", sleepStageToString(stage.stage))
602
+ stageObject.put("startDate", formatter.format(stage.startTime))
603
+ stageObject.put("endDate", formatter.format(stage.endTime))
604
+ stagesArray.put(stageObject)
605
+ }
606
+ payload.put("stages", stagesArray)
607
+ }
608
+
609
+ // Source information
610
+ val dataOrigin = session.metadata.dataOrigin
611
+ payload.put("sourceId", dataOrigin.packageName)
612
+ payload.put("sourceName", dataOrigin.packageName)
613
+ session.metadata.device?.let { device ->
614
+ val manufacturer = device.manufacturer?.takeIf { it.isNotBlank() }
615
+ val model = device.model?.takeIf { it.isNotBlank() }
616
+ val label = listOfNotNull(manufacturer, model).joinToString(" ").trim()
617
+ if (label.isNotEmpty()) {
618
+ payload.put("sourceName", label)
619
+ }
620
+ }
621
+
622
+ return payload
623
+ }
624
+
625
+ private fun sleepStageToString(stage: Int): String {
626
+ return when (stage) {
627
+ SleepSessionRecord.STAGE_TYPE_UNKNOWN -> "unknown"
628
+ SleepSessionRecord.STAGE_TYPE_AWAKE -> "awake"
629
+ SleepSessionRecord.STAGE_TYPE_SLEEPING -> "sleeping"
630
+ SleepSessionRecord.STAGE_TYPE_OUT_OF_BED -> "outOfBed"
631
+ SleepSessionRecord.STAGE_TYPE_LIGHT -> "light"
632
+ SleepSessionRecord.STAGE_TYPE_DEEP -> "deep"
633
+ SleepSessionRecord.STAGE_TYPE_REM -> "rem"
634
+ SleepSessionRecord.STAGE_TYPE_AWAKE_IN_BED -> "awakeInBed"
635
+ else -> "unknown"
636
+ }
637
+ }
638
+
639
+ suspend fun queryHydration(
640
+ client: HealthConnectClient,
641
+ startTime: Instant,
642
+ endTime: Instant,
643
+ limit: Int,
644
+ ascending: Boolean
645
+ ): JSArray {
646
+ val hydrationRecords = mutableListOf<Pair<Instant, JSObject>>()
647
+
648
+ var pageToken: String? = null
649
+ val pageSize = if (limit > 0) min(limit, MAX_PAGE_SIZE) else DEFAULT_PAGE_SIZE
650
+ var fetched = 0
651
+
652
+ do {
653
+ val request = ReadRecordsRequest(
654
+ recordType = HydrationRecord::class,
655
+ timeRangeFilter = TimeRangeFilter.between(startTime, endTime),
656
+ pageSize = pageSize,
657
+ pageToken = pageToken
658
+ )
659
+ val response = client.readRecords(request)
660
+
661
+ response.records.forEach { record ->
662
+ val hydration = record as HydrationRecord
663
+ val payload = createHydrationPayload(hydration)
664
+ hydrationRecords.add(hydration.startTime to payload)
665
+ }
666
+
667
+ fetched += response.records.size
668
+ pageToken = response.pageToken
669
+ } while (pageToken != null && (limit <= 0 || fetched < limit))
670
+
671
+ val sorted = hydrationRecords.sortedBy { it.first }
672
+ val ordered = if (ascending) sorted else sorted.asReversed()
673
+ val limited = if (limit > 0) ordered.take(limit) else ordered
674
+
675
+ val array = JSArray()
676
+ limited.forEach { array.put(it.second) }
677
+ return array
678
+ }
679
+
680
+ private fun createHydrationPayload(hydration: HydrationRecord): JSObject {
681
+ val payload = JSObject()
682
+
683
+ // Volume in liters
684
+ val volumeLiters = hydration.volume.inLiters
685
+ payload.put("volume", volumeLiters)
686
+
687
+ // Start and end dates
688
+ payload.put("startDate", formatter.format(hydration.startTime))
689
+ payload.put("endDate", formatter.format(hydration.endTime))
690
+
691
+ // Source information
692
+ val dataOrigin = hydration.metadata.dataOrigin
693
+ payload.put("sourceId", dataOrigin.packageName)
694
+ payload.put("sourceName", dataOrigin.packageName)
695
+ hydration.metadata.device?.let { device ->
696
+ val manufacturer = device.manufacturer?.takeIf { it.isNotBlank() }
697
+ val model = device.model?.takeIf { it.isNotBlank() }
698
+ val label = listOfNotNull(manufacturer, model).joinToString(" ").trim()
699
+ if (label.isNotEmpty()) {
700
+ payload.put("sourceName", label)
701
+ }
702
+ }
703
+
704
+ // Metadata if available
705
+ if (hydration.metadata.clientRecordId != null || hydration.metadata.clientRecordVersion > 0) {
706
+ val metadataObj = JSObject()
707
+ hydration.metadata.clientRecordId?.let { metadataObj.put("clientRecordId", it) }
708
+ if (hydration.metadata.clientRecordVersion > 0) {
709
+ metadataObj.put("clientRecordVersion", hydration.metadata.clientRecordVersion)
710
+ }
711
+ if (metadataObj.length() > 0) {
712
+ payload.put("metadata", metadataObj)
713
+ }
714
+ }
715
+
716
+ return payload
717
+ }
718
+
504
719
  companion object {
505
720
  private const val DEFAULT_PAGE_SIZE = 100
506
721
  private const val MAX_PAGE_SIZE = 500