@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### New features
|
|
6
|
+
|
|
7
|
+
- Initial Expo Modules API wrapper for Apple HealthKit
|
|
8
|
+
- Android Health Connect backend behind the same identifier-based JS API
|
|
9
|
+
- Generic quantity, category, workout, statistics, and anchored queries
|
|
10
|
+
- ECG, activity rings, clinical records, audiograms, and workout GPS route queries
|
|
11
|
+
- Blood pressure / food correlations (`queryCorrelations`, `saveCorrelation`) and heartbeat series
|
|
12
|
+
- Authorization, characteristics, observers, and background delivery
|
|
13
|
+
- Config plugin for HealthKit entitlements, usage descriptions, Health Connect permissions, and Android `minSdk` 26
|
|
14
|
+
|
|
15
|
+
### Fixes
|
|
16
|
+
|
|
17
|
+
- Request Health Connect permissions via the Android 14+ runtime permission APIs instead of `startActivityForResult`, which throws `ActivityNotFoundException`
|
|
18
|
+
- Convert HealthKit `NSException` crashes (invalid predicates, missing calendars) into `ERR_HEALTHKIT_NATIVE` promise rejections so the JS app can show the error instead of aborting
|
|
19
|
+
|
|
20
|
+
### Packaging
|
|
21
|
+
|
|
22
|
+
- Published as [`@appeeky/expo-healthkit`](https://www.npmjs.com/package/@appeeky/expo-healthkit)
|
|
23
|
+
- npm-ready `exports`, `.npmignore`, and `prepublishOnly` build
|
|
24
|
+
- App Store privacy manifest (`PrivacyInfo.xcprivacy`)
|
|
25
|
+
- GitHub Actions for lint, typecheck, tests with coverage thresholds, and pack contents
|
|
26
|
+
- [PolyForm Shield 1.0.0](./LICENSE) (source-available; apps may use the SDK, competing SDKs may not)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# PolyForm Shield License 1.0.0
|
|
2
|
+
|
|
3
|
+
https://polyformproject.org/licenses/shield/1.0.0
|
|
4
|
+
|
|
5
|
+
Required Notice: Copyright 2026 Erencan Arica (https://github.com/Eronred)
|
|
6
|
+
Licensor Line of Business: expo-healthkit HealthKit and Health Connect SDK for Expo and React Native (https://github.com/appeeky/expo-healthkit)
|
|
7
|
+
|
|
8
|
+
## Acceptance
|
|
9
|
+
|
|
10
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
11
|
+
|
|
12
|
+
## Copyright License
|
|
13
|
+
|
|
14
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
15
|
+
|
|
16
|
+
## Distribution License
|
|
17
|
+
|
|
18
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
19
|
+
|
|
20
|
+
## Notices
|
|
21
|
+
|
|
22
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
23
|
+
|
|
24
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
25
|
+
|
|
26
|
+
## Changes and New Works License
|
|
27
|
+
|
|
28
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
29
|
+
|
|
30
|
+
## Patent License
|
|
31
|
+
|
|
32
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
33
|
+
|
|
34
|
+
## Noncompete
|
|
35
|
+
|
|
36
|
+
Any purpose is a permitted purpose, except for providing any product that competes with the software or any product the licensor or any of its affiliates provides using the software.
|
|
37
|
+
|
|
38
|
+
## Competition
|
|
39
|
+
|
|
40
|
+
Goods and services compete even when they provide functionality through different kinds of interfaces or for different technical platforms. Applications can compete with services, libraries with plugins, frameworks with development tools, and so on, even if they're written in different programming languages or for different computer architectures. Goods and services compete even when provided free of charge. If you market a product as a practical substitute for the software or another product, it definitely competes.
|
|
41
|
+
|
|
42
|
+
## New Products
|
|
43
|
+
|
|
44
|
+
If you are using the software to provide a product that does not compete, but the licensor or any of its affiliates brings your product into competition by providing a new version of the software or another product using the software, you may continue using versions of the software available under these terms beforehand to provide your competing product, but not any later versions.
|
|
45
|
+
|
|
46
|
+
## Discontinued Products
|
|
47
|
+
|
|
48
|
+
You may begin using the software to compete with a product or service that the licensor or any of its affiliates has stopped providing, unless the licensor includes a plain-text line beginning with `Licensor Line of Business:` with the software that mentions that line of business. For example:
|
|
49
|
+
|
|
50
|
+
> Licensor Line of Business: YoyodyneCMS Content Management System (http://example.com/cms)
|
|
51
|
+
|
|
52
|
+
## Sales of Business
|
|
53
|
+
|
|
54
|
+
If the licensor or any of its affiliates sells a line of business developing the software or using the software to provide a product, the buyer can also enforce [Noncompete](#noncompete) for that product.
|
|
55
|
+
|
|
56
|
+
## Fair Use
|
|
57
|
+
|
|
58
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
59
|
+
|
|
60
|
+
## No Other Rights
|
|
61
|
+
|
|
62
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
63
|
+
|
|
64
|
+
## Patent Defense
|
|
65
|
+
|
|
66
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
67
|
+
|
|
68
|
+
## Violations
|
|
69
|
+
|
|
70
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
71
|
+
|
|
72
|
+
## No Liability
|
|
73
|
+
|
|
74
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
75
|
+
|
|
76
|
+
## Definitions
|
|
77
|
+
|
|
78
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
79
|
+
|
|
80
|
+
A **product** can be a good or service, or a combination of them.
|
|
81
|
+
|
|
82
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
83
|
+
|
|
84
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all its affiliates.
|
|
85
|
+
|
|
86
|
+
**Affiliates** means the other organizations than an organization has control over, is under the control of, or is under common control with.
|
|
87
|
+
|
|
88
|
+
**Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
89
|
+
|
|
90
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
91
|
+
|
|
92
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
package/README.md
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# @appeeky/expo-healthkit
|
|
4
|
+
|
|
5
|
+
Apple [HealthKit](https://developer.apple.com/documentation/healthkit) and Android [Health Connect](https://developer.android.com/health-and-fitness/guides/health-connect) for [Expo](https://expo.dev) and React Native, built on the [Expo Modules API](https://docs.expo.dev/modules/overview/).
|
|
6
|
+
|
|
7
|
+
One JavaScript API on both platforms. You pass Apple HealthKit type identifiers (`HKQuantityTypeIdentifierStepCount`, …), query or save samples, and the native layer talks to HealthKit on iOS and Health Connect on Android.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
| | iOS | Android |
|
|
11
|
+
| ------------ | ----------------------------------------- | ------------------------------------------------ |
|
|
12
|
+
| Backend | HealthKit | Health Connect |
|
|
13
|
+
| App code | Same identifiers and methods | Same identifiers and methods |
|
|
14
|
+
| Availability | `isAvailable()` when HealthKit is present | `isAvailable()` when Health Connect is installed |
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
- Expo SDK 53+ / New Architecture (tested on SDK 57)
|
|
18
|
+
- Promise-based TypeScript API
|
|
19
|
+
- Config plugin for HealthKit entitlements **and** Health Connect permissions (`minSdk` 26)
|
|
20
|
+
- iOS 16.4+, Android 8+ (API 26+). Web reports `isAvailable() === false`
|
|
21
|
+
|
|
22
|
+
> Health data APIs cannot run in Expo Go. Use a development build (`npx expo run:ios` or `npx expo run:android`).
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## 📋 Requirements
|
|
27
|
+
|
|
28
|
+
- Expo SDK 53 or later and a **development build** (not Expo Go)
|
|
29
|
+
- iOS 16.4+, physical iPhone or Simulator, with the HealthKit capability enabled on the App ID in the [Apple Developer portal](https://developer.apple.com/account)
|
|
30
|
+
- Android 8+ with [Health Connect](https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata) (built-in on Android 14+)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## 📦 Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npx expo install @appeeky/expo-healthkit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
From GitHub, if you are tracking `main` before a release:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npx expo install github:appeeky/expo-healthkit
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Add the config plugin in `app.json` / `app.config.ts`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"expo": {
|
|
51
|
+
"plugins": [
|
|
52
|
+
[
|
|
53
|
+
"@appeeky/expo-healthkit",
|
|
54
|
+
{
|
|
55
|
+
"healthSharePermission": "Allow $(PRODUCT_NAME) to read your health data",
|
|
56
|
+
"healthUpdatePermission": "Allow $(PRODUCT_NAME) to write health data",
|
|
57
|
+
"isBackgroundDeliveryEnabled": true
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
],
|
|
61
|
+
"ios": {
|
|
62
|
+
"bundleIdentifier": "com.example.app"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then rebuild native code:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npx expo prebuild --clean
|
|
72
|
+
npx expo run:ios
|
|
73
|
+
# or
|
|
74
|
+
npx expo run:android
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Enable the **HealthKit** capability on the App ID in the Apple Developer portal. The plugin writes the entitlement into the generated Xcode project; the capability still has to be allowed for that bundle ID.
|
|
78
|
+
|
|
79
|
+
On Android, install [Health Connect](https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata) if the OS does not already include it (built-in on Android 14+). The plugin declares the Health Connect permissions and raises `minSdk` to 26.
|
|
80
|
+
|
|
81
|
+
## ⚡️ Quick start
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import * as HealthKit from '@appeeky/expo-healthkit';
|
|
85
|
+
|
|
86
|
+
if (!HealthKit.isAvailable()) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await HealthKit.requestAuthorization({
|
|
91
|
+
toRead: [
|
|
92
|
+
HealthKit.QuantityType.stepCount,
|
|
93
|
+
HealthKit.QuantityType.heartRate,
|
|
94
|
+
HealthKit.CategoryType.sleepAnalysis,
|
|
95
|
+
HealthKit.WorkoutType.workout,
|
|
96
|
+
],
|
|
97
|
+
toShare: [HealthKit.QuantityType.stepCount],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const today = new Date();
|
|
101
|
+
today.setHours(0, 0, 0, 0);
|
|
102
|
+
|
|
103
|
+
const steps = await HealthKit.queryStatistics({
|
|
104
|
+
type: HealthKit.QuantityType.stepCount,
|
|
105
|
+
unit: HealthKit.Unit.count,
|
|
106
|
+
from: today,
|
|
107
|
+
to: new Date(),
|
|
108
|
+
options: HealthKit.StatisticsOption.cumulativeSum,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const heartRate = await HealthKit.queryQuantitySamples({
|
|
112
|
+
type: HealthKit.QuantityType.heartRate,
|
|
113
|
+
unit: HealthKit.Unit.countPerMinute,
|
|
114
|
+
from: today,
|
|
115
|
+
to: new Date(),
|
|
116
|
+
limit: 20,
|
|
117
|
+
ascending: false,
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## 📚 API
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
### 🔐 Availability and permissions
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
| Method | Notes |
|
|
131
|
+
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
132
|
+
| `isAvailable()` | `true` on iOS when HealthKit is present, and on Android when Health Connect is installed |
|
|
133
|
+
| `requestAuthorization({ toRead, toShare })` | Shows the system permission sheet (HealthKit or Health Connect) |
|
|
134
|
+
| `getAuthorizationStatus(type)` | iOS: reliable for **write** types. Android: reflects granted Health Connect read or write access |
|
|
135
|
+
| `getRequestStatusForAuthorization({ toRead, toShare })` | Whether you still need to prompt |
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
On iOS, read authorization is intentionally opaque: `getAuthorizationStatus` returning `notDetermined` / `sharingDenied` does **not** mean the user blocked reads.
|
|
139
|
+
|
|
140
|
+
### 📊 Samples and statistics
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
| | Method | Use for |
|
|
144
|
+
| --- | ------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
145
|
+
| ❤️ | `queryQuantitySamples({ type, unit, from, to, limit, ascending })` | Heart rate, weight, glucose, … |
|
|
146
|
+
| 😴 | `queryCategorySamples({ type, from, to, limit, ascending })` | Sleep, mindful sessions, … |
|
|
147
|
+
| 🏃 | `queryWorkouts({ from, to, limit, ascending, activityType })` | Workouts |
|
|
148
|
+
| 🫀 | `queryElectrocardiograms({ from, to, limit, ascending, includeVoltage })` | ECG samples. Voltage is omitted unless `includeVoltage: true` |
|
|
149
|
+
| 🔴 | `queryActivitySummaries({ from, to })` | Move / Exercise / Stand rings |
|
|
150
|
+
| 🏥 | `queryClinicalRecords({ type, from, to, limit, ascending })` | FHIR clinical records. Needs `isClinicalDataEnabled` |
|
|
151
|
+
| 👂 | `queryAudiograms({ from, to, limit, ascending })` | Hearing-test audiograms |
|
|
152
|
+
| 📍 | `queryWorkoutRoute({ workoutUUID })` | GPS locations for a workout |
|
|
153
|
+
| 🩺 | `queryCorrelations({ type, from, to, limit, ascending })` | Blood pressure and food correlations |
|
|
154
|
+
| 💓 | `queryHeartbeatSeries({ from, to, limit, ascending, includeBeats })` | Beat-to-beat series used for HRV |
|
|
155
|
+
| 📈 | `queryStatistics({ type, unit, from, to, options })` | Totals / min / max / average for a range |
|
|
156
|
+
| 📅 | `queryStatisticsCollection({ type, unit, from, to, interval, options })` | Daily (or hourly) buckets |
|
|
157
|
+
| 🔄 | `queryAnchored({ type, unit, from, to, limit, anchor })` | Incremental sync. Store `anchor` and pass it back |
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
`limit` defaults to HealthKit's unlimited query. Dates accept `Date` or ISO-8601 strings. Units are HealthKit unit strings (`count`, `count/min`, `kcal`, `kg`, `m`, `%`, …) also exported as `Unit`.
|
|
161
|
+
|
|
162
|
+
ECG, activity rings, clinical records, audiograms, workout GPS routes, heartbeat series, and correlations are not quantity/category/workout samples. Request the matching identifier in `toRead`:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
await HealthKit.requestAuthorization({
|
|
166
|
+
toRead: [
|
|
167
|
+
HealthKit.ElectrocardiogramType.electrocardiogram,
|
|
168
|
+
HealthKit.ActivitySummaryType.activitySummary,
|
|
169
|
+
HealthKit.ClinicalType.allergyRecord,
|
|
170
|
+
HealthKit.AudiogramType.audiogram,
|
|
171
|
+
HealthKit.WorkoutType.workout,
|
|
172
|
+
HealthKit.SeriesType.workoutRoute,
|
|
173
|
+
HealthKit.SeriesType.heartbeat,
|
|
174
|
+
HealthKit.CorrelationType.bloodPressure,
|
|
175
|
+
HealthKit.CorrelationType.food,
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const rings = await HealthKit.queryActivitySummaries({ from: today });
|
|
180
|
+
const [workout] = await HealthKit.queryWorkouts({ limit: 1 });
|
|
181
|
+
const routes = workout
|
|
182
|
+
? await HealthKit.queryWorkoutRoute({ workoutUUID: workout.uuid })
|
|
183
|
+
: [];
|
|
184
|
+
const pressure = await HealthKit.queryCorrelations({
|
|
185
|
+
type: HealthKit.CorrelationType.bloodPressure,
|
|
186
|
+
limit: 20,
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Clinical reads also need `isClinicalDataEnabled: true` on the config plugin.
|
|
191
|
+
|
|
192
|
+
### ✍️ Write / delete
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
await HealthKit.saveQuantitySample({
|
|
196
|
+
type: HealthKit.QuantityType.stepCount,
|
|
197
|
+
unit: HealthKit.Unit.count,
|
|
198
|
+
value: 100,
|
|
199
|
+
startDate: new Date(Date.now() - 60_000),
|
|
200
|
+
endDate: new Date(),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await HealthKit.saveCategorySample({
|
|
204
|
+
type: HealthKit.CategoryType.sleepAnalysis,
|
|
205
|
+
value: HealthKit.SleepAnalysisValue.asleepCore,
|
|
206
|
+
startDate,
|
|
207
|
+
endDate,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await HealthKit.saveWorkout({
|
|
211
|
+
activityType: HealthKit.WorkoutActivityType.running,
|
|
212
|
+
startDate,
|
|
213
|
+
endDate,
|
|
214
|
+
energyBurned: 400,
|
|
215
|
+
energyBurnedUnit: HealthKit.Unit.kilocalorie,
|
|
216
|
+
distance: 5000,
|
|
217
|
+
distanceUnit: HealthKit.Unit.meter,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await HealthKit.saveCorrelation({
|
|
221
|
+
type: HealthKit.CorrelationType.bloodPressure,
|
|
222
|
+
startDate: new Date(),
|
|
223
|
+
objects: [
|
|
224
|
+
{
|
|
225
|
+
type: HealthKit.QuantityType.bloodPressureSystolic,
|
|
226
|
+
unit: HealthKit.Unit.millimeterOfMercury,
|
|
227
|
+
value: 120,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
type: HealthKit.QuantityType.bloodPressureDiastolic,
|
|
231
|
+
unit: HealthKit.Unit.millimeterOfMercury,
|
|
232
|
+
value: 80,
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await HealthKit.deleteObjects({
|
|
238
|
+
type: HealthKit.QuantityType.stepCount,
|
|
239
|
+
uuid,
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
### 👤 Characteristics
|
|
246
|
+
|
|
247
|
+
`getBiologicalSex()`, `getBloodType()`, `getDateOfBirth()`, `getFitzpatrickSkinType()`, `getWheelchairUse()`. These are read-only HealthKit characteristics; include the matching `CharacteristicType` in `toRead`.
|
|
248
|
+
|
|
249
|
+
### 🔔 Background updates
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
await HealthKit.observe([HealthKit.QuantityType.stepCount]);
|
|
253
|
+
|
|
254
|
+
const subscription = HealthKit.addUpdateListener(({ type }) => {
|
|
255
|
+
// Re-query that type
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await HealthKit.enableBackgroundDelivery(
|
|
259
|
+
HealthKit.QuantityType.stepCount,
|
|
260
|
+
HealthKit.UpdateFrequency.hourly
|
|
261
|
+
);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`useHealthKitUpdates()` is a React hook around the same event.
|
|
265
|
+
|
|
266
|
+
Background delivery needs `isBackgroundDeliveryEnabled: true` on the config plugin and the HealthKit background mode.
|
|
267
|
+
|
|
268
|
+
## 🏷️ Identifiers
|
|
269
|
+
|
|
270
|
+
Pass Apple's raw identifier strings. Named constants are provided for autocomplete; unknown identifiers still work if HealthKit knows them.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
HealthKit.QuantityType.stepCount
|
|
274
|
+
// 'HKQuantityTypeIdentifierStepCount'
|
|
275
|
+
|
|
276
|
+
HealthKit.queryQuantitySamples({
|
|
277
|
+
type: 'HKQuantityTypeIdentifierStepCount',
|
|
278
|
+
unit: 'count',
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
App code should keep using Apple identifier strings on both platforms. See [Cross-platform](#cross-platform) for what Health Connect maps, what stays iOS-only, and how to handle those APIs without `Platform.OS` forks.
|
|
283
|
+
|
|
284
|
+
## 🌍 Cross-platform
|
|
285
|
+
|
|
286
|
+
JavaScript always speaks HealthKit. You never pass Health Connect record class names from app code. The native layer chooses the backend:
|
|
287
|
+
|
|
288
|
+
```mermaid
|
|
289
|
+
flowchart LR
|
|
290
|
+
App["App JS<br/>QuantityType.stepCount"] --> API["@appeeky/expo-healthkit"]
|
|
291
|
+
API --> iOS["iOS<br/>HealthKit"]
|
|
292
|
+
API --> Android["Android<br/>Health Connect"]
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Mapped types (steps, heart rate, sleep, workouts, weight, blood pressure, nutrition, …) use the **same method and identifier** on both platforms. Unmapped identifiers are skipped in Android `requestAuthorization`. Calling an Apple-only method on Android throws `ERR_HEALTH_CONNECT_UNSUPPORTED` instead of requiring `if (Platform.OS === 'ios')` on every query.
|
|
296
|
+
|
|
297
|
+
Hide Apple-only UI by catching that code:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
try {
|
|
301
|
+
const rings = await HealthKit.queryActivitySummaries({ from: today });
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error instanceof Error && 'code' in error && error.code === 'ERR_HEALTH_CONNECT_UNSUPPORTED') {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### What works on both
|
|
311
|
+
|
|
312
|
+
| Area | Identifiers / methods |
|
|
313
|
+
| --- | --- |
|
|
314
|
+
| Activity | `QuantityType.stepCount`, walking/running/cycling/swimming/wheelchair distance, `flightsClimbed`, `pushCount`, `activeEnergyBurned`, `basalEnergyBurned` |
|
|
315
|
+
| Heart | `heartRate`, `restingHeartRate`, `heartRateVariabilitySDNN`, `vo2Max`, `oxygenSaturation`, `respiratoryRate` |
|
|
316
|
+
| Body | `bodyMass`, `height`, `bodyFatPercentage`, `leanBodyMass`, `bodyTemperature`, `bloodGlucose` |
|
|
317
|
+
| Blood pressure | `CorrelationType.bloodPressure` plus systolic / diastolic quantities |
|
|
318
|
+
| Nutrition | `CorrelationType.food`, `dietaryWater`, other `HKQuantityTypeIdentifierDietary*` types |
|
|
319
|
+
| Sleep | `CategoryType.sleepAnalysis` |
|
|
320
|
+
| Workouts | `queryWorkouts` / `saveWorkout` (`WorkoutActivityType` mapped onto Health Connect exercise types) |
|
|
321
|
+
| Aggregates | `queryStatistics`, `queryStatisticsCollection`, `queryAnchored` |
|
|
322
|
+
| Writes | `saveQuantitySample`, `saveCategorySample`, `saveCorrelation`, `deleteObjects` |
|
|
323
|
+
|
|
324
|
+
### iOS-only
|
|
325
|
+
|
|
326
|
+
These throw `ERR_HEALTH_CONNECT_UNSUPPORTED` on Android. Health Connect has no equivalent (or no stable mapping yet):
|
|
327
|
+
|
|
328
|
+
| API | Why |
|
|
329
|
+
| --- | --- |
|
|
330
|
+
| `queryElectrocardiograms` | Apple Watch ECG |
|
|
331
|
+
| `queryActivitySummaries` | Move / Exercise / Stand rings |
|
|
332
|
+
| `queryClinicalRecords` | FHIR health records |
|
|
333
|
+
| `queryAudiograms` | Hearing-test charts |
|
|
334
|
+
| `queryWorkoutRoute` | Workout GPS polylines |
|
|
335
|
+
| `queryHeartbeatSeries` | Beat-to-beat series |
|
|
336
|
+
| `getBiologicalSex`, `getBloodType`, `getDateOfBirth`, `getFitzpatrickSkinType`, `getWheelchairUse` | HealthKit characteristics |
|
|
337
|
+
| `observe`, `enableBackgroundDelivery` | HealthKit observer queries / background delivery |
|
|
338
|
+
|
|
339
|
+
### Behavioral differences
|
|
340
|
+
|
|
341
|
+
| | iOS | Android |
|
|
342
|
+
| --- | --- | --- |
|
|
343
|
+
| Backend | HealthKit | Health Connect (`connect-client` 1.1) |
|
|
344
|
+
| `isAvailable()` | HealthKit present | Health Connect installed (built-in on Android 14+; otherwise the Health Connect app) |
|
|
345
|
+
| Read permission | Apple does **not** disclose read grants | `getAuthorizationStatus` reflects granted Health Connect read or write |
|
|
346
|
+
| Unmapped types in `requestAuthorization` | Forwarded if HealthKit knows them | Skipped |
|
|
347
|
+
| History window | Granted samples, any age | Often last **30 days** unless the user also grants history access (the SDK requests it) |
|
|
348
|
+
| Anchored sync | `HKQueryAnchor` (opaque string) | Health Connect changes token (opaque string). Do not reuse an iOS anchor on Android |
|
|
349
|
+
| HRV | SDNN | Mapped to Health Connect RMSSD on the same `heartRateVariabilitySDNN` identifier — not the same statistic |
|
|
350
|
+
| Workout GPS | `queryWorkoutRoute` | Unsupported |
|
|
351
|
+
| `minSdk` | iOS 16.4 | API 26 (plugin raises it) |
|
|
352
|
+
|
|
353
|
+
Do **not** fork mapped queries per platform. Fork only when you need different UI for an Apple-only feature.
|
|
354
|
+
|
|
355
|
+
## 🔌 Config plugin
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
| Prop | Default | Purpose |
|
|
359
|
+
| --------------------------------- | -------------------------------------------------- | --------------------------------------------------------------- |
|
|
360
|
+
| `healthSharePermission` | `"Allow $(PRODUCT_NAME) to read your health data"` | `NSHealthShareUsageDescription` |
|
|
361
|
+
| `healthUpdatePermission` | `"Allow $(PRODUCT_NAME) to write health data"` | `NSHealthUpdateUsageDescription` |
|
|
362
|
+
| `healthClinicalRecordsPermission` | clinical default | Used when `isClinicalDataEnabled` is true |
|
|
363
|
+
| `isBackgroundDeliveryEnabled` | `false` | HealthKit background delivery entitlement + `UIBackgroundModes` |
|
|
364
|
+
| `isClinicalDataEnabled` | `false` | `health-records` access |
|
|
365
|
+
| `healthConnectPrivacyPolicyUrl` | — | Shown on the Health Connect permission rationale screen |
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
The plugin also sets `android.minSdkVersion` to 26 (Health Connect’s floor) unless the project already uses a higher value.
|
|
369
|
+
|
|
370
|
+
Set a permission string to `false` to skip writing that Info.plist key (if you manage it yourself).
|
|
371
|
+
|
|
372
|
+
## 📱 Example app
|
|
373
|
+
|
|
374
|
+
```sh
|
|
375
|
+
cd example
|
|
376
|
+
npm install
|
|
377
|
+
npx expo run:ios
|
|
378
|
+
# or
|
|
379
|
+
npx expo run:android
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The example is an Expo SDK 57 app that autolinks this module on **iOS and Android**. It will not work in Expo Go.
|
|
383
|
+
|
|
384
|
+
## 🤖 Agent skills
|
|
385
|
+
|
|
386
|
+
Coding agents (Cursor, Claude Code, Copilot, and similar) should load the bundled skill.
|
|
387
|
+
|
|
388
|
+
Copy [`.cursor/skills/expo-healthkit/`](https://github.com/appeeky/expo-healthkit/tree/main/.cursor/skills/expo-healthkit) into your app’s `.cursor/skills/expo-healthkit/` (or the agent’s skills folder):
|
|
389
|
+
|
|
390
|
+
| File | What agents get |
|
|
391
|
+
| --- | --- |
|
|
392
|
+
| [`SKILL.md`](https://github.com/appeeky/expo-healthkit/blob/main/.cursor/skills/expo-healthkit/SKILL.md) | When to use which query, auth rules, and plugin flags |
|
|
393
|
+
| [`reference.md`](https://github.com/appeeky/expo-healthkit/blob/main/.cursor/skills/expo-healthkit/reference.md) | Method signatures, units, identifiers |
|
|
394
|
+
| [`examples.md`](https://github.com/appeeky/expo-healthkit/blob/main/.cursor/skills/expo-healthkit/examples.md) | Copy-paste flows (steps, sleep, rings, blood pressure, food) |
|
|
395
|
+
|
|
396
|
+
The skill tells the agent to pass Apple identifier strings through one query/save API on iOS and Android, request every type in `toRead`, and rebuild native code after Swift, Kotlin, or plugin changes.
|
|
397
|
+
|
|
398
|
+
## 🤝 Contributing
|
|
399
|
+
|
|
400
|
+
Issues and pull requests are welcome. Contributions are licensed under [PolyForm Shield 1.0.0](./LICENSE).
|
|
401
|
+
|
|
402
|
+
### Checks
|
|
403
|
+
|
|
404
|
+
CI on `main` and every PR runs lint, typecheck, Jest, and an `npm pack` contents check (`.github/workflows/ci.yml`). Run the same locally before opening a PR:
|
|
405
|
+
|
|
406
|
+
```sh
|
|
407
|
+
npm ci
|
|
408
|
+
npm run lint
|
|
409
|
+
npm run typecheck
|
|
410
|
+
npm test -- --watchAll=false
|
|
411
|
+
npm run test:coverage
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
`npm test` opens Jest watch mode on a TTY. Pass `--watchAll=false` (as CI does) for a single run. `test:coverage` writes an HTML report to `coverage/` and must stay green for the JavaScript that tests already cover (`src/identifiers.ts`, `src/errors.ts`, `src/dates.ts`).
|
|
415
|
+
|
|
416
|
+
### What to test
|
|
417
|
+
|
|
418
|
+
| Change | Expected coverage |
|
|
419
|
+
| --- | --- |
|
|
420
|
+
| Identifiers, errors, date helpers, public JS types | Add or extend Jest in `src/__tests__/`. Keep `npm run test:coverage` passing |
|
|
421
|
+
| Config plugin | Typecheck `plugin/`; if you add pure helpers, put Jest next to them |
|
|
422
|
+
| Kotlin Health Connect mapping | Update the [Cross-platform](#cross-platform) tables **and** `.cursor/skills/expo-healthkit/`. Run the example on an Android device with Health Connect |
|
|
423
|
+
| Swift HealthKit | Same skill/README update. Run the example on Simulator or a device with the HealthKit capability |
|
|
424
|
+
| Example UI | `cd example && npx expo run:ios` / `npx expo run:android`. Health APIs do not run in Expo Go |
|
|
425
|
+
|
|
426
|
+
Native HealthKit / Health Connect cannot run in Node. Device or Simulator verification is the coverage for Swift and Kotlin. Do not add `Platform.OS` branches in `example/` for types that already map.
|
|
427
|
+
|
|
428
|
+
### Native rebuild
|
|
429
|
+
|
|
430
|
+
Metro reload is not enough after Swift, Kotlin, or plugin edits:
|
|
431
|
+
|
|
432
|
+
```sh
|
|
433
|
+
npx expo prebuild --clean
|
|
434
|
+
npx expo run:ios
|
|
435
|
+
# or
|
|
436
|
+
npx expo run:android
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Docs
|
|
440
|
+
|
|
441
|
+
If the public API or Android mapping changes, update `README.md`, `CHANGELOG.md`, and `.cursor/skills/expo-healthkit/` (`SKILL.md`, `reference.md`, `examples.md`) in the same PR.
|
|
442
|
+
|
|
443
|
+
## 🔒 Privacy
|
|
444
|
+
|
|
445
|
+
Health data stays on-device unless your app uploads it. Request only the types you need, explain why in the iOS usage strings, declare the matching Health Connect data types in Play Console, and follow Apple HealthKit and Google Health Connect review guidelines. Android reads are often limited to the last 30 days unless the user also grants history access.
|
|
446
|
+
|
|
447
|
+
## 📄 License
|
|
448
|
+
|
|
449
|
+
[PolyForm Shield 1.0.0](./LICENSE). See [`LICENSE`](./LICENSE) for the full terms.
|
|
450
|
+
|
|
451
|
+
You **can** install and use this SDK inside your own Expo / React Native apps, including commercial products.
|
|
452
|
+
|
|
453
|
+
You **cannot** copy this SDK and sell, publish, or otherwise offer a competing HealthKit / Health Connect library based on it.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id 'com.android.library'
|
|
3
|
+
id 'expo-module-gradle-plugin'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
group = 'expo.modules.healthkit'
|
|
7
|
+
version = '0.1.0'
|
|
8
|
+
|
|
9
|
+
android {
|
|
10
|
+
namespace "expo.modules.healthkit"
|
|
11
|
+
defaultConfig {
|
|
12
|
+
versionCode 1
|
|
13
|
+
versionName "0.1.0"
|
|
14
|
+
}
|
|
15
|
+
lintOptions {
|
|
16
|
+
abortOnError false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
dependencies {
|
|
21
|
+
implementation "androidx.health.connect:connect-client:1.1.0"
|
|
22
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<queries>
|
|
3
|
+
<package android:name="com.google.android.apps.healthdata" />
|
|
4
|
+
</queries>
|
|
5
|
+
|
|
6
|
+
<uses-permission android:name="android.permission.health.READ_STEPS" />
|
|
7
|
+
<uses-permission android:name="android.permission.health.WRITE_STEPS" />
|
|
8
|
+
<uses-permission android:name="android.permission.health.READ_DISTANCE" />
|
|
9
|
+
<uses-permission android:name="android.permission.health.WRITE_DISTANCE" />
|
|
10
|
+
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
|
|
11
|
+
<uses-permission android:name="android.permission.health.WRITE_ACTIVE_CALORIES_BURNED" />
|
|
12
|
+
<uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED" />
|
|
13
|
+
<uses-permission android:name="android.permission.health.WRITE_TOTAL_CALORIES_BURNED" />
|
|
14
|
+
<uses-permission android:name="android.permission.health.READ_FLOORS_CLIMBED" />
|
|
15
|
+
<uses-permission android:name="android.permission.health.WRITE_FLOORS_CLIMBED" />
|
|
16
|
+
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />
|
|
17
|
+
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE" />
|
|
18
|
+
<uses-permission android:name="android.permission.health.READ_RESTING_HEART_RATE" />
|
|
19
|
+
<uses-permission android:name="android.permission.health.WRITE_RESTING_HEART_RATE" />
|
|
20
|
+
<uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY" />
|
|
21
|
+
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE_VARIABILITY" />
|
|
22
|
+
<uses-permission android:name="android.permission.health.READ_OXYGEN_SATURATION" />
|
|
23
|
+
<uses-permission android:name="android.permission.health.WRITE_OXYGEN_SATURATION" />
|
|
24
|
+
<uses-permission android:name="android.permission.health.READ_RESPIRATORY_RATE" />
|
|
25
|
+
<uses-permission android:name="android.permission.health.WRITE_RESPIRATORY_RATE" />
|
|
26
|
+
<uses-permission android:name="android.permission.health.READ_BODY_TEMPERATURE" />
|
|
27
|
+
<uses-permission android:name="android.permission.health.WRITE_BODY_TEMPERATURE" />
|
|
28
|
+
<uses-permission android:name="android.permission.health.READ_BLOOD_GLUCOSE" />
|
|
29
|
+
<uses-permission android:name="android.permission.health.WRITE_BLOOD_GLUCOSE" />
|
|
30
|
+
<uses-permission android:name="android.permission.health.READ_BLOOD_PRESSURE" />
|
|
31
|
+
<uses-permission android:name="android.permission.health.WRITE_BLOOD_PRESSURE" />
|
|
32
|
+
<uses-permission android:name="android.permission.health.READ_VO2_MAX" />
|
|
33
|
+
<uses-permission android:name="android.permission.health.WRITE_VO2_MAX" />
|
|
34
|
+
<uses-permission android:name="android.permission.health.READ_HEIGHT" />
|
|
35
|
+
<uses-permission android:name="android.permission.health.WRITE_HEIGHT" />
|
|
36
|
+
<uses-permission android:name="android.permission.health.READ_WEIGHT" />
|
|
37
|
+
<uses-permission android:name="android.permission.health.WRITE_WEIGHT" />
|
|
38
|
+
<uses-permission android:name="android.permission.health.READ_BODY_FAT" />
|
|
39
|
+
<uses-permission android:name="android.permission.health.WRITE_BODY_FAT" />
|
|
40
|
+
<uses-permission android:name="android.permission.health.READ_LEAN_BODY_MASS" />
|
|
41
|
+
<uses-permission android:name="android.permission.health.WRITE_LEAN_BODY_MASS" />
|
|
42
|
+
<uses-permission android:name="android.permission.health.READ_SLEEP" />
|
|
43
|
+
<uses-permission android:name="android.permission.health.WRITE_SLEEP" />
|
|
44
|
+
<uses-permission android:name="android.permission.health.READ_EXERCISE" />
|
|
45
|
+
<uses-permission android:name="android.permission.health.WRITE_EXERCISE" />
|
|
46
|
+
<uses-permission android:name="android.permission.health.READ_NUTRITION" />
|
|
47
|
+
<uses-permission android:name="android.permission.health.WRITE_NUTRITION" />
|
|
48
|
+
<uses-permission android:name="android.permission.health.READ_HYDRATION" />
|
|
49
|
+
<uses-permission android:name="android.permission.health.WRITE_HYDRATION" />
|
|
50
|
+
<uses-permission android:name="android.permission.health.READ_WHEELCHAIR_PUSHES" />
|
|
51
|
+
<uses-permission android:name="android.permission.health.WRITE_WHEELCHAIR_PUSHES" />
|
|
52
|
+
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_HISTORY" />
|
|
53
|
+
|
|
54
|
+
<application>
|
|
55
|
+
<activity
|
|
56
|
+
android:name="expo.modules.healthkit.HealthConnectRationaleActivity"
|
|
57
|
+
android:exported="true"
|
|
58
|
+
android:theme="@android:style/Theme.DeviceDefault.Light.Dialog.NoActionBar">
|
|
59
|
+
<intent-filter>
|
|
60
|
+
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
|
61
|
+
</intent-filter>
|
|
62
|
+
</activity>
|
|
63
|
+
<activity-alias
|
|
64
|
+
android:name="ViewPermissionUsageActivity"
|
|
65
|
+
android:exported="true"
|
|
66
|
+
android:permission="android.permission.START_VIEW_PERMISSION_USAGE"
|
|
67
|
+
android:targetActivity="expo.modules.healthkit.HealthConnectRationaleActivity">
|
|
68
|
+
<intent-filter>
|
|
69
|
+
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
|
|
70
|
+
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
|
|
71
|
+
</intent-filter>
|
|
72
|
+
</activity-alias>
|
|
73
|
+
</application>
|
|
74
|
+
</manifest>
|