├── .npmrc ├── .prettierignore ├── .eslintrc.js ├── config.json ├── images └── healthkit-googlefit.jpg ├── .prettierrc.js ├── packages └── health-data │ ├── .npmignore │ ├── platforms │ ├── ios │ │ ├── app.entitlements │ │ └── Info.plist │ └── android │ │ ├── AndroidManifest.xml │ │ └── include.gradle │ ├── tsconfig.json │ ├── package.json │ ├── blueprint.md │ └── README.md ├── .gitmodules ├── src └── health-data │ ├── index.d.ts │ ├── platforms │ ├── ios │ │ ├── app.entitlements │ │ └── Info.plist │ └── android │ │ ├── include.gradle │ │ └── AndroidManifest.xml │ ├── index.common.ts │ ├── index.android.ts │ └── index.ios.ts ├── tsconfig.json ├── demo-snippets ├── package.json └── ng │ ├── install.module.ts │ └── items │ ├── items.component.html │ └── items.component.ts ├── lerna.json ├── .gitignore ├── references.d.ts ├── .travis.yml ├── package.json ├── README.md └── LICENSE /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules/ 3 | plugin/ 4 | docs/ 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: './tools/.eslintrc.js' 3 | }; 4 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "readme": true, 3 | "angular": true, 4 | "demos": [ 5 | "ng" 6 | ] 7 | } -------------------------------------------------------------------------------- /images/healthkit-googlefit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nativescript-community/nativescript-health-data/HEAD/images/healthkit-googlefit.jpg -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 200, 3 | semi: true, 4 | tabWidth: 4, 5 | trailingComma: 'none', 6 | singleQuote: true 7 | }; 8 | -------------------------------------------------------------------------------- /packages/health-data/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | pnpm-global/ 3 | src/ 4 | bin/ 5 | hooks/ 6 | *.ts 7 | *.map 8 | *.old 9 | tsconfig.json 10 | !*.d.ts 11 | blueprint.md -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "demo-ng"] 2 | path = demo-ng 3 | url = git@github.com:nativescript-community/plugin-seed-demo-ng.git 4 | [submodule "tools"] 5 | path = tools 6 | url = git@github.com:nativescript-community/plugin-seed-tools.git 7 | -------------------------------------------------------------------------------- /src/health-data/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * iOS and Android apis should match. 3 | * It doesn't matter if you export `.ios` or `.android`, either one but only one. 4 | */ 5 | export * from './index.ios'; 6 | 7 | // Export any shared classes, constants, etc. 8 | export * from './index.common'; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tools/tsconfig", 3 | "compilerOptions": { 4 | "paths": { 5 | "@nativescript-community/health-data": ["src/health-data"], 6 | "@nativescript-community/health-data/*": ["src/health-data/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/health-data/platforms/ios/app.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.healthkit 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/health-data/platforms/ios/app.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.healthkit 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo-snippets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nativescript-community/template-snippet", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "@nativescript-community/health-data": "file:../packages/health-data" 6 | }, 7 | "nativescript": { 8 | "platforms": { 9 | "android": "*", 10 | "ios": "*" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/health-data/platforms/android/include.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | jcenter() 4 | maven { 5 | url "https://maven.google.com" 6 | } 7 | } 8 | } 9 | 10 | dependencies { 11 | implementation 'com.google.android.gms:play-services-fitness:16.0.1' 12 | implementation 'com.google.android.gms:play-services-auth:16.0.1' 13 | } 14 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.0.33", 6 | "npmClient": "npm", 7 | "command": { 8 | "publish": { 9 | "conventionalCommits": true, 10 | "ignoreChanges": [ 11 | "ignored-file", 12 | "*.md" 13 | ] 14 | }, 15 | "bootstrap": { 16 | "npmClientArgs": [ 17 | "--no-package-lock" 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/health-data/platforms/ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSHealthShareUsageDescription 6 | HealthKit Share Usage Description 7 | NSHealthUpdateUsageDescription 8 | HealthKit Update Usage Description 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/health-data/platforms/ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSHealthShareUsageDescription 6 | HealthKit Share Usage Description 7 | NSHealthUpdateUsageDescription 8 | HealthKit Update Usage Description 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo-snippets/ng/install.module.ts: -------------------------------------------------------------------------------- 1 | import { NO_ERRORS_SCHEMA, NgModule } from '@angular/core'; 2 | 3 | 4 | import { ItemsComponent } from './items/items.component'; 5 | 6 | export const COMPONENTS = [ItemsComponent]; 7 | @NgModule({ 8 | schemas: [NO_ERRORS_SCHEMA] 9 | }) 10 | export class InstallModule {} 11 | 12 | export function installPlugin() {} 13 | 14 | export const demos = [ 15 | { name: 'Basic', path: 'basic', component: ItemsComponent } 16 | ]; 17 | -------------------------------------------------------------------------------- /packages/health-data/platforms/android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/health-data/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "../../src/health-data", 5 | "outDir": "./", 6 | "paths": { 7 | "tns-core-modules": ["./node_modules/@nativescript/core"], 8 | "tns-core-modules/*": ["./node_modules/@nativescript/core/*"] 9 | } 10 | }, 11 | "include": ["../../src/health-data/**/*", "../../references.d.ts", "../../src/references.d.ts"], 12 | "exclude": ["../../src/health-data/angular/**"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/health-data/platforms/android/include.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | mavenCentral() 4 | maven { 5 | url "https://maven.google.com" 6 | } 7 | } 8 | } 9 | 10 | dependencies { 11 | def androidGMSFitness = project.hasProperty("androidGMSFitness") ? project.androidGMSFitness : "16.0.1" 12 | def androidGMSAuth = project.hasProperty("androidGMSAuth") ? project.androidGMSAuth : "16.0.1" 13 | 14 | implementation "com.google.android.gms:play-services-fitness:$androidGMSFitness" 15 | implementation "com.google.android.gms:play-services-auth:$androidGMSAuth" 16 | } 17 | -------------------------------------------------------------------------------- /src/health-data/platforms/android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NativeScript 2 | hooks/ 3 | node_modules/ 4 | /platforms/ 5 | 6 | # NativeScript Template 7 | *.js.map 8 | !ngcc.config.js 9 | !webpack.config.js 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # General 19 | .DS_Store 20 | .AppleDouble 21 | .LSOverride 22 | .idea 23 | .cloud 24 | .project 25 | tmp/ 26 | package-lock.json 27 | 28 | !.eslintrc.js 29 | !.prettierrc.js 30 | 31 | !e2e/*.js 32 | !detox.config.js 33 | devices.js 34 | 35 | *.framework 36 | **/*.js.map 37 | src/**/*.js 38 | packages/**/*.js 39 | packages/**/*.d.ts 40 | bin 41 | build 42 | Pods 43 | !packages/platforms 44 | /packages/**/*.aar 45 | *.xcuserdatad 46 | /packages/README.md 47 | packages/**/*js.map 48 | packages/**/*js 49 | packages/**/angular/*.json 50 | packages/*.ngsummary.json 51 | packages/*.metadata.json 52 | packages/angular 53 | packages/typings 54 | pnpm-lock.yaml 55 | 56 | /blueprint.md -------------------------------------------------------------------------------- /references.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | 11 | /// 12 | 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | android: 2 | components: 3 | - tools 4 | - platform-tools 5 | - build-tools-26.0.1 6 | - android-26 7 | - extra-android-m2repository 8 | - sys-img-armeabi-v7a-android-21 9 | 10 | before_cache: 11 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 12 | 13 | cache: 14 | directories: 15 | - .nvm 16 | - $HOME/.gradle/caches/ 17 | - $HOME/.gradle/wrapper/ 18 | 19 | install: 20 | - echo no | npm install -g nativescript 21 | - tns usage-reporting disable 22 | - tns error-reporting disable 23 | - cd src 24 | - npm run setup 25 | 26 | script: 27 | 28 | 29 | matrix: 30 | include: 31 | - stage: "Lint" 32 | language: node_js 33 | os: linux 34 | node_js: "6" 35 | script: "npm run tslint" 36 | - stage: "Build Angular Demo" 37 | env: 38 | - BuildAndroid="25" 39 | language: android 40 | os: linux 41 | jdk: oraclejdk8 42 | before_install: nvm install 6.10.3 43 | script: 44 | - cd ../demo-ng && tns build android 45 | - os: osx 46 | env: 47 | - BuildiOS="11" 48 | - Xcode="9" 49 | osx_image: xcode9 50 | language: node_js 51 | node_js: "6" 52 | jdk: oraclejdk8 53 | script: 54 | - cd ../demo-ng && tns build ios 55 | -------------------------------------------------------------------------------- /packages/health-data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nativescript-community/health-data", 3 | "version": "2.0.0", 4 | "description": "Health Data plugin for Nativescript, using Google Fit and Apple HealthKit.", 5 | "main": "./index", 6 | "sideEffects": false, 7 | "typings": "./index.d.ts", 8 | "scripts": { 9 | "build": "npm run readme && npm run tsc", 10 | "build.watch": "npm run tsc -- -w", 11 | "build.all": "npm run build && npm run build.angular", 12 | "build.angular": "../../node_modules/.bin/ng-packagr -p ../../src/health-data/angular/ng-package.json -c ../../src/health-data/angular/tsconfig.json", 13 | "readme": "../../node_modules/.bin/readme generate -c ../../tools/readme/blueprint.json", 14 | "tsc": "../../node_modules/.bin/cpy '**/*.d.ts' '../../packages/health-data' --parents --cwd=../../src/health-data && ../../node_modules/.bin/tsc -skipLibCheck -d", 15 | "clean": "../../node_modules/.bin/rimraf ./*.d.ts ./*.js ./*.js.map" 16 | }, 17 | "nativescript": { 18 | "platforms": { 19 | "android": "6.0.0", 20 | "ios": "6.0.0" 21 | } 22 | }, 23 | "keywords": [ 24 | "NativeScript", 25 | "JavaScript", 26 | "Android", 27 | "iOS", 28 | "Health", 29 | "HealthKit", 30 | "Google Fit" 31 | ], 32 | "author": { 33 | "name": "Eddy Verbruggen", 34 | "email": "eddyverbruggen@gmail.com" 35 | }, 36 | "contributors": [ 37 | { 38 | "name": "Filipe Mendes", 39 | "email": "filipemendes1994@gmail.com" 40 | }, 41 | { 42 | "name": "Daniel Leal", 43 | "url": "https://github.com/danielgek" 44 | } 45 | ], 46 | "bugs": { 47 | "url": "https://github.com/nativescript-community/health-data/issues" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "https://github.com/nativescript-community/health-data" 52 | }, 53 | "license": "Apache-2.0", 54 | "readmeFilename": "README.md", 55 | "dependencies": {} 56 | } 57 | -------------------------------------------------------------------------------- /demo-snippets/ng/items/items.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 11 | 13 | 15 | 17 | 19 | 20 | 21 | 22 | 24 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /demo-snippets/ng/items/items.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgZone } from '@angular/core'; 2 | import { Dialogs } from '@nativescript/core'; 3 | import { AggregateBy, HealthData, HealthDataType } from '@nativescript-community/health-data'; 4 | 5 | @Component({ 6 | selector: 'ns-items', 7 | templateUrl: './items.component.html', 8 | }) 9 | export class ItemsComponent { 10 | private static TYPES: Array = [ 11 | { name: 'height', accessType: 'read' }, 12 | { name: 'weight', accessType: 'readAndWrite' }, // just for show 13 | { name: 'steps', accessType: 'read' }, 14 | { name: 'distance', accessType: 'read' }, 15 | { name: 'heartRate', accessType: 'read' }, 16 | { name: 'fatPercentage', accessType: 'read' }, 17 | { name: 'cardio', accessType: 'read' }, 18 | ]; 19 | 20 | private healthData: HealthData; 21 | resultToShow = ''; 22 | 23 | constructor(private zone: NgZone) { 24 | this.healthData = new HealthData(); 25 | } 26 | 27 | isAvailable(): void { 28 | this.healthData 29 | .isAvailable(true) 30 | .then((available) => (this.resultToShow = available ? 'Health Data available' : 'Health Data not available :(')); 31 | } 32 | 33 | isAuthorized(): void { 34 | this.healthData.isAuthorized([{ name: 'weight', accessType: 'read' }]).then((authorized) => 35 | setTimeout( 36 | () => 37 | Dialogs.alert({ 38 | title: 'Authentication result', 39 | message: (authorized ? '' : 'Not ') + 'authorized for ' + JSON.stringify(ItemsComponent.TYPES), 40 | okButtonText: 'Ok!', 41 | }), 42 | 300 43 | ) 44 | ); 45 | } 46 | 47 | requestAuthForVariousTypes(): void { 48 | this.healthData 49 | .requestAuthorization(ItemsComponent.TYPES) 50 | .then((authorized) => 51 | setTimeout( 52 | () => 53 | Dialogs.alert({ 54 | title: 'Authentication result', 55 | message: (authorized ? '' : 'Not ') + 'authorized for ' + JSON.stringify(ItemsComponent.TYPES), 56 | okButtonText: 'Ok!', 57 | }), 58 | 300 59 | ) 60 | ) 61 | .catch((error) => console.log('Request auth error: ', error)); 62 | } 63 | 64 | getData(dataType: string, unit?: string, aggregateBy?: AggregateBy): Promise { 65 | return this.healthData 66 | .query({ 67 | startDate: new Date(new Date().getTime() - 24 * 60 * 60 * 1000), // 1 day ago 68 | endDate: new Date(), // now 69 | dataType, 70 | unit, 71 | aggregateBy, 72 | sortOrder: 'desc', 73 | }) 74 | .then((result) => { 75 | this.zone.run(() => { 76 | console.log(JSON.stringify(result)); 77 | this.resultToShow = JSON.stringify(result); 78 | }); 79 | }) 80 | .catch((error) => (this.resultToShow = error)); 81 | } 82 | 83 | startMonitoringData(dataType: string, unit: string): void { 84 | this.healthData 85 | .startMonitoring({ 86 | dataType: dataType, 87 | enableBackgroundUpdates: true, 88 | backgroundUpdateFrequency: 'immediate', 89 | onUpdate: (completionHandler: () => void) => { 90 | console.log('Our app was notified that health data changed, so querying...'); 91 | this.getData(dataType, unit).then(() => completionHandler()); 92 | }, 93 | }) 94 | .then(() => (this.resultToShow = `Started monitoring ${dataType}`)) 95 | .catch((error) => (this.resultToShow = error)); 96 | } 97 | 98 | stopMonitoringData(dataType: string): void { 99 | this.healthData 100 | .stopMonitoring({ 101 | dataType: dataType, 102 | }) 103 | .then(() => (this.resultToShow = `Stopped monitoring ${dataType}`)) 104 | .catch((error) => (this.resultToShow = error)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nativescript-community/health-data", 3 | "version": "2.0.0", 4 | "homepage": "https://github.com/nativescript-community/health-data#readme", 5 | "bugs": { 6 | "url": "https://github.com/nativescript-community/health-data/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/nativescript-community/health-data.git" 11 | }, 12 | "license": "ISC", 13 | "author": "", 14 | "scripts": { 15 | "build": "lerna run build", 16 | "build.watch": "lerna run build.watch --parallel", 17 | "build.all": "lerna run build.all", 18 | "build.angular": "lerna run build.angular", 19 | "clean": "rimraf 'packages/**/*.d.ts' 'packages/**/*.js' 'packages/**/*.js.map' 'packages/**/*.metadata.json' 'packages/**/*.ngsummary.json' 'packages/**/*.mjs' 'packages/**/*.mjs.map' 'packages/**/node_modules' 'packages/**/angular/package.json' 'package-lock.json' 'pnpm-lock.yaml' 'node_modules'", 20 | "commitmsg": "commitlint -e $GIT_PARAMS", 21 | "demo.ng.android": "cd ./demo-ng && ns run android --no-hmr", 22 | "demo.ng.clean": "cd ./demo-ng && ns clean", 23 | "demo.ng.ios": "cd ./demo-ng && ns run ios --no-hmr", 24 | "demo.react.android": "cd ./demo-react && ns run android --no-hmr", 25 | "demo.react.clean": "cd ./demo-react && ns clean", 26 | "demo.react.ios": "cd ./demo-react && ns run ios --no-hmr", 27 | "demo.svelte.android": "cd ./demo-svelte && ns run android --no-hmr", 28 | "demo.svelte.clean": "cd ./demo-svelte && ns clean", 29 | "demo.svelte.ios": "cd ./demo-svelte && ns run ios --no-hmr", 30 | "demo.vue.android": "cd ./demo-vue && ns run android --no-hmr", 31 | "demo.vue.clean": "cd ./demo-vue && ns clean", 32 | "demo.vue.ios": "cd ./demo-vue && ns run ios --no-hmr", 33 | "postinstall": "npm run setup", 34 | "publish": "npm run setup && npm run build.all && lerna publish --create-release=github --force-publish", 35 | "readme": "node ./tools/readme.js", 36 | "setup": "npm run submodules && ts-patch install", 37 | "start": "./node_modules/.bin/ntl -A -s 15 -o", 38 | "submodules": "git submodule update --init", 39 | "sync": "node ./tools/sync.js", 40 | "sync.test": "node ./tools/sync.js", 41 | "tsc": "cpy '**/*.d.ts' '../plugin' --parents --cwd=src && tsc -skipLibCheck -d", 42 | "update": "node ./tools/update.js" 43 | }, 44 | "commitlint": { 45 | "extends": [ 46 | "@commitlint/config-conventional" 47 | ] 48 | }, 49 | "dependencies": { 50 | "@nativescript-community/plugin-seed-tools": "file:tools" 51 | }, 52 | "ntl": { 53 | "descriptions": { 54 | "build": "Build the plugin", 55 | "build.angular": "Build the plugin for Angular", 56 | "build.all": "Build the plugin for all platforms", 57 | "clean": "Clean the local environment.", 58 | "demo.ng.android": "Runs the Angular demo on Android.", 59 | "demo.ng.ios": "Runs the Angular demo on iOS.", 60 | "demo.react.android": "Runs the React demo on Android.", 61 | "demo.react.ios": "Runs the React demo on iOS.", 62 | "demo.svelte.android": "Runs the Svelte demo on Android.", 63 | "demo.svelte.ios": "Runs the Svelte demo on iOS.", 64 | "demo.vue.android": "Runs the Vue demo on Android.", 65 | "demo.vue.ios": "Runs the Vue demo on iOS.", 66 | "watch": "Watch for changes in the plugin source and re-build." 67 | } 68 | }, 69 | "dependenciesMeta": { 70 | "@nativescript-community/plugin-seed-tools": { 71 | "injected": true 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/health-data/index.common.ts: -------------------------------------------------------------------------------- 1 | export interface ConfigurationData { 2 | startDate: Date; 3 | endDate: Date; 4 | gfBucketUnit: string; 5 | gfBucketSize: number; 6 | typeOfData: string; 7 | } 8 | 9 | export interface HealthDataType { 10 | name: string; 11 | accessType: 'read' | 'write' | 'readAndWrite'; 12 | } 13 | 14 | export type AggregateBy = 'hour' | 'day' | 'sourceAndDay'; 15 | 16 | export type BackgroundUpdateFrequency = 'immediate' | 'hourly' | 'daily' | 'weekly'; 17 | 18 | export type SortOrder = 'asc' | 'desc'; 19 | 20 | export interface QueryRequest { 21 | startDate: Date; 22 | endDate: Date; 23 | dataType: string; 24 | unit: string; 25 | aggregateBy?: AggregateBy; 26 | /** 27 | * Default "asc" 28 | */ 29 | sortOrder?: SortOrder; 30 | /** 31 | * Default undefined, so whatever the platform limit is. 32 | */ 33 | limit?: number; 34 | } 35 | 36 | export interface StartMonitoringRequest { 37 | /** 38 | * Default false 39 | */ 40 | enableBackgroundUpdates?: boolean; 41 | /** 42 | * Default 'immediate', only relevant when 'enableBackgroundUpdates' is 'true'. 43 | */ 44 | backgroundUpdateFrequency?: BackgroundUpdateFrequency; 45 | dataType: string; 46 | /** 47 | * This callback function is invoked when the health store receives an update (add/delete data). 48 | * You can use this trigger to fetch the latest data. 49 | */ 50 | onUpdate: (completionHandler: () => void) => void; 51 | onError?: (error: string) => void; 52 | } 53 | 54 | export interface StopMonitoringRequest { 55 | dataType?: string; 56 | } 57 | 58 | export interface ResponseItem { 59 | start: Date; 60 | end: Date; 61 | value: number; 62 | /** 63 | * Added this, because on Android this may be different than what was requested 64 | */ 65 | unit: string; 66 | source?: string; 67 | } 68 | 69 | /* 70 | export interface ResultResponse { 71 | status: { 72 | action: string; 73 | message: string; 74 | }; 75 | data: { 76 | type: string; 77 | response: Array; 78 | }; 79 | } 80 | 81 | export interface ErrorResponse { 82 | action: string; 83 | description: string; 84 | } 85 | 86 | export function createResultResponse(action: string, message: string, type?: string, result?: Array): ResultResponse { 87 | return { 88 | status: {action, message}, 89 | data: { 90 | type: type || null, 91 | response: result || null 92 | } 93 | }; 94 | } 95 | 96 | export function createErrorResponse(action: string, description: string): ErrorResponse { 97 | return {action, description}; 98 | } 99 | */ 100 | 101 | export interface HealthDataApi { 102 | isAvailable(updateGooglePlayServicesIfNeeded?: /* for Android, default true */ boolean): Promise; 103 | 104 | isAuthorized(types: HealthDataType[]): Promise; 105 | 106 | requestAuthorization(types: HealthDataType[]): Promise; 107 | 108 | query(opts: QueryRequest): Promise; 109 | 110 | startMonitoring(opts: StartMonitoringRequest): Promise; 111 | 112 | stopMonitoring(opts: StopMonitoringRequest): Promise; 113 | } 114 | 115 | export abstract class Common { 116 | protected aggregate(parsedData: ResponseItem[], aggregateBy?: AggregateBy): ResponseItem[] { 117 | if (aggregateBy) { 118 | const result: ResponseItem[] = []; 119 | if (aggregateBy === 'sourceAndDay') { 120 | // extract the unique sources 121 | const distinctSources: Set = new Set(); 122 | parsedData.forEach((item) => distinctSources.add(item.source)); 123 | // for each source, filter and aggregate the data 124 | distinctSources.forEach((source) => 125 | this.aggregateData( 126 | parsedData.filter((item) => item.source === source), 127 | aggregateBy, 128 | result 129 | ) 130 | ); 131 | } else { 132 | this.aggregateData(parsedData, aggregateBy, result); 133 | } 134 | return result; 135 | } else { 136 | return parsedData; 137 | } 138 | } 139 | 140 | private aggregateData(parsedData: ResponseItem[], aggregateBy: AggregateBy, result: ResponseItem[]): void { 141 | parsedData.forEach((item, i) => { 142 | const previousItem = i === 0 ? null : parsedData[i - 1]; 143 | if (previousItem === null || !this.isSameAggregationInterval(item, previousItem, aggregateBy)) { 144 | result.push({ 145 | source: item.source, 146 | start: item.start, 147 | end: item.end, 148 | value: item.value 149 | } as ResponseItem); 150 | } else { 151 | result[result.length - 1].value += item.value; 152 | result[result.length - 1].end = item.end; 153 | } 154 | }); 155 | } 156 | 157 | // note that the startdate determines the interval 158 | private isSameAggregationInterval(item: ResponseItem, previousItem: ResponseItem, aggregateBy: AggregateBy) { 159 | const isSameDay = item.start.toDateString() === previousItem.start.toDateString(); 160 | switch (aggregateBy) { 161 | case 'hour': 162 | return isSameDay && item.start.getHours() === previousItem.start.getHours(); 163 | case 'day': 164 | case 'sourceAndDay': 165 | // note that when this function is called, the sources have already been dealt with, so treat it equal to "day" 166 | return isSameDay; 167 | default: 168 | return false; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /packages/health-data/blueprint.md: -------------------------------------------------------------------------------- 1 | {{ load:../../tools/readme/edit-warning.md }} 2 | {{ template:title }} 3 | {{ template:badges }} 4 | {{ template:description }} 5 | 6 | 7 | 8 | {{ template:toc }} 9 | 10 | ## Installation 11 | Run the following command from the root of your project: 12 | 13 | `ns plugin add {{ pkg.name }}` 14 | 15 | ## Configuration 16 | 17 | ### Android 18 | 19 | Google Fit API Key - Go to the [Google Developers Console](https://console.developers.google.com/), create a project, and enable the `Fitness API`. 20 | Then under `Credentials`, create a `Fitness API` OAuth2 client ID for an Android App (select `User data` and press the `What credentials do I need?` button). 21 | If you are using Linux/maxOS, generate your SHA1-key with the code below. 22 | 23 | ```shell 24 | keytool -exportcert -keystore path-to-debug-or-production-keystore -list -v 25 | ``` 26 | 27 | > Note that the default (debug) keystore password is empty. 28 | 29 | ### iOS 30 | 31 | Make sure you enable the `HealthKit` entitlement in your app ID. 32 | 33 | ## API 34 | 35 | The examples below are all in TypeScript, and the [demo](https://github.com/EddyVerbruggen/nativescript-health-data/tree/master/demo-ng) was developed in Nativescript w/ Angular. 36 | 37 | This is how you can import and instantiate the plugin, all examples expect this setup: 38 | 39 | ```typescript 40 | import { AggregateBy, HealthData, HealthDataType } from 'nativescript-health-data'; 41 | 42 | export class MyHealthyClass { 43 | private healthData: HealthData; 44 | 45 | constructor() { 46 | this.healthData = new HealthData(); 47 | } 48 | } 49 | ``` 50 | 51 | ### `isAvailable` 52 | 53 | This tells you whether or not the device supports Health Data. On iOS this is probably always `true`. 54 | On Android the user will be prompted to (automatically) update their Play Services version in case it's not sufficiently up to date. 55 | If you don't want this behavior, pass false to this function, as shown below. 56 | 57 | ```typescript 58 | this.healthData.isAvailable(false).then((available) => console.log(available)); 59 | ``` 60 | 61 | ### `isAuthorized` 62 | 63 | This function (and the next one) takes an `Array` of `HealthDataType`'s. Each of those has a `name` and an `accessType`. 64 | 65 | - The `name` can be one of the ['Available Data Types'](#available-data-types). 66 | - The accessType can be one of `read`, `write`, or `readAndWrite` (note that this plugin currently only supports reading data, but that will change). 67 | 68 | > iOS is a bit silly here: if you've only requested 'read' access, you'll never get a `true` response from this method. [Details here.](https://stackoverflow.com/a/29128231/2596974) 69 | 70 | ```typescript 71 | this.healthData 72 | .isAuthorized([{ name: 'steps', accessType: 'read' }]) 73 | .then((authorized) => console.log(authorized)); 74 | ``` 75 | 76 | ### `requestAuthorization` 77 | 78 | This function takes the same argument as `isAuthorized`, and will trigger a consent screen in case the user hasn't previously authorized your app to access any of the passed `HealthDataType`'s. 79 | 80 | Note that this plugin currently only supports reading data, but that will change. 81 | 82 | ```typescript 83 | const types: Array = [ 84 | { name: 'height', accessType: 'write' }, 85 | { name: 'weight', accessType: 'readAndWrite' }, 86 | { name: 'steps', accessType: 'read' }, 87 | { name: 'distance', accessType: 'read' }, 88 | ]; 89 | 90 | this.healthData 91 | .requestAuthorization(types) 92 | .then((authorized) => console.log(authorized)) 93 | .catch((error) => console.log('Request auth error: ', error)); 94 | ``` 95 | 96 | ### `query` 97 | 98 | Mandatory properties are `startData`, `endDate`, and `dataType`. 99 | The `dataType` must be one of the ['Available Data Types'](#available-data-types). 100 | 101 | By default data is not aggregated, so all individual datapoints are returned. 102 | This plugin however offers a way to aggregate the data by either `hour`, `day`, or `sourceAndDay`, 103 | the latter will enable you to read daily data per source (Fitbit, Nike Run Club, manual entry, etc). 104 | 105 | If you didn't run `requestAuthorization` before running `query`, 106 | the plugin will run `requestAuthorization` for you (for the requested `dataType`). You're welcome. 😉 107 | 108 | ```typescript 109 | this.healthData 110 | .query({ 111 | startDate: new Date(new Date().getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days ago 112 | endDate: new Date(), // now 113 | dataType: 'steps', // equal to the 'name' property of 'HealthDataType' 114 | unit: 'count', // make sure this is compatible with the 'dataType' (see below) 115 | aggregateBy: 'day', // optional, one of: "hour", "day", "sourceAndDay" 116 | sortOrder: 'desc', // optional, default "asc" 117 | }) 118 | .then((result) => console.log(JSON.stringify(result))) 119 | .catch((error) => (this.resultToShow = error)); 120 | ``` 121 | 122 | ### `queryAggregateData` 123 | 124 | Difference between `query` and `queryAggregateData`: if you use `query`, you will probably find that the number of steps (or other types of data) do not match those shown by the Google Fit and Apple Health apps. If you wanted to accurately compute the user's data then use: `queryAggregateData` 125 | 126 | Mandatory properties are `startData`, `endDate`, and `dataType`. 127 | The `dataType` must be one of the ['Available Data Types'](#available-data-types). 128 | 129 | By default data is aggregated by `day`. 130 | This plugin however offers a way to aggregate the data by either `hour` and `day`. (`month` and `year` available soon) 131 | 132 | Note that `queryAggregateData` only supports `steps`, `calories` and `distance` on Android. (More data types available soon). 133 | 134 | If you didn't run `requestAuthorization` before running `query`, 135 | the plugin will run `requestAuthorization` for you (for the requested `dataType`). You're welcome. 😉 136 | 137 | ```typescript 138 | this.healthData 139 | .queryAggregateData({ 140 | startDate: new Date(new Date().getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days ago 141 | endDate: new Date(), // now 142 | dataType: 'steps', // equal to the 'name' property of 'HealthDataType' 143 | unit: 'count', // make sure this is compatible with the 'dataType' (see below) 144 | aggregateBy: 'day', // optional, one of: "hour", "day" ; default: "day" 145 | }) 146 | .then((result) => console.log(JSON.stringify(result))) 147 | .catch((error) => (this.resultToShow = error)); 148 | ``` 149 | 150 | ### `startMonitoring` (iOS only, for now) 151 | 152 | If you want to be notified when health data was changed, you can monitor specific types. 153 | This even works when your app is in the background, with `enableBackgroundUpdates: true`. 154 | Note that iOS will wake up your app so you can act upon this notification (in the `onUpdate` function by fi. querying recent changes to this data type), 155 | but in return you are responsible for telling iOS you're done. So make sure you invoke the `completionHandler` as shown below. 156 | 157 | Not all data types support `backgroundUpdateFrequency: "immediate"`, 158 | so your app may not always be invoked immediately when data is added/deleted in HealthKit. 159 | 160 | > Background notifications probably don't work on the iOS simulator, so please test those on a real device. 161 | 162 | ```typescript 163 | this.healthData 164 | .startMonitoring({ 165 | dataType: 'heartRate', 166 | enableBackgroundUpdates: true, 167 | backgroundUpdateFrequency: 'immediate', 168 | onUpdate: (completionHandler: () => void) => { 169 | console.log('Our app was notified that health data changed, so querying...'); 170 | this.getData('heartRate', 'count/min').then(() => completionHandler()); 171 | }, 172 | }) 173 | .then(() => (this.resultToShow = `Started monitoring heartRate`)) 174 | .catch((error) => (this.resultToShow = error)); 175 | ``` 176 | 177 | ### `stopMonitoring` (iOS only, for now) 178 | 179 | It's best to call this method in case you no longer wish to receive notifications when health data changes. 180 | 181 | ```typescript 182 | this.healthData 183 | .stopMonitoring({ 184 | dataType: 'heartRate', 185 | }) 186 | .then(() => (this.resultToShow = `Stopped monitoring heartRate`)) 187 | .catch((error) => (this.resultToShow = error)); 188 | ``` 189 | 190 | ## Available Data Types 191 | 192 | With version 1.0.0 these are the supported types of data you can read. Also, make sure you pass in the correct `unit`. 193 | 194 | Note that you are responsible for passing the correct `unit`, although there's only 1 option for each type. _Well actually, the `unit` is ignored on Android at the moment, and on iOS there are undocumented types you can pass in (fi. `mi` for `distance`)._ 195 | 196 | The reason is I intend to support more units per type, but that is yet to be implemented... so it's for the sake of future backward-compatibility! 🤯 197 | 198 | | TypeOfData | Unit | GoogleFit Data Type | Apple HealthKit Data Type | 199 | | ------------- | --------- | -------------------------- | ------------------------------------------------ | 200 | | distance | m | `TYPE_DISTANCE_DELTA` | `HKQuantityTypeIdentifierDistanceWalkingRunning` | 201 | | steps | count | `TYPE_STEP_COUNT_DELTA` | `HKQuantityTypeIdentifierStepCount` | 202 | | calories | count | `TYPE_CALORIES_EXPENDED` | `HKQuantityTypeIdentifierActiveEnergyBurned` | 203 | | height | m | `TYPE_HEIGHT` | `HKQuantityTypeIdentifierHeight` | 204 | | weight | kg | `TYPE_WEIGHT` | `HKQuantityTypeIdentifierBodyMass` | 205 | | heartRate | count/min | `TYPE_HEART_RATE_BPM` | `HKQuantityTypeIdentifierHeartRate` | 206 | | fatPercentage | % | `TYPE_BODY_FAT_PERCENTAGE` | `HKQuantityTypeIdentifierBodyFatPercentage` | 207 | 208 | ## Credits 209 | 210 | - [Filipe Mendes](https://github.com/filipemendes1994/) for a superb first version of this repo, while working for SPMS, Shared Services for Ministry of Health (of Portugal). He kindly transferred this repo to me when he no longer had time to maintain it. 211 | - Daniel Leal, for [a great PR](https://github.com/EddyVerbruggen/nativescript-health-data/pull/4). 212 | 213 | ### Examples: 214 | 215 | - [Basic Drawer](demo-snippets/vue/BasicDrawer.vue) 216 | - A basic sliding drawer. 217 | - [All Sides](demo-snippets/vue/AllSides.vue) 218 | - An example of drawers on all sides: left, right, top, bottom. 219 | 220 | {{ load:../../tools/readme/demos-and-development.md }} 221 | {{ load:../../tools/readme/questions.md }} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Health Data plugin for NativeScript 6 | 7 | This is a NativeScript plugin that abstracts Apple HealthKit and Google Fit to read health data from the user's device. 8 | 9 | [![Build Status][build-status]][build-url] 10 | [![NPM version][npm-image]][npm-url] 11 | [![Downloads][downloads-image]][npm-url] 12 | [![Twitter Follow][twitter-image]][twitter-url] 13 | 14 | [build-status]: https://travis-ci.org/EddyVerbruggen/nativescript-health-data.svg?branch=master 15 | [build-url]: https://travis-ci.org/EddyVerbruggen/nativescript-health-data 16 | [npm-image]: http://img.shields.io/npm/v/nativescript-health-data.svg 17 | [npm-url]: https://npmjs.org/package/nativescript-health-data 18 | [downloads-image]: http://img.shields.io/npm/dm/nativescript-health-data.svg 19 | [twitter-image]: https://img.shields.io/twitter/follow/eddyverbruggen.svg?style=social&label=Follow%20me 20 | [twitter-url]: https://twitter.com/eddyverbruggen 21 | 22 | ## Prerequisites 23 | 24 | ### Android 25 | 26 | Google Fit API Key - Go to the [Google Developers Console](https://console.developers.google.com/), create a project, and enable the `Fitness API`. 27 | Then under `Credentials`, create a `Fitness API` OAuth2 client ID for an Android App (select `User data` and press the `What credentials do I need?` button). 28 | If you are using Linux/maxOS, generate your SHA1-key with the code below. 29 | 30 | ```shell 31 | keytool -exportcert -keystore path-to-debug-or-production-keystore -list -v 32 | ``` 33 | 34 | > Note that the default (debug) keystore password is empty. 35 | 36 | ### iOS 37 | 38 | Make sure you enable the `HealthKit` entitlement in your app ID. 39 | 40 | ## Installation 41 | 42 | Install the plugin using the NativeScript CLI: 43 | 44 | ``` 45 | tns plugin add nativescript-health-data 46 | ``` 47 | 48 | ## API 49 | 50 | The examples below are all in TypeScript, and the [demo](https://github.com/EddyVerbruggen/nativescript-health-data/tree/master/demo-ng) was developed in Nativescript w/ Angular. 51 | 52 | This is how you can import and instantiate the plugin, all examples expect this setup: 53 | 54 | ```typescript 55 | import { AggregateBy, HealthData, HealthDataType } from 'nativescript-health-data'; 56 | 57 | export class MyHealthyClass { 58 | private healthData: HealthData; 59 | 60 | constructor() { 61 | this.healthData = new HealthData(); 62 | } 63 | } 64 | ``` 65 | 66 | ### `isAvailable` 67 | 68 | This tells you whether or not the device supports Health Data. On iOS this is probably always `true`. 69 | On Android the user will be prompted to (automatically) update their Play Services version in case it's not sufficiently up to date. 70 | If you don't want this behavior, pass false to this function, as shown below. 71 | 72 | ```typescript 73 | this.healthData.isAvailable(false).then((available) => console.log(available)); 74 | ``` 75 | 76 | ### `isAuthorized` 77 | 78 | This function (and the next one) takes an `Array` of `HealthDataType`'s. Each of those has a `name` and an `accessType`. 79 | 80 | - The `name` can be one of the ['Available Data Types'](#available-data-types). 81 | - The accessType can be one of `read`, `write`, or `readAndWrite` (note that this plugin currently only supports reading data, but that will change). 82 | 83 | > iOS is a bit silly here: if you've only requested 'read' access, you'll never get a `true` response from this method. [Details here.](https://stackoverflow.com/a/29128231/2596974) 84 | 85 | ```typescript 86 | this.healthData 87 | .isAuthorized([{ name: 'steps', accessType: 'read' }]) 88 | .then((authorized) => console.log(authorized)); 89 | ``` 90 | 91 | ### `requestAuthorization` 92 | 93 | This function takes the same argument as `isAuthorized`, and will trigger a consent screen in case the user hasn't previously authorized your app to access any of the passed `HealthDataType`'s. 94 | 95 | Note that this plugin currently only supports reading data, but that will change. 96 | 97 | ```typescript 98 | const types: Array = [ 99 | { name: 'height', accessType: 'write' }, 100 | { name: 'weight', accessType: 'readAndWrite' }, 101 | { name: 'steps', accessType: 'read' }, 102 | { name: 'distance', accessType: 'read' }, 103 | ]; 104 | 105 | this.healthData 106 | .requestAuthorization(types) 107 | .then((authorized) => console.log(authorized)) 108 | .catch((error) => console.log('Request auth error: ', error)); 109 | ``` 110 | 111 | ### `query` 112 | 113 | Mandatory properties are `startData`, `endDate`, and `dataType`. 114 | The `dataType` must be one of the ['Available Data Types'](#available-data-types). 115 | 116 | By default data is not aggregated, so all individual datapoints are returned. 117 | This plugin however offers a way to aggregate the data by either `hour`, `day`, or `sourceAndDay`, 118 | the latter will enable you to read daily data per source (Fitbit, Nike Run Club, manual entry, etc). 119 | 120 | If you didn't run `requestAuthorization` before running `query`, 121 | the plugin will run `requestAuthorization` for you (for the requested `dataType`). You're welcome. 😉 122 | 123 | ```typescript 124 | this.healthData 125 | .query({ 126 | startDate: new Date(new Date().getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days ago 127 | endDate: new Date(), // now 128 | dataType: 'steps', // equal to the 'name' property of 'HealthDataType' 129 | unit: 'count', // make sure this is compatible with the 'dataType' (see below) 130 | aggregateBy: 'day', // optional, one of: "hour", "day", "sourceAndDay" 131 | sortOrder: 'desc', // optional, default "asc" 132 | }) 133 | .then((result) => console.log(JSON.stringify(result))) 134 | .catch((error) => (this.resultToShow = error)); 135 | ``` 136 | 137 | ### `queryAggregateData` 138 | 139 | Difference between `query` and `queryAggregateData`: if you use `query`, you will probably find that the number of steps (or other types of data) do not match those shown by the Google Fit and Apple Health apps. If you wanted to accurately compute the user's data then use: `queryAggregateData` 140 | 141 | Mandatory properties are `startData`, `endDate`, and `dataType`. 142 | The `dataType` must be one of the ['Available Data Types'](#available-data-types). 143 | 144 | By default data is aggregated by `day`. 145 | This plugin however offers a way to aggregate the data by either `hour` and `day`. (`month` and `year` available soon) 146 | 147 | Note that `queryAggregateData` only supports `steps`, `calories` and `distance` on Android. (More data types available soon). 148 | 149 | If you didn't run `requestAuthorization` before running `query`, 150 | the plugin will run `requestAuthorization` for you (for the requested `dataType`). You're welcome. 😉 151 | 152 | ```typescript 153 | this.healthData 154 | .queryAggregateData({ 155 | startDate: new Date(new Date().getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days ago 156 | endDate: new Date(), // now 157 | dataType: 'steps', // equal to the 'name' property of 'HealthDataType' 158 | unit: 'count', // make sure this is compatible with the 'dataType' (see below) 159 | aggregateBy: 'day', // optional, one of: "hour", "day" ; default: "day" 160 | }) 161 | .then((result) => console.log(JSON.stringify(result))) 162 | .catch((error) => (this.resultToShow = error)); 163 | ``` 164 | 165 | ### `startMonitoring` (iOS only, for now) 166 | 167 | If you want to be notified when health data was changed, you can monitor specific types. 168 | This even works when your app is in the background, with `enableBackgroundUpdates: true`. 169 | Note that iOS will wake up your app so you can act upon this notification (in the `onUpdate` function by fi. querying recent changes to this data type), 170 | but in return you are responsible for telling iOS you're done. So make sure you invoke the `completionHandler` as shown below. 171 | 172 | Not all data types support `backgroundUpdateFrequency: "immediate"`, 173 | so your app may not always be invoked immediately when data is added/deleted in HealthKit. 174 | 175 | > Background notifications probably don't work on the iOS simulator, so please test those on a real device. 176 | 177 | ```typescript 178 | this.healthData 179 | .startMonitoring({ 180 | dataType: 'heartRate', 181 | enableBackgroundUpdates: true, 182 | backgroundUpdateFrequency: 'immediate', 183 | onUpdate: (completionHandler: () => void) => { 184 | console.log('Our app was notified that health data changed, so querying...'); 185 | this.getData('heartRate', 'count/min').then(() => completionHandler()); 186 | }, 187 | }) 188 | .then(() => (this.resultToShow = `Started monitoring heartRate`)) 189 | .catch((error) => (this.resultToShow = error)); 190 | ``` 191 | 192 | ### `stopMonitoring` (iOS only, for now) 193 | 194 | It's best to call this method in case you no longer wish to receive notifications when health data changes. 195 | 196 | ```typescript 197 | this.healthData 198 | .stopMonitoring({ 199 | dataType: 'heartRate', 200 | }) 201 | .then(() => (this.resultToShow = `Stopped monitoring heartRate`)) 202 | .catch((error) => (this.resultToShow = error)); 203 | ``` 204 | 205 | ## Available Data Types 206 | 207 | With version 1.0.0 these are the supported types of data you can read. Also, make sure you pass in the correct `unit`. 208 | 209 | Note that you are responsible for passing the correct `unit`, although there's only 1 option for each type. _Well actually, the `unit` is ignored on Android at the moment, and on iOS there are undocumented types you can pass in (fi. `mi` for `distance`)._ 210 | 211 | The reason is I intend to support more units per type, but that is yet to be implemented... so it's for the sake of future backward-compatibility! 🤯 212 | 213 | | TypeOfData | Unit | GoogleFit Data Type | Apple HealthKit Data Type | 214 | | ------------- | --------- | -------------------------- | ------------------------------------------------ | 215 | | distance | m | `TYPE_DISTANCE_DELTA` | `HKQuantityTypeIdentifierDistanceWalkingRunning` | 216 | | steps | count | `TYPE_STEP_COUNT_DELTA` | `HKQuantityTypeIdentifierStepCount` | 217 | | calories | count | `TYPE_CALORIES_EXPENDED` | `HKQuantityTypeIdentifierActiveEnergyBurned` | 218 | | height | m | `TYPE_HEIGHT` | `HKQuantityTypeIdentifierHeight` | 219 | | weight | kg | `TYPE_WEIGHT` | `HKQuantityTypeIdentifierBodyMass` | 220 | | heartRate | count/min | `TYPE_HEART_RATE_BPM` | `HKQuantityTypeIdentifierHeartRate` | 221 | | fatPercentage | % | `TYPE_BODY_FAT_PERCENTAGE` | `HKQuantityTypeIdentifierBodyFatPercentage` | 222 | 223 | ## Credits 224 | 225 | - [Filipe Mendes](https://github.com/filipemendes1994/) for a superb first version of this repo, while working for SPMS, Shared Services for Ministry of Health (of Portugal). He kindly transferred this repo to me when he no longer had time to maintain it. 226 | - Daniel Leal, for [a great PR](https://github.com/EddyVerbruggen/nativescript-health-data/pull/4). 227 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /packages/health-data/README.md: -------------------------------------------------------------------------------- 1 | 2 | 21 |

nativescript-health-data

22 |

23 | Downloads per month 24 | NPM Version 25 |

26 | 27 |

28 | Health Data plugin for Nativescript, using Google Fit and Apple HealthKit.
29 | 30 |

31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | [](#table-of-contents) 39 | 40 | ## Table of Contents 41 | 42 | * [Installation](#installation) 43 | * [Configuration](#configuration) 44 | * [Android](#android) 45 | * [iOS](#ios) 46 | * [API](#api) 47 | * [`isAvailable`](#isavailable) 48 | * [`isAuthorized`](#isauthorized) 49 | * [`requestAuthorization`](#requestauthorization) 50 | * [`query`](#query) 51 | * [`queryAggregateData`](#queryaggregatedata) 52 | * [`startMonitoring` (iOS only, for now)](#startmonitoring-ios-only-for-now) 53 | * [`stopMonitoring` (iOS only, for now)](#stopmonitoring-ios-only-for-now) 54 | * [Available Data Types](#available-data-types) 55 | * [Credits](#credits) 56 | * [Examples:](#examples) 57 | * [Demos and Development](#demos-and-development) 58 | * [Setup](#setup) 59 | * [Build](#build) 60 | * [Demos](#demos) 61 | * [Questions](#questions) 62 | 63 | 64 | [](#installation) 65 | 66 | ## Installation 67 | Run the following command from the root of your project: 68 | 69 | `ns plugin add nativescript-health-data` 70 | 71 | 72 | [](#configuration) 73 | 74 | ## Configuration 75 | 76 | ### Android 77 | 78 | Google Fit API Key - Go to the [Google Developers Console](https://console.developers.google.com/), create a project, and enable the `Fitness API`. 79 | Then under `Credentials`, create a `Fitness API` OAuth2 client ID for an Android App (select `User data` and press the `What credentials do I need?` button). 80 | If you are using Linux/maxOS, generate your SHA1-key with the code below. 81 | 82 | ```shell 83 | keytool -exportcert -keystore path-to-debug-or-production-keystore -list -v 84 | ``` 85 | 86 | > Note that the default (debug) keystore password is empty. 87 | 88 | ### iOS 89 | 90 | Make sure you enable the `HealthKit` entitlement in your app ID. 91 | 92 | 93 | [](#api) 94 | 95 | ## API 96 | 97 | The examples below are all in TypeScript, and the [demo](https://github.com/EddyVerbruggen/nativescript-health-data/tree/master/demo-ng) was developed in Nativescript w/ Angular. 98 | 99 | This is how you can import and instantiate the plugin, all examples expect this setup: 100 | 101 | ```typescript 102 | import { AggregateBy, HealthData, HealthDataType } from 'nativescript-health-data'; 103 | 104 | export class MyHealthyClass { 105 | private healthData: HealthData; 106 | 107 | constructor() { 108 | this.healthData = new HealthData(); 109 | } 110 | } 111 | ``` 112 | 113 | ### `isAvailable` 114 | 115 | This tells you whether or not the device supports Health Data. On iOS this is probably always `true`. 116 | On Android the user will be prompted to (automatically) update their Play Services version in case it's not sufficiently up to date. 117 | If you don't want this behavior, pass false to this function, as shown below. 118 | 119 | ```typescript 120 | this.healthData.isAvailable(false).then((available) => console.log(available)); 121 | ``` 122 | 123 | ### `isAuthorized` 124 | 125 | This function (and the next one) takes an `Array` of `HealthDataType`'s. Each of those has a `name` and an `accessType`. 126 | 127 | - The `name` can be one of the ['Available Data Types'](#available-data-types). 128 | - The accessType can be one of `read`, `write`, or `readAndWrite` (note that this plugin currently only supports reading data, but that will change). 129 | 130 | > iOS is a bit silly here: if you've only requested 'read' access, you'll never get a `true` response from this method. [Details here.](https://stackoverflow.com/a/29128231/2596974) 131 | 132 | ```typescript 133 | this.healthData 134 | .isAuthorized([{ name: 'steps', accessType: 'read' }]) 135 | .then((authorized) => console.log(authorized)); 136 | ``` 137 | 138 | ### `requestAuthorization` 139 | 140 | This function takes the same argument as `isAuthorized`, and will trigger a consent screen in case the user hasn't previously authorized your app to access any of the passed `HealthDataType`'s. 141 | 142 | Note that this plugin currently only supports reading data, but that will change. 143 | 144 | ```typescript 145 | const types: Array = [ 146 | { name: 'height', accessType: 'write' }, 147 | { name: 'weight', accessType: 'readAndWrite' }, 148 | { name: 'steps', accessType: 'read' }, 149 | { name: 'distance', accessType: 'read' }, 150 | ]; 151 | 152 | this.healthData 153 | .requestAuthorization(types) 154 | .then((authorized) => console.log(authorized)) 155 | .catch((error) => console.log('Request auth error: ', error)); 156 | ``` 157 | 158 | ### `query` 159 | 160 | Mandatory properties are `startData`, `endDate`, and `dataType`. 161 | The `dataType` must be one of the ['Available Data Types'](#available-data-types). 162 | 163 | By default data is not aggregated, so all individual datapoints are returned. 164 | This plugin however offers a way to aggregate the data by either `hour`, `day`, or `sourceAndDay`, 165 | the latter will enable you to read daily data per source (Fitbit, Nike Run Club, manual entry, etc). 166 | 167 | If you didn't run `requestAuthorization` before running `query`, 168 | the plugin will run `requestAuthorization` for you (for the requested `dataType`). You're welcome. 😉 169 | 170 | ```typescript 171 | this.healthData 172 | .query({ 173 | startDate: new Date(new Date().getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days ago 174 | endDate: new Date(), // now 175 | dataType: 'steps', // equal to the 'name' property of 'HealthDataType' 176 | unit: 'count', // make sure this is compatible with the 'dataType' (see below) 177 | aggregateBy: 'day', // optional, one of: "hour", "day", "sourceAndDay" 178 | sortOrder: 'desc', // optional, default "asc" 179 | }) 180 | .then((result) => console.log(JSON.stringify(result))) 181 | .catch((error) => (this.resultToShow = error)); 182 | ``` 183 | 184 | ### `queryAggregateData` 185 | 186 | Difference between `query` and `queryAggregateData`: if you use `query`, you will probably find that the number of steps (or other types of data) do not match those shown by the Google Fit and Apple Health apps. If you wanted to accurately compute the user's data then use: `queryAggregateData` 187 | 188 | Mandatory properties are `startData`, `endDate`, and `dataType`. 189 | The `dataType` must be one of the ['Available Data Types'](#available-data-types). 190 | 191 | By default data is aggregated by `day`. 192 | This plugin however offers a way to aggregate the data by either `hour` and `day`. (`month` and `year` available soon) 193 | 194 | Note that `queryAggregateData` only supports `steps`, `calories` and `distance` on Android. (More data types available soon). 195 | 196 | If you didn't run `requestAuthorization` before running `query`, 197 | the plugin will run `requestAuthorization` for you (for the requested `dataType`). You're welcome. 😉 198 | 199 | ```typescript 200 | this.healthData 201 | .queryAggregateData({ 202 | startDate: new Date(new Date().getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days ago 203 | endDate: new Date(), // now 204 | dataType: 'steps', // equal to the 'name' property of 'HealthDataType' 205 | unit: 'count', // make sure this is compatible with the 'dataType' (see below) 206 | aggregateBy: 'day', // optional, one of: "hour", "day" ; default: "day" 207 | }) 208 | .then((result) => console.log(JSON.stringify(result))) 209 | .catch((error) => (this.resultToShow = error)); 210 | ``` 211 | 212 | ### `startMonitoring` (iOS only, for now) 213 | 214 | If you want to be notified when health data was changed, you can monitor specific types. 215 | This even works when your app is in the background, with `enableBackgroundUpdates: true`. 216 | Note that iOS will wake up your app so you can act upon this notification (in the `onUpdate` function by fi. querying recent changes to this data type), 217 | but in return you are responsible for telling iOS you're done. So make sure you invoke the `completionHandler` as shown below. 218 | 219 | Not all data types support `backgroundUpdateFrequency: "immediate"`, 220 | so your app may not always be invoked immediately when data is added/deleted in HealthKit. 221 | 222 | > Background notifications probably don't work on the iOS simulator, so please test those on a real device. 223 | 224 | ```typescript 225 | this.healthData 226 | .startMonitoring({ 227 | dataType: 'heartRate', 228 | enableBackgroundUpdates: true, 229 | backgroundUpdateFrequency: 'immediate', 230 | onUpdate: (completionHandler: () => void) => { 231 | console.log('Our app was notified that health data changed, so querying...'); 232 | this.getData('heartRate', 'count/min').then(() => completionHandler()); 233 | }, 234 | }) 235 | .then(() => (this.resultToShow = `Started monitoring heartRate`)) 236 | .catch((error) => (this.resultToShow = error)); 237 | ``` 238 | 239 | ### `stopMonitoring` (iOS only, for now) 240 | 241 | It's best to call this method in case you no longer wish to receive notifications when health data changes. 242 | 243 | ```typescript 244 | this.healthData 245 | .stopMonitoring({ 246 | dataType: 'heartRate', 247 | }) 248 | .then(() => (this.resultToShow = `Stopped monitoring heartRate`)) 249 | .catch((error) => (this.resultToShow = error)); 250 | ``` 251 | 252 | 253 | [](#available-data-types) 254 | 255 | ## Available Data Types 256 | 257 | With version 1.0.0 these are the supported types of data you can read. Also, make sure you pass in the correct `unit`. 258 | 259 | Note that you are responsible for passing the correct `unit`, although there's only 1 option for each type. _Well actually, the `unit` is ignored on Android at the moment, and on iOS there are undocumented types you can pass in (fi. `mi` for `distance`)._ 260 | 261 | The reason is I intend to support more units per type, but that is yet to be implemented... so it's for the sake of future backward-compatibility! 🤯 262 | 263 | | TypeOfData | Unit | GoogleFit Data Type | Apple HealthKit Data Type | 264 | | ------------- | --------- | -------------------------- | ------------------------------------------------ | 265 | | distance | m | `TYPE_DISTANCE_DELTA` | `HKQuantityTypeIdentifierDistanceWalkingRunning` | 266 | | steps | count | `TYPE_STEP_COUNT_DELTA` | `HKQuantityTypeIdentifierStepCount` | 267 | | calories | count | `TYPE_CALORIES_EXPENDED` | `HKQuantityTypeIdentifierActiveEnergyBurned` | 268 | | height | m | `TYPE_HEIGHT` | `HKQuantityTypeIdentifierHeight` | 269 | | weight | kg | `TYPE_WEIGHT` | `HKQuantityTypeIdentifierBodyMass` | 270 | | heartRate | count/min | `TYPE_HEART_RATE_BPM` | `HKQuantityTypeIdentifierHeartRate` | 271 | | fatPercentage | % | `TYPE_BODY_FAT_PERCENTAGE` | `HKQuantityTypeIdentifierBodyFatPercentage` | 272 | 273 | 274 | [](#credits) 275 | 276 | ## Credits 277 | 278 | - [Filipe Mendes](https://github.com/filipemendes1994/) for a superb first version of this repo, while working for SPMS, Shared Services for Ministry of Health (of Portugal). He kindly transferred this repo to me when he no longer had time to maintain it. 279 | - Daniel Leal, for [a great PR](https://github.com/EddyVerbruggen/nativescript-health-data/pull/4). 280 | 281 | ### Examples: 282 | 283 | - [Basic Drawer](demo-snippets/vue/BasicDrawer.vue) 284 | - A basic sliding drawer. 285 | - [All Sides](demo-snippets/vue/AllSides.vue) 286 | - An example of drawers on all sides: left, right, top, bottom. 287 | 288 | 289 | [](#demos-and-development) 290 | 291 | ## Demos and Development 292 | 293 | 294 | ### Setup 295 | 296 | To run the demos, you must clone this repo **recursively**. 297 | 298 | ``` 299 | git clone https://github.com/nativescript-health-data.git --recursive 300 | ``` 301 | 302 | **Install Dependencies:** 303 | ```bash 304 | npm i # or 'yarn install' or 'pnpm install' 305 | ``` 306 | 307 | **Interactive Menu:** 308 | 309 | To start the interactive menu, run `npm start` (or `yarn start` or `pnpm start`). This will list all of the commonly used scripts. 310 | 311 | ### Build 312 | 313 | ```bash 314 | npm run build 315 | 316 | npm run build.angular # or for Angular 317 | ``` 318 | 319 | ### Demos 320 | 321 | ```bash 322 | npm run demo.[ng|react|svelte|vue].[ios|android] 323 | 324 | npm run demo.svelte.ios # Example 325 | ``` 326 | 327 | [](#questions) 328 | 329 | ## Questions 330 | 331 | If you have any questions/issues/comments please feel free to create an issue or start a conversation in the [NativeScript Community Discord](https://nativescript.org/discord). -------------------------------------------------------------------------------- /src/health-data/index.android.ts: -------------------------------------------------------------------------------- 1 | import { Common, HealthDataApi, HealthDataType, QueryRequest, ResponseItem, StartMonitoringRequest, StopMonitoringRequest } from './index.common'; 2 | import { AndroidActivityResultEventData, AndroidApplication, Application, Utils } from '@nativescript/core'; 3 | 4 | const GOOGLE_FIT_PERMISSIONS_REQUEST_CODE = 2; 5 | 6 | // const AppPackageName = useAndroidX() ? global.androidx.core.app : android.support.v4.app; 7 | // const ContentPackageName = useAndroidX() ? global.androidx.core.content : android.support.v4.content; 8 | 9 | // android imports 10 | const DataReadRequest = com.google.android.gms.fitness.request.DataReadRequest; 11 | const DataSource = com.google.android.gms.fitness.data.DataSource; 12 | const DataType = com.google.android.gms.fitness.data.DataType; 13 | const Fitness = com.google.android.gms.fitness.Fitness; 14 | const GoogleApiAvailability = com.google.android.gms.common.GoogleApiAvailability; 15 | const TimeUnit = global.java.util.concurrent.TimeUnit; 16 | const FitnessOptions = com.google.android.gms.fitness.FitnessOptions; 17 | const GoogleSignIn = com.google.android.gms.auth.api.signin.GoogleSignIn; 18 | 19 | export class HealthData extends Common implements HealthDataApi { 20 | isAvailable(updateGooglePlayServicesIfNeeded = true): Promise { 21 | return new Promise((resolve, reject) => { 22 | const gApi = GoogleApiAvailability.getInstance(); 23 | const apiResult = gApi.isGooglePlayServicesAvailable(Utils.ad.getApplicationContext()); 24 | const available = apiResult === com.google.android.gms.common.ConnectionResult.SUCCESS; 25 | if (!available && updateGooglePlayServicesIfNeeded && gApi.isUserResolvableError(apiResult)) { 26 | // show a dialog offering the user to update (no need to wait for it to finish) 27 | gApi.showErrorDialogFragment( 28 | Application.android.foregroundActivity || Application.android.startActivity, 29 | apiResult, 30 | 1, 31 | new android.content.DialogInterface.OnCancelListener({ 32 | onCancel: (dialogInterface) => console.log('Google Play Services update dialog was canceled') 33 | }) 34 | ); 35 | } 36 | resolve(available); 37 | }); 38 | } 39 | 40 | isAuthorized(types: HealthDataType[]): Promise { 41 | return new Promise((resolve, reject) => { 42 | const fitnessOptionsBuilder = FitnessOptions.builder(); 43 | 44 | types.filter((t) => t.accessType === 'read' || t.accessType === 'readAndWrite').forEach((t) => fitnessOptionsBuilder.addDataType(this.getDataType(t.name), FitnessOptions.ACCESS_READ)); 45 | types.filter((t) => t.accessType === 'write' || t.accessType === 'readAndWrite').forEach((t) => fitnessOptionsBuilder.addDataType(this.getDataType(t.name), FitnessOptions.ACCESS_WRITE)); 46 | 47 | resolve(GoogleSignIn.hasPermissions(GoogleSignIn.getLastSignedInAccount(Application.android.foregroundActivity || Application.android.startActivity), fitnessOptionsBuilder.build())); 48 | }); 49 | } 50 | 51 | requestAuthorization(types: HealthDataType[]): Promise { 52 | return Promise.all([this.requestHardwarePermissions(), this.requestAuthorizationInternal(types)]).then((results) => Promise.resolve(results[0] && results[1])); 53 | } 54 | 55 | requestAuthorizationInternal(types: HealthDataType[]): Promise { 56 | return new Promise((resolve, reject) => { 57 | const fitnessOptionsBuilder = FitnessOptions.builder(); 58 | 59 | types.filter((t) => t.accessType === 'read' || t.accessType === 'readAndWrite').forEach((t) => fitnessOptionsBuilder.addDataType(this.getDataType(t.name), FitnessOptions.ACCESS_READ)); 60 | types.filter((t) => t.accessType === 'write' || t.accessType === 'readAndWrite').forEach((t) => fitnessOptionsBuilder.addDataType(this.getDataType(t.name), FitnessOptions.ACCESS_WRITE)); 61 | 62 | const fitnessOptions = fitnessOptionsBuilder.build(); 63 | 64 | if (GoogleSignIn.hasPermissions(GoogleSignIn.getLastSignedInAccount(Application.android.foregroundActivity || Application.android.startActivity), fitnessOptions)) { 65 | resolve(true); 66 | return; 67 | } 68 | 69 | const activityResultHandler = (args: AndroidActivityResultEventData) => { 70 | Application.android.off(AndroidApplication.activityResultEvent, activityResultHandler); 71 | resolve(args.requestCode === GOOGLE_FIT_PERMISSIONS_REQUEST_CODE && args.resultCode === android.app.Activity.RESULT_OK); 72 | }; 73 | Application.android.on(AndroidApplication.activityResultEvent, activityResultHandler); 74 | 75 | GoogleSignIn.requestPermissions( 76 | Application.android.foregroundActivity, 77 | GOOGLE_FIT_PERMISSIONS_REQUEST_CODE, 78 | GoogleSignIn.getLastSignedInAccount(Application.android.foregroundActivity || Application.android.startActivity), 79 | fitnessOptions 80 | ); 81 | }); 82 | } 83 | 84 | query(opts: QueryRequest): Promise { 85 | return new Promise((resolve, reject) => { 86 | try { 87 | // make sure the user is authorized 88 | this.requestAuthorizationInternal([{ name: opts.dataType, accessType: 'read' }]).then((authorized) => { 89 | if (!authorized) { 90 | reject('Not authorized'); 91 | return; 92 | } 93 | 94 | const readRequest = new DataReadRequest.Builder() 95 | // using 'read' instead of 'aggregate' for now, for more finegrain control 96 | // .aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA) 97 | // .bucketByTime(1, TimeUnit.HOURS) 98 | .read(this.getDataType(opts.dataType)) 99 | .setTimeRange(opts.startDate.getTime(), opts.endDate.getTime(), TimeUnit.MILLISECONDS) 100 | .build(); 101 | 102 | Fitness.getHistoryClient( 103 | Application.android.foregroundActivity || Application.android.startActivity, 104 | GoogleSignIn.getLastSignedInAccount(Application.android.foregroundActivity || Application.android.startActivity) 105 | ) 106 | .readData(readRequest) 107 | .addOnSuccessListener( 108 | new com.google.android.gms.tasks.OnSuccessListener({ 109 | onSuccess: (dataReadResponse: any /* com.google.android.gms.fitness.result.DataReadResponse */) => { 110 | resolve(this.parseData(dataReadResponse.getResult(), opts)); 111 | } 112 | }) 113 | ) 114 | .addOnFailureListener( 115 | new com.google.android.gms.tasks.OnFailureListener({ 116 | onFailure: (exception: any) => { 117 | reject(exception.getMessage()); 118 | } 119 | }) 120 | ) 121 | .addOnCompleteListener( 122 | new com.google.android.gms.tasks.OnCompleteListener({ 123 | onComplete: (task: any) => { 124 | // noop 125 | } 126 | }) 127 | ); 128 | }); 129 | } catch (e) { 130 | reject(e); 131 | } 132 | }); 133 | } 134 | 135 | queryAggregateData(opts: QueryRequest): Promise { 136 | return new Promise((resolve, reject) => { 137 | try { 138 | // make sure the user is authorized 139 | this.requestAuthorizationInternal([{ name: opts.dataType, accessType: 'read' }]).then((authorized) => { 140 | if (!authorized) { 141 | reject('Not authorized'); 142 | return; 143 | } 144 | let _aggregateBy; 145 | switch (opts.aggregateBy) { 146 | case 'hour': 147 | _aggregateBy = TimeUnit.HOURS; 148 | break; 149 | case 'day': 150 | _aggregateBy = TimeUnit.DAYS; 151 | break; 152 | default: 153 | _aggregateBy = TimeUnit.DAYS; 154 | } 155 | 156 | const myDataSource = new DataSource.Builder() 157 | .setDataType(this.getDataType(opts.dataType)) 158 | .setType(DataSource.TYPE_DERIVED) 159 | .setStreamName(this.getStreamName(opts.dataType)) 160 | .setAppPackageName('com.google.android.gms') 161 | .build(); 162 | 163 | const readRequest = new DataReadRequest.Builder() 164 | .aggregate(myDataSource, this.getAggregatedDataType(opts.dataType)) 165 | .setTimeRange(opts.startDate.getTime(), opts.endDate.getTime(), TimeUnit.MILLISECONDS) 166 | .bucketByTime(1, _aggregateBy) 167 | .enableServerQueries() 168 | .build(); 169 | 170 | Fitness.getHistoryClient( 171 | Application.android.foregroundActivity || Application.android.startActivity, 172 | GoogleSignIn.getLastSignedInAccount(Application.android.foregroundActivity || Application.android.startActivity) 173 | ) 174 | .readData(readRequest) 175 | .addOnSuccessListener( 176 | new com.google.android.gms.tasks.OnSuccessListener({ 177 | onSuccess: (dataReadResponse: any /* com.google.android.gms.fitness.result.DataReadResponse */) => { 178 | resolve(this.parseData(dataReadResponse.getResult(), opts)); 179 | } 180 | }) 181 | ) 182 | .addOnFailureListener( 183 | new com.google.android.gms.tasks.OnFailureListener({ 184 | onFailure: (exception: any) => { 185 | reject(exception.getMessage()); 186 | } 187 | }) 188 | ) 189 | .addOnCompleteListener( 190 | new com.google.android.gms.tasks.OnCompleteListener({ 191 | onComplete: (task: any) => { 192 | // noop 193 | } 194 | }) 195 | ); 196 | }); 197 | } catch (e) { 198 | reject(e); 199 | } 200 | }); 201 | } 202 | 203 | startMonitoring(opts: StartMonitoringRequest): Promise { 204 | return new Promise((resolve, reject) => { 205 | reject('Not supported'); 206 | }); 207 | } 208 | 209 | stopMonitoring(opts: StopMonitoringRequest): Promise { 210 | return new Promise((resolve, reject) => { 211 | reject('Not supported'); 212 | }); 213 | } 214 | 215 | private parseData(readResult: any /* com.google.android.gms.fitness.result.DataReadResult */, opts: QueryRequest) { 216 | let result = []; 217 | if (readResult.getBuckets().size() > 0) { 218 | for (let indexBucket = 0; indexBucket < readResult.getBuckets().size(); indexBucket++) { 219 | const dataSets = readResult.getBuckets().get(indexBucket).getDataSets(); 220 | for (let indexDataSet = 0; indexDataSet < dataSets.size(); indexDataSet++) { 221 | result = result.concat(this.dumpDataSet(dataSets.get(indexDataSet), opts)); 222 | } 223 | } 224 | } else if (readResult.getDataSets().size() > 0) { 225 | for (let index = 0; index < readResult.getDataSets().size(); index++) { 226 | result = result.concat(this.dumpDataSet(readResult.getDataSets().get(index), opts)); 227 | } 228 | } 229 | 230 | // the result is sorted asc, so reverse in case that was requested 231 | if (opts.sortOrder === 'desc') { 232 | result = result.reverse(); 233 | } 234 | 235 | return result; 236 | } 237 | 238 | private dumpDataSet(dataSet: any /* com.google.android.gms.fitness.data.DataSet */, opts: QueryRequest) { 239 | const parsedData: ResponseItem[] = []; 240 | const packageManager = Utils.ad.getApplicationContext().getPackageManager(); 241 | const packageToAppNameCache = new Map(); 242 | 243 | for (let index = 0; index < dataSet.getDataPoints().size(); index++) { 244 | const pos = dataSet.getDataPoints().get(index); 245 | 246 | for (let indexField = 0; indexField < pos.getDataType().getFields().size(); indexField++) { 247 | const field = pos.getDataType().getFields().get(indexField); 248 | const value = pos.getValue(field); 249 | 250 | // Note that the bit below is not yet required - per the README 251 | /* 252 | // Google Fit seems to have fixed unit types, so either: 253 | // - convert these in the plugin, or 254 | // - report the unit and have the user handle it (opting for this one for now) 255 | let unit = opts.unit; 256 | if (opts.dataType === "distance") { 257 | unit = "m"; 258 | } 259 | */ 260 | 261 | const packageName = pos.getOriginalDataSource().getAppPackageName(); 262 | let source = packageName ? packageName : pos.getOriginalDataSource().getStreamName(); 263 | if (packageName) { 264 | if (!packageToAppNameCache.has(packageName)) { 265 | try { 266 | const appName = packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName, android.content.pm.PackageManager.GET_META_DATA)); 267 | packageToAppNameCache.set(packageName, appName); 268 | } catch (ignore) { 269 | // the app has probably been uninstalled, so use the package name 270 | packageToAppNameCache.set(packageName, packageName); 271 | } 272 | } 273 | source = packageToAppNameCache.get(packageName); 274 | } 275 | 276 | parsedData.push({ 277 | start: new Date(pos.getStartTime(TimeUnit.MILLISECONDS)), 278 | end: new Date(pos.getEndTime(TimeUnit.MILLISECONDS)), 279 | // https://developers.google.com/android/reference/com/google/android/gms/fitness/data/Value 280 | value: value.getFormat() === 1 ? value.asInt() : Math.round(value.asFloat() * 1000) / 1000, 281 | unit: opts.unit, 282 | source 283 | } as ResponseItem); 284 | } 285 | } 286 | 287 | return this.aggregate(parsedData, opts.aggregateBy); 288 | } 289 | 290 | private getAggregatedDataType(pluginType: string): any /*com.google.android.gms.fitness.data.DataType */ { 291 | // TODO check if the passed type is ok 292 | const typeOfData = acceptableDataTypesForCommonity[pluginType]; 293 | return aggregatedDataTypes[typeOfData]; 294 | } 295 | 296 | private getDataType(pluginType: string): any /*com.google.android.gms.fitness.data.DataType */ { 297 | // TODO check if the passed type is ok 298 | const typeOfData = acceptableDataTypesForCommonity[pluginType]; 299 | return dataTypes[typeOfData]; 300 | } 301 | 302 | private getStreamName(pluginType: string): any /*com.google.android.gms.fitness.data.DataType */ { 303 | // TODO check if the passed type is ok 304 | const typeOfData = acceptableDataTypesForCommonity[pluginType]; 305 | return streamNames[typeOfData]; 306 | } 307 | 308 | private requestHardwarePermissions(): Promise { 309 | return this.requestPermissionFor(this.permissionsNeeded().filter((permission) => !this.wasPermissionGranted(permission))); 310 | } 311 | 312 | private wasPermissionGranted(permission: any) { 313 | let hasPermission = android.os.Build.VERSION.SDK_INT < 23; // Android M. (6.0) 314 | if (!hasPermission) { 315 | hasPermission = android.content.pm.PackageManager.PERMISSION_GRANTED === androidx.core.content.ContextCompat.checkSelfPermission(Utils.ad.getApplicationContext(), permission); 316 | } 317 | return hasPermission; 318 | } 319 | 320 | private wasPermissionsGrantedForAll(): boolean { 321 | return this.permissionsNeeded().every((permission) => this.wasPermissionGranted(permission)); 322 | } 323 | 324 | private requestPermissionFor(permissions: any[]): Promise { 325 | return new Promise((resolve, reject) => { 326 | if (!this.wasPermissionsGrantedForAll()) { 327 | const activityRequestPermissionsHandler = (args) => { 328 | Application.android.off(AndroidApplication.activityRequestPermissionsEvent, activityRequestPermissionsHandler); 329 | resolve(true); 330 | }; 331 | Application.android.on(AndroidApplication.activityRequestPermissionsEvent, activityRequestPermissionsHandler); 332 | 333 | androidx.core.app.ActivityCompat.requestPermissions( 334 | Application.android.foregroundActivity, 335 | permissions, 336 | 235 // irrelevant since we simply invoke onPermissionGranted 337 | ); 338 | } else { 339 | resolve(true); 340 | } 341 | }); 342 | } 343 | 344 | private permissionsNeeded(): any[] { 345 | const permissions = [android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_NETWORK_STATE, android.Manifest.permission.GET_ACCOUNTS]; 346 | 347 | if (android.os.Build.VERSION.SDK_INT > 19) { 348 | permissions.push(android.Manifest.permission.BODY_SENSORS); 349 | } 350 | 351 | return permissions; 352 | } 353 | } 354 | 355 | const aggregatedDataTypes = { 356 | TYPE_STEP_COUNT_DELTA: DataType.AGGREGATE_STEP_COUNT_DELTA, 357 | TYPE_DISTANCE_DELTA: DataType.AGGREGATE_DISTANCE_DELTA, 358 | TYPE_CALORIES_EXPENDED: DataType.AGGREGATE_CALORIES_EXPENDED, 359 | TYPE_HEIGHT: DataType.TYPE_HEIGHT, 360 | // TYPE_WEIGHT: DataType.AGGREGATE_WEIGHT_SUMMARY, 361 | TYPE_WEIGHT: DataType.TYPE_WEIGHT, 362 | TYPE_HEART_RATE_BPM: DataType.AGGREGATE_HEART_RATE_SUMMARY, 363 | TYPE_BODY_FAT_PERCENTAGE: DataType.AGGREGATE_BODY_FAT_PERCENTAGE_SUMMARY, 364 | TYPE_NUTRITION: DataType.AGGREGATE_NUTRITION_SUMMARY, 365 | TYPE_HEART_POINTS: DataType.TYPE_HEART_POINTS 366 | }; 367 | 368 | const dataTypes = { 369 | TYPE_STEP_COUNT_DELTA: DataType.TYPE_STEP_COUNT_DELTA, 370 | TYPE_DISTANCE_DELTA: DataType.TYPE_DISTANCE_DELTA, 371 | TYPE_CALORIES_EXPENDED: DataType.TYPE_CALORIES_EXPENDED, 372 | TYPE_HEIGHT: DataType.TYPE_HEIGHT, 373 | TYPE_WEIGHT: DataType.TYPE_WEIGHT, 374 | TYPE_HEART_RATE_BPM: DataType.TYPE_HEART_RATE_BPM, 375 | TYPE_BODY_FAT_PERCENTAGE: DataType.TYPE_BODY_FAT_PERCENTAGE, 376 | TYPE_NUTRITION: DataType.TYPE_NUTRITION, 377 | TYPE_HEART_POINTS: DataType.TYPE_HEART_POINTS 378 | }; 379 | 380 | const streamNames = { 381 | TYPE_STEP_COUNT_DELTA: 'estimated_steps', 382 | TYPE_DISTANCE_DELTA: 'merge_distance_delta', 383 | TYPE_CALORIES_EXPENDED: 'merge_calories_expended' 384 | }; 385 | 386 | const acceptableDataTypesForCommonity = { 387 | steps: 'TYPE_STEP_COUNT_DELTA', 388 | distance: 'TYPE_DISTANCE_DELTA', 389 | calories: 'TYPE_CALORIES_EXPENDED', 390 | // "activity": DataType.TYPE_ACTIVITY_SEGMENT, 391 | height: 'TYPE_HEIGHT', 392 | weight: 'TYPE_WEIGHT', 393 | heartRate: 'TYPE_HEART_RATE_BPM', 394 | fatPercentage: 'TYPE_BODY_FAT_PERCENTAGE', 395 | cardio: 'TYPE_HEART_POINTS' 396 | // "nutrition": "TYPE_NUTRITION", 397 | }; 398 | -------------------------------------------------------------------------------- /src/health-data/index.ios.ts: -------------------------------------------------------------------------------- 1 | import { BackgroundUpdateFrequency, Common, HealthDataApi, HealthDataType, QueryRequest, ResponseItem, StartMonitoringRequest, StopMonitoringRequest } from './index.common'; 2 | 3 | export class HealthData extends Common implements HealthDataApi { 4 | private healthStore: HKHealthStore; 5 | private monitorQueries: Map = new Map(); 6 | 7 | constructor() { 8 | super(); 9 | if (HKHealthStore.isHealthDataAvailable()) { 10 | this.healthStore = HKHealthStore.new(); 11 | } 12 | } 13 | 14 | isAvailable(updateGooglePlayServicesIfNeeded?: /* for Android */ boolean): Promise { 15 | return new Promise((resolve, reject) => { 16 | resolve(this.healthStore !== undefined); 17 | }); 18 | } 19 | 20 | isAuthorized(types: HealthDataType[]): Promise { 21 | return new Promise((resolve, reject) => { 22 | if (this.healthStore === undefined) { 23 | reject('Health not available'); 24 | return; 25 | } 26 | 27 | let authorized = true; 28 | types.forEach((t) => { 29 | authorized = authorized && this.healthStore.authorizationStatusForType(this.resolveDataType(acceptableDataTypes[t.name])) === HKAuthorizationStatus.SharingAuthorized; 30 | }); 31 | resolve(authorized); 32 | }); 33 | } 34 | 35 | requestAuthorization(types: HealthDataType[]): Promise { 36 | return new Promise((resolve, reject) => { 37 | if (this.healthStore === undefined) { 38 | reject('Health not available'); 39 | return; 40 | } 41 | 42 | const readDataTypes = NSMutableSet.alloc().init(); 43 | const writeDataTypes = NSMutableSet.alloc().init(); 44 | types.filter((t) => t.accessType === 'read' || t.accessType === 'readAndWrite').forEach((t) => readDataTypes.addObject(this.resolveDataType(acceptableDataTypes[t.name]))); 45 | types.filter((t) => t.accessType === 'write' || t.accessType === 'readAndWrite').forEach((t) => writeDataTypes.addObject(this.resolveDataType(acceptableDataTypes[t.name]))); 46 | 47 | this.healthStore.requestAuthorizationToShareTypesReadTypesCompletion(writeDataTypes.copy(), readDataTypes, (success, error) => { 48 | if (success) { 49 | resolve(true); 50 | } else { 51 | reject('You do not have permissions for requested data type'); 52 | } 53 | }); 54 | }); 55 | } 56 | 57 | query(opts: QueryRequest): Promise { 58 | return new Promise((resolve, reject) => { 59 | // make sure the user is authorized 60 | this.requestAuthorization([{ name: opts.dataType, accessType: 'read' }]).then((authorized) => { 61 | if (!authorized) { 62 | reject('Not authorized'); 63 | return; 64 | } 65 | 66 | const typeOfData = acceptableDataTypes[opts.dataType]; 67 | if (quantityTypes[typeOfData] || categoryTypes[typeOfData]) { 68 | this.queryForQuantityOrCategoryData(typeOfData, opts, (res, error) => { 69 | if (error) { 70 | reject(error); 71 | } else { 72 | resolve(res); 73 | } 74 | }); 75 | // } else if (characteristicTypes[typeOfData]) { 76 | // resolve(this.queryForCharacteristicData(typeOfData)); 77 | } else { 78 | reject('Type not supported (yet)'); 79 | } 80 | }); 81 | }); 82 | } 83 | 84 | queryAggregateData(opts: QueryRequest): Promise { 85 | return new Promise((resolve, reject) => { 86 | // make sure the user is authorized 87 | this.requestAuthorization([{ name: opts.dataType, accessType: 'read' }]).then((authorized) => { 88 | if (!authorized) { 89 | reject('Not authorized'); 90 | return; 91 | } 92 | 93 | const typeOfData = acceptableDataTypes[opts.dataType]; 94 | if (quantityTypes[typeOfData] || categoryTypes[typeOfData]) { 95 | this.queryForStatisticsCollectionData(typeOfData, opts, (res, error) => { 96 | if (error) { 97 | reject(error); 98 | } else { 99 | resolve(res); 100 | } 101 | }); 102 | // } else if (characteristicTypes[typeOfData]) { 103 | // resolve(this.queryForCharacteristicData(typeOfData)); 104 | } else { 105 | reject('Type not supported (yet)'); 106 | } 107 | }); 108 | }); 109 | } 110 | 111 | startMonitoring(opts: StartMonitoringRequest): Promise { 112 | return new Promise((resolve, reject) => { 113 | // make sure the user is authorized 114 | this.requestAuthorization([{ name: opts.dataType, accessType: 'read' }]).then((authorized) => { 115 | if (!authorized) { 116 | reject('Not authorized'); 117 | return; 118 | } 119 | 120 | const typeOfData = acceptableDataTypes[opts.dataType]; 121 | if (quantityTypes[typeOfData] || categoryTypes[typeOfData]) { 122 | this.monitorData(typeOfData, opts); 123 | resolve(); 124 | } else { 125 | reject('Type not supported (yet)'); 126 | } 127 | }); 128 | }); 129 | } 130 | 131 | stopMonitoring(opts: StopMonitoringRequest): Promise { 132 | return new Promise((resolve, reject) => { 133 | const typeOfData = acceptableDataTypes[opts.dataType]; 134 | const objectType = this.resolveDataType(typeOfData); 135 | 136 | if (quantityTypes[typeOfData] || categoryTypes[typeOfData]) { 137 | const rememberedQuery = this.monitorQueries.get(opts.dataType); 138 | if (rememberedQuery) { 139 | this.healthStore.stopQuery(rememberedQuery); 140 | this.monitorQueries.delete(opts.dataType); 141 | } 142 | 143 | this.healthStore.disableBackgroundDeliveryForTypeWithCompletion(objectType, (success: boolean, error: NSError) => { 144 | success ? resolve() : reject(error.localizedDescription); 145 | }); 146 | } else { 147 | reject('Type not supported (yet)'); 148 | } 149 | }); 150 | } 151 | 152 | private resolveDataType(type: string): HKObjectType { 153 | if (quantityTypes[type]) { 154 | return HKObjectType.quantityTypeForIdentifier(quantityTypes[type]); 155 | } else if (characteristicTypes[type]) { 156 | return HKObjectType.characteristicTypeForIdentifier(characteristicTypes[type]); 157 | } else if (categoryTypes[type]) { 158 | return HKObjectType.categoryTypeForIdentifier(categoryTypes[type]); 159 | } else { 160 | console.log('Constant not supported: ' + type); 161 | return null; 162 | } 163 | } 164 | 165 | private resolveQuantityType(type: string): HKQuantityType { 166 | if (quantityTypes[type]) { 167 | return HKQuantityType.quantityTypeForIdentifier(quantityTypes[type]); 168 | } else { 169 | console.log('Constant not supported: ' + type); 170 | return null; 171 | } 172 | } 173 | 174 | private queryForQuantityOrCategoryData(dataType: string, opts: QueryRequest, callback: (data: ResponseItem[], error: string) => void) { 175 | const objectType = this.resolveDataType(dataType); 176 | 177 | const predicate = HKQuery.predicateForSamplesWithStartDateEndDateOptions(opts.startDate, opts.endDate, HKQueryOptions.StrictStartDate); 178 | 179 | const endDateSortDescriptor = NSSortDescriptor.alloc().initWithKeyAscending(HKSampleSortIdentifierEndDate, opts.sortOrder !== 'desc'); 180 | const sortBy = NSArray.arrayWithObject(endDateSortDescriptor); 181 | 182 | // note that passing an invalid 'unitString' will crash the app (can't catch that error either) 183 | const unit = opts.unit ? HKUnit.unitFromString(opts.unit) : undefined; 184 | 185 | const query = HKSampleQuery.alloc().initWithSampleTypePredicateLimitSortDescriptorsResultsHandler( 186 | objectType as HKSampleType, 187 | predicate, 188 | null, 189 | sortBy, 190 | (query: HKSampleQuery, listResults: NSArray, error: NSError) => { 191 | if (listResults) { 192 | const parsedData: ResponseItem[] = []; 193 | 194 | for (let index = 0; index < listResults.count; index++) { 195 | const sample: HKSample = listResults.objectAtIndex(index); 196 | const { startDate, endDate, source } = sample; 197 | 198 | const resultItem = { 199 | source: source.name, 200 | unit: opts.unit, 201 | start: startDate, 202 | end: endDate 203 | }; 204 | 205 | // TODO other types, see https://github.com/Telerik-Verified-Plugins/HealthKit/blob/c6b15ea8096bae3e61dc71a3cb0098da44f411fd/src/ios/HealthKit.m#L1333 206 | if (sample instanceof HKCategorySample) { 207 | resultItem.value = sample.value; 208 | } else if (sample instanceof HKQuantitySample) { 209 | if ((sample).quantity.isCompatibleWithUnit(unit)) { 210 | resultItem.value = (sample).quantity.doubleValueForUnit(unit); 211 | } else { 212 | console.log('Incompatible unit passed: ' + opts.unit + ' (' + unit + ')'); 213 | } 214 | } 215 | 216 | parsedData.push(resultItem); 217 | } 218 | 219 | callback(this.aggregate(parsedData, opts.aggregateBy), null); 220 | } else { 221 | console.dir(error); 222 | callback(null, error.localizedDescription); 223 | } 224 | } 225 | ); 226 | this.healthStore.executeQuery(query); 227 | } 228 | 229 | private queryForStatisticsCollectionData(dataType: string, opts: QueryRequest, callback: (data: ResponseItem[], error: string) => void) { 230 | const objectType = this.resolveQuantityType(dataType); 231 | // note that passing an invalid 'unitString' will crash the app (can't catch that error either) 232 | const unit = opts.unit ? HKUnit.unitFromString(opts.unit) : undefined; 233 | const aggregate = NSDateComponents.alloc().init(); 234 | 235 | switch (opts.aggregateBy) { 236 | case 'hour': 237 | aggregate.hour = 1; 238 | aggregate.nanosecond = 100; 239 | break; 240 | case 'day': 241 | aggregate.day = 1; 242 | aggregate.nanosecond = 100; 243 | break; 244 | } 245 | 246 | const query = HKStatisticsCollectionQuery.alloc().initWithQuantityTypeQuantitySamplePredicateOptionsAnchorDateIntervalComponents(objectType, null, 16, opts.startDate, aggregate); 247 | query.initialResultsHandler = function (query: HKStatisticsCollectionQuery, listResults: HKStatisticsCollection, error: NSError) { 248 | const statsCollection = listResults; 249 | const parsedData: ResponseItem[] = []; 250 | statsCollection.enumerateStatisticsFromDateToDateWithBlock(opts.startDate, opts.endDate, function (statistics, stop) { 251 | if (statistics.sumQuantity()) { 252 | const startDate = statistics.startDate; 253 | const endDate = statistics.endDate; 254 | const resultItem = { 255 | unit: opts.unit, 256 | start: startDate, 257 | end: endDate 258 | }; 259 | resultItem.value = Math.floor(statistics.sumQuantity().doubleValueForUnit(unit)); 260 | parsedData.push(resultItem); 261 | } 262 | }); 263 | callback(parsedData, null); 264 | }; 265 | this.healthStore.executeQuery(query); 266 | } 267 | 268 | private monitorData(dataType: string, opts: StartMonitoringRequest): void { 269 | const objectType = this.resolveDataType(dataType); 270 | 271 | const query = HKObserverQuery.alloc().initWithSampleTypePredicateUpdateHandler(objectType as HKSampleType, null, (observerQuery: HKObserverQuery, handler: () => void, error: NSError) => { 272 | if (error) { 273 | opts.onError(error.localizedDescription); 274 | handler(); 275 | } else { 276 | // We need to tell iOS when our app is done background processing by calling the handler 277 | opts.onUpdate(() => handler()); 278 | } 279 | }); 280 | 281 | // remember this query, so we can stop it at a later time 282 | this.monitorQueries.set(opts.dataType, query); 283 | 284 | this.healthStore.executeQuery(query); 285 | 286 | if (opts.enableBackgroundUpdates) { 287 | this.healthStore.enableBackgroundDeliveryForTypeFrequencyWithCompletion(objectType, this.getHKUpdateFrequency(opts.backgroundUpdateFrequency), (success: boolean, error: NSError) => { 288 | if (!success) { 289 | opts.onError(error.localizedDescription); 290 | } 291 | }); 292 | } 293 | } 294 | 295 | private getHKUpdateFrequency(frequency: BackgroundUpdateFrequency): HKUpdateFrequency { 296 | if (frequency === 'weekly') { 297 | return HKUpdateFrequency.Weekly; 298 | } else if (frequency === 'daily') { 299 | return HKUpdateFrequency.Daily; 300 | } else if (frequency === 'hourly') { 301 | return HKUpdateFrequency.Hourly; 302 | } else { 303 | return HKUpdateFrequency.Immediate; 304 | } 305 | } 306 | 307 | private queryForCharacteristicData(dataType: string) { 308 | // console.log('ask for characteristic data ' + data); 309 | switch (characteristicTypes[dataType]) { 310 | case HKCharacteristicTypeIdentifierBiologicalSex: 311 | return { 312 | type: dataType, 313 | result: this.healthStore.biologicalSexWithError().biologicalSex 314 | }; 315 | case HKCharacteristicTypeIdentifierBloodType: 316 | return { 317 | type: dataType, 318 | result: this.healthStore.bloodTypeWithError().bloodType 319 | }; 320 | case HKCharacteristicTypeIdentifierDateOfBirth: 321 | return { 322 | type: dataType, 323 | result: this.healthStore.dateOfBirthComponentsWithError().date 324 | }; 325 | case HKCharacteristicTypeIdentifierFitzpatrickSkinType: 326 | return { 327 | type: dataType, 328 | result: this.healthStore.fitzpatrickSkinTypeWithError().skinType 329 | }; 330 | case HKCharacteristicTypeIdentifierWheelchairUse: 331 | return { 332 | type: dataType, 333 | result: this.healthStore.wheelchairUseWithError().wheelchairUse 334 | }; 335 | default: 336 | console.log('Characteristic not implemented!'); 337 | return null; 338 | } 339 | } 340 | } 341 | 342 | const quantityTypes = { 343 | activeEnergyBurned: HKQuantityTypeIdentifierActiveEnergyBurned, 344 | appleExerciseTime: HKQuantityTypeIdentifierAppleExerciseTime, 345 | basalBodyTemperature: HKQuantityTypeIdentifierBasalBodyTemperature, 346 | basalEnergyBurned: HKQuantityTypeIdentifierBasalEnergyBurned, 347 | bloodAlcoholContent: HKQuantityTypeIdentifierBloodAlcoholContent, 348 | bloodGlucose: HKQuantityTypeIdentifierBloodGlucose, 349 | bloodPressureDiastolic: HKQuantityTypeIdentifierBloodPressureDiastolic, 350 | bloodPressureSystolic: HKQuantityTypeIdentifierBloodPressureSystolic, 351 | bodyFatPercentage: HKQuantityTypeIdentifierBodyFatPercentage, 352 | bodyMass: HKQuantityTypeIdentifierBodyMass, 353 | bodyMassIndex: HKQuantityTypeIdentifierBodyMassIndex, 354 | bodyTemperature: HKQuantityTypeIdentifierBodyTemperature, 355 | dietaryBiotin: HKQuantityTypeIdentifierDietaryBiotin, 356 | dietaryCaffeine: HKQuantityTypeIdentifierDietaryCaffeine, 357 | dietaryCalcium: HKQuantityTypeIdentifierDietaryCalcium, 358 | dietaryCarbohydrates: HKQuantityTypeIdentifierDietaryCarbohydrates, 359 | dietaryChloride: HKQuantityTypeIdentifierDietaryChloride, 360 | dietaryCholesterol: HKQuantityTypeIdentifierDietaryCholesterol, 361 | dietaryChromium: HKQuantityTypeIdentifierDietaryChromium, 362 | dietaryCopper: HKQuantityTypeIdentifierDietaryCopper, 363 | dietaryEnergyConsumed: HKQuantityTypeIdentifierDietaryEnergyConsumed, 364 | dietaryFatMonounsaturated: HKQuantityTypeIdentifierDietaryFatMonounsaturated, 365 | dietaryFatPolyunsaturated: HKQuantityTypeIdentifierDietaryFatPolyunsaturated, 366 | dietaryFatSaturated: HKQuantityTypeIdentifierDietaryFatSaturated, 367 | dietaryFatTotal: HKQuantityTypeIdentifierDietaryFatTotal, 368 | dietaryFiber: HKQuantityTypeIdentifierDietaryFiber, 369 | dietaryFolate: HKQuantityTypeIdentifierDietaryFolate, 370 | dietaryIodine: HKQuantityTypeIdentifierDietaryIodine, 371 | dietaryIron: HKQuantityTypeIdentifierDietaryIron, 372 | dietaryMagnesium: HKQuantityTypeIdentifierDietaryMagnesium, 373 | dietaryManganese: HKQuantityTypeIdentifierDietaryManganese, 374 | dietaryaMolybdenum: HKQuantityTypeIdentifierDietaryMolybdenum, 375 | dietaryNiacin: HKQuantityTypeIdentifierDietaryNiacin, 376 | dietaryPantothenicAcid: HKQuantityTypeIdentifierDietaryPantothenicAcid, 377 | dietaryPhosphorus: HKQuantityTypeIdentifierDietaryPhosphorus, 378 | dietaryPotassium: HKQuantityTypeIdentifierDietaryPotassium, 379 | dietaryProtein: HKQuantityTypeIdentifierDietaryProtein, 380 | dietaryRiboflavin: HKQuantityTypeIdentifierDietaryRiboflavin, 381 | dietarySelenium: HKQuantityTypeIdentifierDietarySelenium, 382 | dietarySodium: HKQuantityTypeIdentifierDietarySodium, 383 | dietarySugar: HKQuantityTypeIdentifierDietarySugar, 384 | dietaryThiamin: HKQuantityTypeIdentifierDietaryThiamin, 385 | dietaryViataminA: HKQuantityTypeIdentifierDietaryVitaminA, 386 | dietaryVitaminB12: HKQuantityTypeIdentifierDietaryVitaminB12, 387 | dietaryVitaminB6: HKQuantityTypeIdentifierDietaryVitaminB6, 388 | dietaryVitaminC: HKQuantityTypeIdentifierDietaryVitaminC, 389 | dietaryVitaminD: HKQuantityTypeIdentifierDietaryVitaminD, 390 | dietaryVitaminE: HKQuantityTypeIdentifierDietaryVitaminE, 391 | dietaryVitaminK: HKQuantityTypeIdentifierDietaryVitaminK, 392 | dietaryWater: HKQuantityTypeIdentifierDietaryWater, 393 | dietaryZinc: HKQuantityTypeIdentifierDietaryZinc, 394 | distanceCycling: HKQuantityTypeIdentifierDistanceCycling, 395 | // distanceSwimming: HKQuantityTypeIdentifierDistanceSwimming, 396 | distanceWalkingRunning: HKQuantityTypeIdentifierDistanceWalkingRunning, 397 | // distanceWheelChair: HKQuantityTypeIdentifierDistanceWheelchair, 398 | electrodermalActivity: HKQuantityTypeIdentifierElectrodermalActivity, 399 | flightsClimbed: HKQuantityTypeIdentifierFlightsClimbed, 400 | forcedExpiratoryVolume1: HKQuantityTypeIdentifierForcedExpiratoryVolume1, 401 | forcedVitalCapacity: HKQuantityTypeIdentifierForcedVitalCapacity, 402 | heartRate: HKQuantityTypeIdentifierHeartRate, 403 | height: HKQuantityTypeIdentifierHeight, 404 | inhalerUsage: HKQuantityTypeIdentifierInhalerUsage, 405 | leanBodyMass: HKQuantityTypeIdentifierLeanBodyMass, 406 | nikeFuel: HKQuantityTypeIdentifierNikeFuel, 407 | numberOfTimesFallen: HKQuantityTypeIdentifierNumberOfTimesFallen, 408 | oxygenSaturation: HKQuantityTypeIdentifierOxygenSaturation, 409 | peakExpiratoryFlowRate: HKQuantityTypeIdentifierPeakExpiratoryFlowRate, 410 | peripheralPerfusionIndex: HKQuantityTypeIdentifierPeripheralPerfusionIndex, 411 | // pushCount: HKQuantityTypeIdentifierPushCount, 412 | respiratoryRate: HKQuantityTypeIdentifierRespiratoryRate, 413 | stepCount: HKQuantityTypeIdentifierStepCount, 414 | // swimmingStrokeCount: HKQuantityTypeIdentifierSwimmingStrokeCount, 415 | uvExposure: HKQuantityTypeIdentifierUVExposure 416 | }; 417 | 418 | if (typeof HKQuantityTypeIdentifierDistanceSwimming !== 'undefined') { 419 | Object.assign(quantityTypes, { 420 | distanceSwimming: HKQuantityTypeIdentifierDistanceSwimming 421 | }); 422 | } 423 | if (typeof HKQuantityTypeIdentifierDistanceWheelchair !== 'undefined') { 424 | Object.assign(quantityTypes, { 425 | distanceWheelChair: HKQuantityTypeIdentifierDistanceWheelchair 426 | }); 427 | // quantityTypes[distanceWheelChair]= HKQuantityTypeIdentifierDistanceWheelchair; 428 | } 429 | if (typeof HKQuantityTypeIdentifierPushCount !== 'undefined') { 430 | Object.assign(quantityTypes, { 431 | pushCount: HKQuantityTypeIdentifierPushCount 432 | }); 433 | // quantityTypes[pushCount]= HKQuantityTypeIdentifierPushCount; 434 | } 435 | if (typeof HKQuantityTypeIdentifierSwimmingStrokeCount !== 'undefined') { 436 | Object.assign(quantityTypes, { 437 | swimmingStrokeCount: HKQuantityTypeIdentifierSwimmingStrokeCount 438 | }); 439 | // quantityTypes[swimmingStrokeCount]= HKQuantityTypeIdentifierSwimmingStrokeCount; 440 | } 441 | 442 | const characteristicTypes = { 443 | biologicalSex: HKCharacteristicTypeIdentifierBiologicalSex, 444 | bloodType: HKCharacteristicTypeIdentifierBloodType, 445 | dateOfBirthComponents: HKCharacteristicTypeIdentifierDateOfBirth, 446 | fitzpatrickSkinType: HKCharacteristicTypeIdentifierFitzpatrickSkinType 447 | // wheelchairUse: HKCharacteristicTypeIdentifierWheelchairUse 448 | }; 449 | if (typeof HKCharacteristicTypeIdentifierWheelchairUse !== 'undefined') { 450 | Object.assign(characteristicTypes, { 451 | wheelchairUse: HKCharacteristicTypeIdentifierWheelchairUse 452 | }); 453 | // characteristicTypes[wheelchairUse]= HKCharacteristicTypeIdentifierWheelchairUse; 454 | } 455 | 456 | const categoryTypes = { 457 | appleStandHour: HKCategoryTypeIdentifierAppleStandHour, 458 | cervicalMucusQuality: HKCategoryTypeIdentifierCervicalMucusQuality, 459 | intermenstrualBleeding: HKCategoryTypeIdentifierIntermenstrualBleeding, 460 | menstrualFlow: HKCategoryTypeIdentifierMenstrualFlow, 461 | // mindfulSession: HKCategoryTypeIdentifierMindfulSession, 462 | ovulationTestResult: HKCategoryTypeIdentifierOvulationTestResult, 463 | sleepAnalysis: HKCategoryTypeIdentifierSleepAnalysis, 464 | sexualActivity: HKCategoryTypeIdentifierSexualActivity 465 | }; 466 | if (typeof HKCategoryTypeIdentifierMindfulSession !== 'undefined') { 467 | Object.assign(categoryTypes, { 468 | mindfulSession: HKCategoryTypeIdentifierMindfulSession 469 | }); 470 | // categoryTypes[mindfulSession]= HKCategoryTypeIdentifierMindfulSession; 471 | } 472 | 473 | const acceptableDataTypes = { 474 | steps: 'stepCount', 475 | sleep: 'sleepAnalysis', 476 | menstrual: 'menstrualFlow', 477 | ovulation: 'ovulationTestResult', 478 | mucus: 'cervicalMucusQuality', 479 | distance: /*"distanceCycling",*/ 'distanceWalkingRunning', 480 | calories: 'activeEnergyBurned' /*"basalEnergyBurned"*/, 481 | // "activity" : "", 482 | height: 'height', 483 | weight: 'bodyMass', 484 | heartRate: 'heartRate', 485 | fatPercentage: 'bodyFatPercentage', 486 | cardio: 'appleExerciseTime' 487 | // "nutrition" : "" 488 | }; 489 | --------------------------------------------------------------------------------