Commit fd20342d94ded246dd5475454a7d72bf440466ae

Authored by 权海
1 parent 24756f25

feat(ui):华为目标值获取数据源修改为原生,并增加上传

xx

(cherry picked from commit cbb244b9)
@@ -533,9 +533,12 @@ final class WatchHealthObserverUploader { @@ -533,9 +533,12 @@ final class WatchHealthObserverUploader {
533 } 533 }
534 534
535 private func uploadActivityTarget(_ summary: HKActivitySummary) async throws { 535 private func uploadActivityTarget(_ summary: HKActivitySummary) async throws {
  536 + let exerciseGoal = Int(summary.appleExerciseTimeGoal.doubleValue(for: .second()))
536 var body: [String: Any] = [ 537 var body: [String: Any] = [
537 "move": Int(summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie())), 538 "move": Int(summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie())),
538 "stand": Int(summary.appleStandHoursGoal.doubleValue(for: .count())), 539 "stand": Int(summary.appleStandHoursGoal.doubleValue(for: .count())),
  540 + "exercise": exerciseGoal,
  541 + "exercise_time_goal": exerciseGoal,
539 "active_energy_burned": summary.activeEnergyBurned.doubleValue(for: .kilocalorie()), 542 "active_energy_burned": summary.activeEnergyBurned.doubleValue(for: .kilocalorie()),
540 "active_energy_burned_goal": summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie()), 543 "active_energy_burned_goal": summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie()),
541 "apple_exercise_time": summary.appleExerciseTime.doubleValue(for: .second()), 544 "apple_exercise_time": summary.appleExerciseTime.doubleValue(for: .second()),
@@ -546,7 +549,6 @@ final class WatchHealthObserverUploader { @@ -546,7 +549,6 @@ final class WatchHealthObserverUploader {
546 body["activity_move_mode"] = summary.activityMoveMode.rawValue 549 body["activity_move_mode"] = summary.activityMoveMode.rawValue
547 body["apple_move_time"] = summary.appleMoveTime.doubleValue(for: .second()) 550 body["apple_move_time"] = summary.appleMoveTime.doubleValue(for: .second())
548 body["apple_move_time_goal"] = summary.appleMoveTimeGoal.doubleValue(for: .second()) 551 body["apple_move_time_goal"] = summary.appleMoveTimeGoal.doubleValue(for: .second())
549 - body["exercise_time_goal"] = summary.exerciseTimeGoal?.doubleValue(for: .second())  
550 body["stand_hours_goal"] = summary.standHoursGoal?.doubleValue(for: .count()) 552 body["stand_hours_goal"] = summary.standHoursGoal?.doubleValue(for: .count())
551 _ = try await healthService.uploadActivityTarget(data: body) 553 _ = try await healthService.uploadActivityTarget(data: body)
552 } 554 }
@@ -422,6 +422,19 @@ class HealthApi { @@ -422,6 +422,19 @@ class HealthApi {
422 ); 422 );
423 } 423 }
424 424
  425 + Future<AppResult<void>> uploadV2ActivityTarget(
  426 + V2ActivityTarget target,
  427 + ) {
  428 + return safeCall(
  429 + call: () async {
  430 + await _dioClient.dio.post(
  431 + ApiPaths.v2ActivityTarget,
  432 + data: target.toJson(),
  433 + );
  434 + },
  435 + );
  436 + }
  437 +
