Skip to content

Commit e03579d

Browse files
Support monitoring / observing health data changes #12
1 parent 286b495 commit e03579d

File tree

7 files changed

+219
-13
lines changed

7 files changed

+219
-13
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,41 @@ this.healthData.query(
117117
.catch(error => this.resultToShow = error);
118118
```
119119

120+
121+
### `startMonitoring` (iOS only, for now)
122+
If you want to be notified when health data was changed, you can monitor specific types. This even works when your app is in the background, with `enableBackgroundUpdates: true`. 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), but in return you are responsible for telling iOS you're done. So make sure you invoke the `completionHandler` as shown below.
123+
124+
Not all data types support `backgroundUpdateFrequency: "immediate"`, so your app may not always be invoked immediately when data is added/deleted in HealthKit.
125+
126+
> Background notifications probably don't work on the iOS simulator, so please test those on a real device.
127+
128+
```typescript
129+
this.healthData.startMonitoring(
130+
{
131+
dataType: dataType,
132+
enableBackgroundUpdates: true,
133+
backgroundUpdateFrequency: "immediate",
134+
onUpdate: (completionHandler: () => void) => {
135+
console.log("Our app was notified that health data changed, so querying...");
136+
this.getData(dataType, unit).then(() => completionHandler());
137+
}
138+
})
139+
.then(() => this.resultToShow = `Started monitoring ${dataType}`)
140+
.catch(error => this.resultToShow = error);
141+
```
142+
143+
### `stopMonitoring` (iOS only, for now)
144+
It's best to call this method in case you no longer wish to receive notifications when health data changes.
145+
146+
```typescript
147+
this.healthData.stopMonitoring(
148+
{
149+
dataType: dataType,
150+
})
151+
.then(() => this.resultToShow = `Stopped monitoring ${dataType}`)
152+
.catch(error => this.resultToShow = error);
153+
```
154+
120155
## Available Data Types
121156
With version 1.0.0 these are the supported types of data you can read. Also, make sure you pass in the correct `unit`.
122157

demo-ng/app/app.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
<Button ios:text="Apple Health Data available?" android:text="Google Fit Data available?" (tap)="isAvailable()"></Button>
66
<Button text="Is Authorized?" (tap)="isAuthorized()"></Button>
77
<Button text="Request Authorization" (tap)="requestAuthForVariousTypes()"></Button>
8+
<Button text="Start background Hearth Rate monitoring" (tap)="startMonitoringData('heartRate', 'count/min')"></Button>
9+
<Button text="Stop background Hearth Rate monitoring" (tap)="stopMonitoringData('heartRate')"></Button>
810
<Button text="Get Daily Steps per source (count)" (tap)="getData('steps', 'count', 'sourceAndDay')"></Button>
911
<Button text="Get Hourly Distance data (km)" (tap)="getData('distance', 'km', 'hour')"></Button>
1012
<!--<Button text="Get Hourly Distance data (mi)" (tap)="getData('distance', 'mi', 'hour')"></Button>-->

demo-ng/app/app.component.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component } from "@angular/core";
1+
import { Component, NgZone } from "@angular/core";
22
import { alert } from "tns-core-modules/ui/dialogs";
33
import { AggregateBy, HealthData, HealthDataType } from "nativescript-health-data";
44

@@ -20,7 +20,7 @@ export class AppComponent {
2020
private healthData: HealthData;
2121
resultToShow = "";
2222

23-
constructor() {
23+
constructor(private zone: NgZone) {
2424
this.healthData = new HealthData();
2525
}
2626

@@ -48,20 +48,46 @@ export class AppComponent {
4848
.catch(error => console.log("Request auth error: ", error));
4949
}
5050

51-
getData(dataType: string, unit: string, aggregateBy?: AggregateBy): void {
52-
this.healthData.query(
51+
getData(dataType: string, unit: string, aggregateBy?: AggregateBy): Promise<void> {
52+
return this.healthData.query(
5353
{
54-
startDate: new Date(new Date().getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days ago
54+
startDate: new Date(new Date().getTime() - 24 * 60 * 60 * 1000), // 1 day ago
5555
endDate: new Date(), // now
5656
dataType: dataType,
5757
unit: unit,
5858
aggregateBy: aggregateBy,
5959
sortOrder: "desc"
6060
})
6161
.then(result => {
62-
console.log(JSON.stringify(result));
63-
this.resultToShow = JSON.stringify(result);
62+
this.zone.run(() => {
63+
console.log(JSON.stringify(result));
64+
this.resultToShow = JSON.stringify(result)
65+
});
6466
})
6567
.catch(error => this.resultToShow = error);
6668
}
69+
70+
startMonitoringData(dataType: string, unit: string): void {
71+
this.healthData.startMonitoring(
72+
{
73+
dataType: dataType,
74+
enableBackgroundUpdates: true,
75+
backgroundUpdateFrequency: "immediate",
76+
onUpdate: (completionHandler: () => void) => {
77+
console.log("Our app was notified that health data changed, so querying...");
78+
this.getData(dataType, unit).then(() => completionHandler());
79+
}
80+
})
81+
.then(() => this.resultToShow = `Started monitoring ${dataType}`)
82+
.catch(error => this.resultToShow = error);
83+
}
84+
85+
stopMonitoringData(dataType: string): void {
86+
this.healthData.stopMonitoring(
87+
{
88+
dataType: dataType,
89+
})
90+
.then(() => this.resultToShow = `Stopped monitoring ${dataType}`)
91+
.catch(error => this.resultToShow = error);
92+
}
6793
}

src/health-data.android.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { Common, HealthDataApi, HealthDataType, QueryRequest, ResponseItem } from './health-data.common';
1+
import {
2+
Common,
3+
HealthDataApi,
4+
HealthDataType,
5+
QueryRequest,
6+
ResponseItem,
7+
StartMonitoringRequest,
8+
StopMonitoringRequest
9+
} from './health-data.common';
210
import * as utils from 'tns-core-modules/utils/utils';
311
import { ad } from 'tns-core-modules/utils/utils';
412
import * as application from 'tns-core-modules/application';
@@ -82,7 +90,7 @@ export class HealthData extends Common implements HealthDataApi {
8290
return new Promise((resolve, reject) => {
8391
try {
8492
// make sure the user is authorized
85-
this.requestAuthorization([{ name: opts.dataType, accessType: "read"}]).then(authorized => {
93+
this.requestAuthorization([{name: opts.dataType, accessType: "read"}]).then(authorized => {
8694
if (!authorized) {
8795
reject("Not authorized");
8896
return;
@@ -121,6 +129,18 @@ export class HealthData extends Common implements HealthDataApi {
121129
});
122130
}
123131

132+
startMonitoring(opts: StartMonitoringRequest): Promise<void> {
133+
return new Promise<void>((resolve, reject) => {
134+
reject("Not supported");
135+
});
136+
}
137+
138+
stopMonitoring(opts: StopMonitoringRequest): Promise<void> {
139+
return new Promise<void>((resolve, reject) => {
140+
reject("Not supported");
141+
});
142+
}
143+
124144
private parseData(readResult: any /* com.google.android.gms.fitness.result.DataReadResult */, opts: QueryRequest) {
125145
let result = [];
126146
if (readResult.getBuckets().size() > 0) {

src/health-data.common.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export interface HealthDataType {
1313

1414
export type AggregateBy = "hour" | "day" | "sourceAndDay";
1515

16+
export type BackgroundUpdateFrequency = "immediate" | "hourly" | "daily" | "weekly";
17+
1618
export type SortOrder = "asc" | "desc";
1719

1820
export interface QueryRequest {
@@ -31,6 +33,28 @@ export interface QueryRequest {
3133
limit?: number;
3234
}
3335

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+
3458
export interface ResponseItem {
3559
start: Date;
3660
end: Date;
@@ -82,6 +106,10 @@ export interface HealthDataApi {
82106
requestAuthorization(types: Array<HealthDataType>): Promise<boolean>;
83107

84108
query(opts: QueryRequest): Promise<Array<ResponseItem>>;
109+
110+
startMonitoring(opts: StartMonitoringRequest): Promise<void>;
111+
112+
stopMonitoring(opts: StopMonitoringRequest): Promise<void>;
85113
}
86114

87115
export abstract class Common {

src/health-data.ios.ts

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1-
import { Common, HealthDataApi, HealthDataType, QueryRequest, ResponseItem } from './health-data.common';
1+
import {
2+
BackgroundUpdateFrequency,
3+
Common,
4+
HealthDataApi,
5+
HealthDataType,
6+
QueryRequest,
7+
ResponseItem,
8+
StartMonitoringRequest,
9+
StopMonitoringRequest
10+
} from './health-data.common';
211

312
export class HealthData extends Common implements HealthDataApi {
413
private healthStore: HKHealthStore;
14+
private monitorQueries: Map<string /* type */, HKObserverQuery> = new Map();
515

616
constructor() {
717
super();
@@ -63,7 +73,7 @@ export class HealthData extends Common implements HealthDataApi {
6373
query(opts: QueryRequest): Promise<Array<ResponseItem>> {
6474
return new Promise((resolve, reject) => {
6575
// make sure the user is authorized
66-
this.requestAuthorization([{ name: opts.dataType, accessType: "read"}]).then(authorized => {
76+
this.requestAuthorization([{name: opts.dataType, accessType: "read"}]).then(authorized => {
6777
if (!authorized) {
6878
reject("Not authorized");
6979
return;
@@ -87,6 +97,49 @@ export class HealthData extends Common implements HealthDataApi {
8797
});
8898
}
8999

100+
startMonitoring(opts: StartMonitoringRequest): Promise<void> {
101+
return new Promise((resolve, reject) => {
102+
// make sure the user is authorized
103+
this.requestAuthorization([{name: opts.dataType, accessType: "read"}]).then(authorized => {
104+
if (!authorized) {
105+
reject("Not authorized");
106+
return;
107+
}
108+
109+
let typeOfData = acceptableDataTypes[opts.dataType];
110+
if (quantityTypes[typeOfData] || categoryTypes[typeOfData]) {
111+
this.monitorData(typeOfData, opts);
112+
resolve();
113+
} else {
114+
reject('Type not supported (yet)');
115+
}
116+
});
117+
});
118+
}
119+
120+
stopMonitoring(opts: StopMonitoringRequest): Promise<void> {
121+
return new Promise((resolve, reject) => {
122+
let typeOfData = acceptableDataTypes[opts.dataType];
123+
const objectType = this.resolveDataType(typeOfData);
124+
125+
if (quantityTypes[typeOfData] || categoryTypes[typeOfData]) {
126+
127+
const rememberedQuery = this.monitorQueries.get(opts.dataType);
128+
if (rememberedQuery) {
129+
this.healthStore.stopQuery(rememberedQuery);
130+
this.monitorQueries.delete(opts.dataType);
131+
}
132+
133+
this.healthStore.disableBackgroundDeliveryForTypeWithCompletion(
134+
objectType, (success: boolean, error: NSError) => {
135+
success ? resolve() : reject(error.localizedDescription);
136+
});
137+
} else {
138+
reject('Type not supported (yet)');
139+
}
140+
});
141+
}
142+
90143
private resolveDataType(type: string): HKObjectType {
91144
if (quantityTypes[type]) {
92145
return HKObjectType.quantityTypeForIdentifier(quantityTypes[type]);
@@ -101,7 +154,7 @@ export class HealthData extends Common implements HealthDataApi {
101154
}
102155

103156
private queryForQuantityOrCategoryData(dataType: string, opts: QueryRequest, callback: (data: Array<ResponseItem>, error: string) => void) {
104-
let objectType = this.resolveDataType(dataType);
157+
const objectType = this.resolveDataType(dataType);
105158

106159
const predicate = HKQuery.predicateForSamplesWithStartDateEndDateOptions(opts.startDate, opts.endDate, HKQueryOptions.StrictStartDate);
107160

@@ -149,6 +202,48 @@ export class HealthData extends Common implements HealthDataApi {
149202
this.healthStore.executeQuery(query);
150203
}
151204

205+
private monitorData(dataType: string, opts: StartMonitoringRequest): void {
206+
const objectType = this.resolveDataType(dataType);
207+
208+
let query = HKObserverQuery.alloc().initWithSampleTypePredicateUpdateHandler(
209+
objectType, null, (observerQuery: HKObserverQuery, handler: () => void, error: NSError) => {
210+
if (error) {
211+
opts.onError(error.localizedDescription);
212+
handler();
213+
} else {
214+
// We need to tell iOS when our app is done background processing by calling the handler
215+
opts.onUpdate(() => handler());
216+
}
217+
}
218+
);
219+
220+
// remember this query, so we can stop it at a later time
221+
this.monitorQueries.set(opts.dataType, query);
222+
223+
this.healthStore.executeQuery(query);
224+
225+
if (opts.enableBackgroundUpdates) {
226+
this.healthStore.enableBackgroundDeliveryForTypeFrequencyWithCompletion(
227+
objectType, this.getHKUpdateFrequency(opts.backgroundUpdateFrequency), (success: boolean, error: NSError) => {
228+
if (!success) {
229+
opts.onError(error.localizedDescription);
230+
}
231+
});
232+
}
233+
}
234+
235+
private getHKUpdateFrequency(frequency: BackgroundUpdateFrequency): HKUpdateFrequency {
236+
if (frequency === "weekly") {
237+
return HKUpdateFrequency.Weekly;
238+
} else if (frequency === "daily") {
239+
return HKUpdateFrequency.Daily;
240+
} else if (frequency === "hourly") {
241+
return HKUpdateFrequency.Hourly;
242+
} else {
243+
return HKUpdateFrequency.Immediate;
244+
}
245+
};
246+
152247
private queryForCharacteristicData(dataType: string) {
153248
// console.log('ask for characteristic data ' + data);
154249
let dataToRetrieve;

src/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nativescript-health-data",
3-
"version": "1.0.2",
3+
"version": "1.1.0",
44
"description": "Health Data plugin for Nativescript, using Google Fit and Apple HealthKit.",
55
"main": "health-data",
66
"typings": "index.d.ts",

0 commit comments

Comments
 (0)