Commit 3e164451656962ffecdae8c3c019e4328c096c69
1 parent
71aab30e
feat(ui):华为目标值获取数据源修改为原生,并增加上传
xx feat(ui):修复ohos目标写法 xx
Showing
5 changed files
with
95 additions
and
14 deletions
| @@ -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'; |
| 6 | +import 'package:doublefeel_flutter/core/network/dio_client.dart'; | ||
| 3 | import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; | 7 | import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; |
| 4 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 8 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 5 | import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart'; | 9 | import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart'; |
| @@ -31,15 +35,37 @@ OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({ | @@ -31,15 +35,37 @@ OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({ | ||
| 31 | return OhosHealthRawDataSyncService( | 35 | return OhosHealthRawDataSyncService( |
| 32 | localStore: localStore, | 36 | localStore: localStore, |
| 33 | remoteDataSource: OhosHarmonyHealthRawDataRemoteDataSource( | 37 | remoteDataSource: OhosHarmonyHealthRawDataRemoteDataSource( |
| 34 | - HarmonyApiOhosRawDataClient(Get.find<HarmonyApi>()), | 38 | + HarmonyApiOhosRawDataClient( |
| 39 | + Get.find<HarmonyApi>(), | ||
| 40 | + healthApi: _resolveHealthApi(), | ||
| 41 | + ), | ||
| 35 | ), | 42 | ), |
| 36 | ); | 43 | ); |
| 37 | } | 44 | } |
| 38 | 45 | ||
| 46 | +/// HealthApi is normally lazy-registered. Activity-goal synchronization can | ||
| 47 | +/// start before that lazy dependency is materialized, so construct the same | ||
| 48 | +/// lightweight API wrapper from the app-wide DioClient as a fallback. | ||
| 49 | +HealthApi? _resolveHealthApi() { | ||
| 50 | + if (Get.isRegistered<HealthApi>()) { | ||
| 51 | + return Get.find<HealthApi>(); | ||
| 52 | + } | ||
| 53 | + if (Get.isRegistered<DioClient>()) { | ||
| 54 | + return HealthApi(Get.find<DioClient>()); | ||
| 55 | + } | ||
| 56 | + return null; | ||
| 57 | +} | ||
| 58 | + | ||
| 39 | class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { | 59 | class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { |
| 40 | - const HarmonyApiOhosRawDataClient(this._harmonyApi, [this._healthKitApi]); | 60 | + const HarmonyApiOhosRawDataClient( |
| 61 | + this._harmonyApi, { | ||
| 62 | + HealthApi? healthApi, | ||
| 63 | + AppHealthKitHostApi? healthKitApi, | ||
| 64 | + }) : _healthApi = healthApi, | ||
| 65 | + _healthKitApi = healthKitApi; | ||
| 41 | 66 | ||
| 42 | final HarmonyApi _harmonyApi; | 67 | final HarmonyApi _harmonyApi; |
| 68 | + final HealthApi? _healthApi; | ||
| 43 | final AppHealthKitHostApi? _healthKitApi; | 69 | final AppHealthKitHostApi? _healthKitApi; |
| 44 | 70 | ||
| 45 | bool get _preferNativeHealthData => | 71 | bool get _preferNativeHealthData => |
| @@ -118,11 +144,6 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { | @@ -118,11 +144,6 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { | ||
| 118 | 144 | ||
| 119 | @override | 145 | @override |
| 120 | Future<AppResult<V2ActivityTarget>> getActivityGoal() async { | 146 | Future<AppResult<V2ActivityTarget>> getActivityGoal() async { |
| 121 | - if (!_preferNativeHealthData) { | ||
| 122 | - AppLogger.i('[OHOS_ACTIVITY_GOAL] source=remote native_enabled=false'); | ||
| 123 | - return _harmonyApi.getActivityGoal(); | ||
| 124 | - } | ||
| 125 | - | ||
| 126 | try { | 147 | try { |
| 127 | final goal = | 148 | final goal = |
| 128 | await (_healthKitApi ?? AppHealthKitHostApi()).readActivityGoal(); | 149 | await (_healthKitApi ?? AppHealthKitHostApi()).readActivityGoal(); |
| @@ -138,6 +159,13 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { | @@ -138,6 +159,13 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { | ||
| 138 | '[OHOS_ACTIVITY_GOAL] ' | 159 | '[OHOS_ACTIVITY_GOAL] ' |
| 139 | 'source=native value=${_goalLogValue(nativeGoal)}', | 160 | 'source=native value=${_goalLogValue(nativeGoal)}', |
| 140 | ); | 161 | ); |
| 162 | + AppLogger.i( | ||
| 163 | + '[OHOS_ACTIVITY_GOAL] ' | ||
| 164 | + 'upload_scheduled value=${_goalLogValue(nativeGoal)}', | ||
| 165 | + ); | ||
| 166 | + // Deliberately do not await: a failed request must not prevent the | ||
| 167 | + // user from entering the app or the health-data sync from continuing. | ||
| 168 | + unawaited(_uploadNativeActivityGoal(nativeGoal)); | ||
| 141 | return AppSuccess(nativeGoal); | 169 | return AppSuccess(nativeGoal); |
| 142 | } | 170 | } |
| 143 | } | 171 | } |
| @@ -163,6 +191,29 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { | @@ -163,6 +191,29 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { | ||
| 163 | return remoteResult; | 191 | return remoteResult; |
| 164 | } | 192 | } |
| 165 | 193 | ||
| 194 | + Future<void> _uploadNativeActivityGoal(V2ActivityTarget goal) async { | ||
| 195 | + final healthApi = _healthApi; | ||
| 196 | + if (healthApi == null) { | ||
| 197 | + AppLogger.i( | ||
| 198 | + '[OHOS_ACTIVITY_GOAL] upload_skipped reason=health_api_missing'); | ||
| 199 | + return; | ||
| 200 | + } | ||
| 201 | + | ||
| 202 | + final result = await healthApi.uploadV2ActivityTarget(goal); | ||
| 203 | + switch (result) { | ||
| 204 | + case AppSuccess(): | ||
| 205 | + AppLogger.i( | ||
| 206 | + '[OHOS_ACTIVITY_GOAL] ' | ||
| 207 | + 'upload_success value=${_goalLogValue(goal)}', | ||
| 208 | + ); | ||
| 209 | + case AppFailure(:final error): | ||
| 210 | + AppLogger.i( | ||
| 211 | + '[OHOS_ACTIVITY_GOAL] ' | ||
| 212 | + 'upload_failed error=$error value=${_goalLogValue(goal)}', | ||
| 213 | + ); | ||
| 214 | + } | ||
| 215 | + } | ||
| 216 | + | ||
| 166 | static bool _hasGoal(V2ActivityTarget goal) { | 217 | static bool _hasGoal(V2ActivityTarget goal) { |
| 167 | return goal.move != null || | 218 | return goal.move != null || |
| 168 | goal.step != null || | 219 | 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) { |
| @@ -99,7 +99,7 @@ export class HealthKitHostApiImpl extends HealthKitHostApi { | @@ -99,7 +99,7 @@ export class HealthKitHostApiImpl extends HealthKitHostApi { | ||
| 99 | result.success(new HealthActivityGoal( | 99 | result.success(new HealthActivityGoal( |
| 100 | this.activeCaloriesToMoveGoal(report.activeCaloriesGoal), | 100 | this.activeCaloriesToMoveGoal(report.activeCaloriesGoal), |
| 101 | report.stepsGoal, | 101 | report.stepsGoal, |
| 102 | - report.activeHoursGoal, | 102 | + this.activeHoursToStandGoal(report.activeHoursGoal), |
| 103 | this.exerciseMinutesToSeconds(report.exerciseGoal), | 103 | this.exerciseMinutesToSeconds(report.exerciseGoal), |
| 104 | )); | 104 | )); |
| 105 | }) | 105 | }) |
| @@ -141,13 +141,22 @@ export class HealthKitHostApiImpl extends HealthKitHostApi { | @@ -141,13 +141,22 @@ export class HealthKitHostApiImpl extends HealthKitHostApi { | ||
| 141 | } | 141 | } |
| 142 | 142 | ||
| 143 | private exerciseMinutesToSeconds(value: number | undefined): number | undefined { | 143 | private exerciseMinutesToSeconds(value: number | undefined): number | undefined { |
| 144 | - return value === undefined ? undefined : Math.round(value * 60); | 144 | + // Health Service Kit reports the goal in minutes; the activity-target API |
| 145 | + // uses seconds. `Int(Double)` on iOS truncates, so use the same rule here. | ||
| 146 | + return value === undefined ? undefined : Math.floor(value * 60); | ||
| 145 | } | 147 | } |
| 146 | 148 | ||
| 147 | private activeCaloriesToMoveGoal(value: number | undefined): number | undefined { | 149 | private activeCaloriesToMoveGoal(value: number | undefined): number | undefined { |
| 150 | + // Health Service Kit uses calories, while the shared activity-target API | ||
| 151 | + // (and HealthKit) use kilocalories. | ||
| 148 | return value === undefined ? undefined : Math.floor(value / 1000); | 152 | return value === undefined ? undefined : Math.floor(value / 1000); |
| 149 | } | 153 | } |
| 150 | 154 | ||
| 155 | + private activeHoursToStandGoal(value: number | undefined): number | undefined { | ||
| 156 | + // Both platforms upload an integral count of stand hours. | ||
| 157 | + return value === undefined ? undefined : Math.floor(value); | ||
| 158 | + } | ||
| 159 | + | ||
| 151 | private async requestAuthorizations(): Promise<healthStore.AuthorizationResponse> { | 160 | private async requestAuthorizations(): Promise<healthStore.AuthorizationResponse> { |
| 152 | await this.ensureInitialized(); | 161 | await this.ensureInitialized(); |
| 153 | return healthStore.requestAuthorizations(this.context, this.authorizationRequest); | 162 | return healthStore.requestAuthorizations(this.context, this.authorizationRequest); |
-
Please register or login to post a comment