425 Future<AppResult<V2HealthEverUploaded>> getHealthDataEverUploaded() { 438 Future<AppResult<V2HealthEverUploaded>> getHealthDataEverUploaded() {
426 return safeCall( 439 return safeCall(
427 call: () async { 440 call: () async {
  1 +import 'dart:async';
  2 +
1 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 3 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
  4 +import 'package:doublefeel_flutter/core/network/api/health_api.dart';
2 import 'package:doublefeel_flutter/core/network/api/harmony_api.dart'; 5 import 'package:doublefeel_flutter/core/network/api/harmony_api.dart';
3 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; 6 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
4 import 'package:doublefeel_flutter/core/result/app_result.dart'; 7 import 'package:doublefeel_flutter/core/result/app_result.dart';
@@ -31,15 +34,24 @@ OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({ @@ -31,15 +34,24 @@ OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({
31 return OhosHealthRawDataSyncService( 34 return OhosHealthRawDataSyncService(
32 localStore: localStore, 35 localStore: localStore,
33 remoteDataSource: OhosHarmonyHealthRawDataRemoteDataSource( 36 remoteDataSource: OhosHarmonyHealthRawDataRemoteDataSource(
34 - HarmonyApiOhosRawDataClient(Get.find<HarmonyApi>()), 37 + HarmonyApiOhosRawDataClient(
  38 + Get.find<HarmonyApi>(),
  39 + healthApi: Get.isRegistered<HealthApi>() ? Get.find<HealthApi>() : null,
  40 + ),
35 ), 41 ),
36 ); 42 );
37 } 43 }
38 44
39 class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { 45 class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient {
40 - const HarmonyApiOhosRawDataClient(this._harmonyApi, [this._healthKitApi]); 46 + const HarmonyApiOhosRawDataClient(
  47 + this._harmonyApi, {
  48 + HealthApi? healthApi,
  49 + AppHealthKitHostApi? healthKitApi,
  50 + }) : _healthApi = healthApi,
  51 + _healthKitApi = healthKitApi;
41 52
42 final HarmonyApi _harmonyApi; 53 final HarmonyApi _harmonyApi;
  54 + final HealthApi? _healthApi;
43 final AppHealthKitHostApi? _healthKitApi; 55 final AppHealthKitHostApi? _healthKitApi;
44 56
45 bool get _preferNativeHealthData => 57 bool get _preferNativeHealthData =>
@@ -118,7 +130,8 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { @@ -118,7 +130,8 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient {
118 130
119 @override 131 @override
120 Future<AppResult<V2ActivityTarget>> getActivityGoal() async { 132 Future<AppResult<V2ActivityTarget>> getActivityGoal() async {
121 - if (!_preferNativeHealthData) { 133 + final useRemoteData = false;
  134 + if (!_preferNativeHealthData || useRemoteData) {
122 AppLogger.i('[OHOS_ACTIVITY_GOAL] source=remote native_enabled=false'); 135 AppLogger.i('[OHOS_ACTIVITY_GOAL] source=remote native_enabled=false');
123 return _harmonyApi.getActivityGoal(); 136 return _harmonyApi.getActivityGoal();
124 } 137 }
@@ -138,6 +151,10 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { @@ -138,6 +151,10 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient {
138 '[OHOS_ACTIVITY_GOAL] ' 151 '[OHOS_ACTIVITY_GOAL] '
139 'source=native value=${_goalLogValue(nativeGoal)}', 152 'source=native value=${_goalLogValue(nativeGoal)}',
140 ); 153 );
  154 + if (useRemoteData || _preferNativeHealthData) {
  155 + // 上传目标值到服务器
  156 + unawaited(_uploadNativeActivityGoal(nativeGoal));
  157 + }
141 return AppSuccess(nativeGoal); 158 return AppSuccess(nativeGoal);
142 } 159 }
143 } 160 }
@@ -163,6 +180,29 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { @@ -163,6 +180,29 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient {
163 return remoteResult; 180 return remoteResult;
164 } 181 }
165 182
  183 + Future<void> _uploadNativeActivityGoal(V2ActivityTarget goal) async {
  184 + final healthApi = _healthApi;
  185 + if (healthApi == null) {
  186 + AppLogger.i(
  187 + '[OHOS_ACTIVITY_GOAL] upload_skipped reason=health_api_missing');
  188 + return;
  189 + }
  190 +
  191 + final result = await healthApi.uploadV2ActivityTarget(goal);
  192 + switch (result) {
  193 + case AppSuccess():
  194 + AppLogger.i(
  195 + '[OHOS_ACTIVITY_GOAL] '
  196 + 'upload_success value=${_goalLogValue(goal)}',
  197 + );
  198 + case AppFailure(:final error):
  199 + AppLogger.i(
  200 + '[OHOS_ACTIVITY_GOAL] '
  201 + 'upload_failed error=$error value=${_goalLogValue(goal)}',
  202 + );
  203 + }
  204 + }
  205 +
166 static bool _hasGoal(V2ActivityTarget goal) { 206 static bool _hasGoal(V2ActivityTarget goal) {
167 return goal.move != null || 207 return goal.move != null ||
168 goal.step != null || 208 goal.step != null ||
@@ -404,7 +404,8 @@ class V2RealtimeStressData { @@ -404,7 +404,8 @@ class V2RealtimeStressData {
404 404
405 /// v2/activity_target/ response 405 /// v2/activity_target/ response
406 /// {"id":0,"create_time":0,"update_time":0,"user_id":131, 406 /// {"id":0,"create_time":0,"update_time":0,"user_id":131,
407 -/// "move":300,"step":8000,"stand":12,"exercise":3600} 407 +/// "move":300,"step":8000,"stand":12,"exercise":3600,
  408 +/// "exercise_time_goal":3600}
408 class V2ActivityTarget { 409 class V2ActivityTarget {
409 const V2ActivityTarget({ 410 const V2ActivityTarget({
410 this.id, 411 this.id,
@@ -430,7 +431,8 @@ class V2ActivityTarget { @@ -430,7 +431,8 @@ class V2ActivityTarget {
430 /// 站立小时目标 431 /// 站立小时目标
431 final int? stand; 432 final int? stand;
432 433
433 - /// 锻炼时长目标(秒) 434 + /// 锻炼时长目标(秒)。上传时也会同步写入同义字段
  435 + /// [exercise_time_goal],两者始终使用同一个值。
434 final int? exercise; 436 final int? exercise;
435 437
436 final int? createTime; 438 final int? createTime;
@@ -444,7 +446,8 @@ class V2ActivityTarget { @@ -444,7 +446,8 @@ class V2ActivityTarget {
444 move: _parseInt(json['move']), 446 move: _parseInt(json['move']),
445 step: _parseInt(json['step']), 447 step: _parseInt(json['step']),
446 stand: _parseInt(json['stand']), 448 stand: _parseInt(json['stand']),
447 - exercise: _parseInt(json['exercise']), 449 + exercise:
  450 + _parseInt(json['exercise']) ?? _parseInt(json['exercise_time_goal']),
448 createTime: _parseInt(json['create_time']), 451 createTime: _parseInt(json['create_time']),
449 updateTime: _parseInt(json['update_time']), 452 updateTime: _parseInt(json['update_time']),
450 sleepTargetDuration: _parseInt(json['sleep_target_duration']), 453 sleepTargetDuration: _parseInt(json['sleep_target_duration']),
@@ -458,7 +461,10 @@ class V2ActivityTarget { @@ -458,7 +461,10 @@ class V2ActivityTarget {
458 if (move != null) val['move'] = move; 461 if (move != null) val['move'] = move;
459 if (step != null) val['step'] = step; 462 if (step != null) val['step'] = step;
460 if (stand != null) val['stand'] = stand; 463 if (stand != null) val['stand'] = stand;
461 - if (exercise != null) val['exercise'] = exercise; 464 + if (exercise != null) {
  465 + val['exercise'] = exercise;
  466 + val['exercise_time_goal'] = exercise;
  467 + }
462 if (createTime != null) val['create_time'] = createTime; 468 if (createTime != null) val['create_time'] = createTime;
463 if (updateTime != null) val['update_time'] = updateTime; 469 if (updateTime != null) val['update_time'] = updateTime;
464 if (sleepTargetDuration != null) { 470 if (sleepTargetDuration != null) {
@@ -109,7 +109,7 @@ export class HealthKitHostApiImpl extends HealthKitHostApi { @@ -109,7 +109,7 @@ export class HealthKitHostApiImpl extends HealthKitHostApi {
109 result.success(new HealthActivityGoal( 109 result.success(new HealthActivityGoal(
110 this.activeCaloriesToMoveGoal(report.activeCaloriesGoal), 110 this.activeCaloriesToMoveGoal(report.activeCaloriesGoal),
111 report.stepsGoal, 111 report.stepsGoal,
112 - report.activeHoursGoal, 112 + this.activeHoursToStandGoal(report.activeHoursGoal),
113 this.exerciseMinutesToSeconds(report.exerciseGoal), 113 this.exerciseMinutesToSeconds(report.exerciseGoal),
114 )); 114 ));
115 }) 115 })
@@ -151,13 +151,22 @@ export class HealthKitHostApiImpl extends HealthKitHostApi { @@ -151,13 +151,22 @@ export class HealthKitHostApiImpl extends HealthKitHostApi {
151 } 151 }
152 152
153 private exerciseMinutesToSeconds(value: number | undefined): number | undefined { 153 private exerciseMinutesToSeconds(value: number | undefined): number | undefined {
154 - return value === undefined ? undefined : Math.round(value * 60); 154 + // Health Service Kit reports the goal in minutes; the activity-target API
  155 + // uses seconds. `Int(Double)` on iOS truncates, so use the same rule here.
  156 + return value === undefined ? undefined : Math.floor(value * 60);
155 } 157 }
156 158
157 private activeCaloriesToMoveGoal(value: number | undefined): number | undefined { 159 private activeCaloriesToMoveGoal(value: number | undefined): number | undefined {
  160 + // Health Service Kit uses calories, while the shared activity-target API
  161 + // (and HealthKit) use kilocalories.
158 return value === undefined ? undefined : Math.floor(value / 1000); 162 return value === undefined ? undefined : Math.floor(value / 1000);
159 } 163 }
160 164
  165 + private activeHoursToStandGoal(value: number | undefined): number | undefined {
  166 + // Both platforms upload an integral count of stand hours.
  167 + return value === undefined ? undefined : Math.floor(value);
  168 + }
  169 +
161 private async requestAuthorizations(): Promise<healthStore.AuthorizationResponse> { 170 private async requestAuthorizations(): Promise<healthStore.AuthorizationResponse> {
162 await this.ensureInitialized(); 171 await this.ensureInitialized();
163 return healthStore.requestAuthorizations(this.context, this.authorizationRequest); 172 return healthStore.requestAuthorizations(this.context, this.authorizationRequest);