HealthKitHostApiImpl.ets
8.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import { HealthActivityGoal, HealthAuthorization, HealthKitHostApi, HealthWorkoutDataPoint, Result } from '../pigeon/HealthKitApi';
import common from '@ohos.app.ability.common';
import { bundleManager, Want } from '@kit.AbilityKit';
import { productViewManager } from '@kit.AppGalleryKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { healthService, healthStore } from '@kit.HealthServiceKit';
/**
* Bridges Flutter's health authorization flow to Health Service Kit.
*/
export class HealthKitHostApiImpl extends HealthKitHostApi {
private static readonly HEALTH_APP_LINK = 'huaweischeme://healthapp/home/main';
private static readonly HEALTH_APP_BUNDLE_NAME = 'com.huawei.hmos.health';
private readonly context: common.UIAbilityContext;
// Keep this list aligned with the health metrics requested by the Flutter app.
private readonly authorizationRequest: healthStore.AuthorizationRequest = {
readDataTypes: [
healthStore.healthDataTypes.DAILY_ACTIVITIES,
healthStore.healthDataTypes.HEART_RATE,
healthStore.healthDataTypes.HEART_RATE_VARIABILITY,
healthStore.healthDataTypes.BLOOD_OXYGEN_SATURATION,
healthStore.healthDataTypes.STRESS,
healthStore.healthDataTypes.SLEEP_RECORD,
healthStore.healthDataTypes.BODY_TEMPERATURE,
healthStore.healthDataTypes.SKIN_TEMPERATURE,
healthStore.healthDataTypes.WORKOUT,
],
writeDataTypes: [],
};
constructor(context: common.UIAbilityContext) {
super();
this.context = context;
}
getHealthServerAuthUrl(result: Result<string>): void {
// Health Service Kit uses a system authorization sheet, not an OAuth URL.
result.success('');
}
checkHealthAppAuthorization(result: Result<HealthAuthorization>): void {
this.getAuthorization().then((response: healthStore.AuthorizationResponse) => {
// The API returns only the data types currently granted to this app.
// A non-empty intersection means the app can read at least one metric.
const hasData = response.readDataTypes.length > 0;
result.success(new HealthAuthorization(hasData ? 1 : 0, hasData));
}).catch((error: Error) => {
// The user still needs to accept the Huawei Health privacy agreement.
// Return "needs authorization" so the user action can launch Health.
console.error(`checkHealthAppAuthorization failed: ${error.name}: ${error.message}`);
result.success(new HealthAuthorization(
this.requiresHealthAppPrivacyAuthorization(error) ? 0 : -1,
false,
));
});
}
requestHealthClientAuthorization(result: Result<boolean>): void {
this.requestAuthorizations().then((response: healthStore.AuthorizationResponse) => {
console.error(`requestHealthClientAuthorization Huawei success`);
result.success(response.readDataTypes.length > 0);
}).catch((error: Error) => {
const code = (error as BusinessError).code;
console.error(`requestHealthClientAuthorization failed: ${code}, ${error.name}: ${error.message}`);
if (!this.requiresHealthAppPrivacyAuthorization(error)) {
result.success(false);
return;
}
// Privacy consent is managed by the Huawei Health app. Opening it does
// not grant this application's data types yet, so Flutter must re-check
// authorization after the user returns.
this.openHealthAppForPrivacyAuthorization().then(() => {
result.success(false);
}).catch(() => {
result.success(false);
});
});
}
/**
* Triggers the manual device-to-cloud synchronization exposed by Huawei
* Health. The application must have obtained the "manual data sync"
* permission in the Health Service Kit console before this call can work.
*/
syncHealthDataToCloud(result: Result<boolean>): void {
this.syncAllHealthData().then(() => {
result.success(true);
}).catch((error: Error) => {
console.error(`syncHealthDataToCloud failed: ${error.name}: ${error.message}`);
result.success(false);
});
}
cancelAuthorizations(result: Result<boolean>): void {
this.ensureInitialized()
.then(() => healthStore.cancelAuthorizations())
.then(() => result.success(true))
.catch((error: BusinessError) => {
console.error(`cancelAuthorizations failed: ${error.code}, ${error.message}`);
result.success(false);
});
}
readActivityGoal(result: Result<HealthActivityGoal | undefined>): void {
this.ensureInitialized()
.then(() => healthService.workout.readActivityReport())
.then((report: healthService.workout.ActivityReport) => {
result.success(new HealthActivityGoal(
this.activeCaloriesToMoveGoal(report.activeCaloriesGoal),
report.stepsGoal,
report.activeHoursGoal,
this.exerciseMinutesToSeconds(report.exerciseGoal),
));
})
.catch((error: Error) => {
console.error(`readActivityGoal failed: ${error.name}: ${error.message}`);
result.error(error);
});
}
readWorkoutData(
startTime: number,
endTime: number,
result: Result<Array<HealthWorkoutDataPoint>>,
): void {
this.ensureInitialized()
.then(() => healthStore.readData<healthStore.ExerciseSequence>({
startTime: startTime * 1000,
endTime: endTime * 1000,
exerciseType: null,
}))
.then((records: Array<healthStore.ExerciseSequence>) => {
result.success(records.map((record: healthStore.ExerciseSequence) => {
return new HealthWorkoutDataPoint(
Math.floor(record.startTime / 1000),
Math.floor(record.endTime / 1000),
record.exerciseType.id,
);
}));
})
.catch((error: Error) => {
console.error(`readWorkoutData failed: ${error.name}: ${error.message}`);
result.error(error);
});
}
private async getAuthorization(): Promise<healthStore.AuthorizationResponse> {
await this.ensureInitialized();
return healthStore.getAuthorizations(this.authorizationRequest);
}
private exerciseMinutesToSeconds(value: number | undefined): number | undefined {
return value === undefined ? undefined : Math.round(value * 60);
}
private activeCaloriesToMoveGoal(value: number | undefined): number | undefined {
return value === undefined ? undefined : Math.floor(value / 1000);
}
private async requestAuthorizations(): Promise<healthStore.AuthorizationResponse> {
await this.ensureInitialized();
return healthStore.requestAuthorizations(this.context, this.authorizationRequest);
}
private async syncAllHealthData(): Promise<void> {
await this.ensureInitialized();
await healthStore.syncAll();
}
private async ensureInitialized(): Promise<void> {
await healthStore.init(this.context);
}
private requiresHealthAppPrivacyAuthorization(error: Error): boolean {
// Health Service Kit uses 1002703001 when the user has not accepted the
// Huawei Health privacy agreement.
return (error as BusinessError).code === 1002703001;
}
async openHealthAppForPrivacyAuthorization(): Promise<void> {
if (this.canOpenHealthApp()) {
await this.openInstalledHealthApp();
return;
}
this.openHealthAppInAppGallery();
}
private canOpenHealthApp(): boolean {
try {
return bundleManager.canOpenLink(HealthKitHostApiImpl.HEALTH_APP_LINK);
} catch (error) {
console.error(`Health app availability check failed: ${error.name}: ${error.message}`);
return false;
}
}
private async openInstalledHealthApp(): Promise<void> {
try {
console.error(`Opening Huawei Health`);
await this.context.openLink(HealthKitHostApiImpl.HEALTH_APP_LINK);
} catch (error) {
console.error(`Failed to open Huawei Health: ${error.name}: ${error.message}`);
this.openHealthAppInAppGallery();
}
}
private openHealthAppInAppGallery(): void {
const want: Want = {
parameters: {
bundleName: HealthKitHostApiImpl.HEALTH_APP_BUNDLE_NAME,
},
};
try {
productViewManager.loadProduct(this.context, want);
} catch (error) {
console.error(`Failed to open AppGallery recommendation: ${error.name}: ${error.message}`);
}
}
}