Commit 1d2a0dd3d3049b12e172e0620a5c8dfe6bf69d47

Authored by 权海
1 parent a209a9f5

feat(ui):增加ohos活动目标

@@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart'; @@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart';
4 import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart'; 4 import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
5 import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart'; 5 import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart';
6 import 'package:doublefeel_flutter/data/models/harmony/privacy_records.dart'; 6 import 'package:doublefeel_flutter/data/models/harmony/privacy_records.dart';
  7 +import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
7 8
8 import '../../result/app_result.dart'; 9 import '../../result/app_result.dart';
9 import '../../result/safe_call.dart'; 10 import '../../result/safe_call.dart';
@@ -105,4 +106,15 @@ class HarmonyApi { @@ -105,4 +106,15 @@ class HarmonyApi {
105 }, 106 },
106 ); 107 );
107 } 108 }
  109 +
  110 + Future<AppResult<V2ActivityTarget>> getActivityGoal() {
  111 + return safeCall(
  112 + call: () async {
  113 + final response = await _dioClient.dio.get(ApiPaths.hmActivityGoal);
  114 + return V2ActivityTarget.fromJson(
  115 + response.data as Map<String, dynamic>,
  116 + );
  117 + },
  118 + );
  119 + }
108 } 120 }
@@ -238,6 +238,58 @@ class HealthApi { @@ -238,6 +238,58 @@ class HealthApi {
238 ); 238 );
239 } 239 }
240 240
  241 + Future<AppResult<void>> uploadV2HrvTrendResults(
  242 + List<Map<String, Object?>> dataList,
  243 + ) {
  244 + return safeCall(
  245 + call: () async {
  246 + await _dioClient.dio.post(
  247 + ApiPaths.v2HrvTrend,
  248 + data: {'data_list': dataList},
  249 + );
  250 + },
  251 + );
  252 + }
  253 +
  254 + Future<AppResult<void>> uploadV2RealtimeStressResults(
  255 + List<Map<String, Object?>> dataList,
  256 + ) {
  257 + return safeCall(
  258 + call: () async {
  259 + await _dioClient.dio.post(
  260 + ApiPaths.v2RealtimeStress,
  261 + data: {'data_list': dataList},
  262 + );
  263 + },
  264 + );
  265 + }
  266 +
  267 + Future<AppResult<void>> uploadV2DailyStressResults(
  268 + List<Map<String, Object?>> dataList,
  269 + ) {
  270 + return safeCall(
  271 + call: () async {
  272 + await _dioClient.dio.post(
  273 + ApiPaths.v2StressScore,
  274 + data: {'data_list': dataList},
  275 + );
  276 + },
  277 + );
  278 + }
  279 +
  280 + Future<AppResult<void>> uploadV2SleepScoreResults(
  281 + List<Map<String, Object?>> dataList,
  282 + ) {
  283 + return safeCall(
  284 + call: () async {
  285 + await _dioClient.dio.post(
  286 + ApiPaths.v2SleepScore,
  287 + data: {'data_list': dataList},
  288 + );
  289 + },
  290 + );
  291 + }
  292 +
241 Future<AppResult<void>> uploadPulseTypeResult(PulseType pulseType) { 293 Future<AppResult<void>> uploadPulseTypeResult(PulseType pulseType) {
242 return safeCall( 294 return safeCall(
243 call: () async { 295 call: () async {
@@ -50,6 +50,7 @@ abstract final class ApiPaths { @@ -50,6 +50,7 @@ abstract final class ApiPaths {
50 static const v2HrvTrend = '/client/doublefeel/health/v2/hrv_trend/'; 50 static const v2HrvTrend = '/client/doublefeel/health/v2/hrv_trend/';
51 static const v2RealtimeStress = 51 static const v2RealtimeStress =
52 '/client/doublefeel/health/v2/realtime_stress/'; 52 '/client/doublefeel/health/v2/realtime_stress/';
  53 + static const v2SleepScore = '/client/doublefeel/health/v2/sleep_score/';
53 static const v2RealtimeStressPush = 54 static const v2RealtimeStressPush =
54 '/client/doublefeel/health/v2/realtime_stress/push/'; 55 '/client/doublefeel/health/v2/realtime_stress/push/';
55 static const v2ActivityTarget = 56 static const v2ActivityTarget =
@@ -96,6 +97,7 @@ abstract final class ApiPaths { @@ -96,6 +97,7 @@ abstract final class ApiPaths {
96 static const hmHealthData = '/client/doublefeel/huawei_hm/health_data/'; 97 static const hmHealthData = '/client/doublefeel/huawei_hm/health_data/';
97 static const hmSleepData = '/client/doublefeel/huawei_hm/sleep_data/'; 98 static const hmSleepData = '/client/doublefeel/huawei_hm/sleep_data/';
98 static const hmActivityTime = '/client/doublefeel/huawei_hm/activity_time/'; 99 static const hmActivityTime = '/client/doublefeel/huawei_hm/activity_time/';
  100 + static const hmActivityGoal = '/client/doublefeel/huawei_hm/activity_goal/';
99 static const hmPrivacyRecords = 101 static const hmPrivacyRecords =
100 '/client/doublefeel/huawei_hm/privacy_records/'; 102 '/client/doublefeel/huawei_hm/privacy_records/';
101 } 103 }
@@ -3,6 +3,7 @@ import 'dart:async'; @@ -3,6 +3,7 @@ import 'dart:async';
3 import 'package:flutter/foundation.dart'; 3 import 'package:flutter/foundation.dart';
4 4
5 import '../../../pigeon/health_kit_raw_data_api.g.dart'; 5 import '../../../pigeon/health_kit_raw_data_api.g.dart';
  6 +import '../../../data/models/health/health_v2_models.dart';
6 import '../../config/app_environment_config.dart'; 7 import '../../config/app_environment_config.dart';
7 import '../../network/api/health_api.dart'; 8 import '../../network/api/health_api.dart';
8 import 'health_raw_models.dart'; 9 import 'health_raw_models.dart';
@@ -255,6 +256,16 @@ class HealthRawDataCoreService { @@ -255,6 +256,16 @@ class HealthRawDataCoreService {
255 ); 256 );
256 } 257 }
257 258
  259 + Future<V2ActivityTarget?> queryOhosActivityGoal({
  260 + bool refresh = true,
  261 + }) {
  262 + final platform = _targetPlatform ?? defaultTargetPlatform;
  263 + if (platform == TargetPlatform.iOS) {
  264 + return Future<V2ActivityTarget?>.value();
  265 + }
  266 + return _ohosService.queryActivityGoal(refresh: refresh);
  267 + }
  268 +
258 Future<void> markHrvStressUploaded({ 269 Future<void> markHrvStressUploaded({
259 required Iterable<int> rawEndTimes, 270 required Iterable<int> rawEndTimes,
260 }) { 271 }) {
1 import 'package:flutter/foundation.dart'; 1 import 'package:flutter/foundation.dart';
2 2
  3 +import '../../../data/models/health/health_v2_models.dart';
3 import '../../platform/pigeon_api_facade.dart'; 4 import '../../platform/pigeon_api_facade.dart';
4 import 'platform_ohos/huawei_health_data_type.dart'; 5 import 'platform_ohos/huawei_health_data_type.dart';
5 import 'platform_ohos/ohos_harmony_health_raw_data_sync_service_factory.dart'; 6 import 'platform_ohos/ohos_harmony_health_raw_data_sync_service_factory.dart';
@@ -28,6 +29,8 @@ abstract class HealthRawDataSource { @@ -28,6 +29,8 @@ abstract class HealthRawDataSource {
28 int startTime, 29 int startTime,
29 int endTime, 30 int endTime,
30 ); 31 );
  32 +
  33 + Future<V2ActivityTarget?> getActivityGoal({bool refresh = true});
31 } 34 }
32 35
33 class PlatformHealthRawDataSource implements HealthRawDataSource { 36 class PlatformHealthRawDataSource implements HealthRawDataSource {
@@ -85,6 +88,11 @@ class PlatformHealthRawDataSource implements HealthRawDataSource { @@ -85,6 +88,11 @@ class PlatformHealthRawDataSource implements HealthRawDataSource {
85 ) { 88 ) {
86 return _currentDataSource.getRawWorkoutData(startTime, endTime); 89 return _currentDataSource.getRawWorkoutData(startTime, endTime);
87 } 90 }
  91 +
  92 + @override
  93 + Future<V2ActivityTarget?> getActivityGoal({bool refresh = true}) {
  94 + return _currentDataSource.getActivityGoal(refresh: refresh);
  95 + }
88 } 96 }
89 97
90 // IOS 98 // IOS
@@ -132,6 +140,11 @@ class AppleHealthRawDataSource implements HealthRawDataSource { @@ -132,6 +140,11 @@ class AppleHealthRawDataSource implements HealthRawDataSource {
132 ) { 140 ) {
133 return _rawDataApi.getHealthKitRawWorkoutData(startTime, endTime); 141 return _rawDataApi.getHealthKitRawWorkoutData(startTime, endTime);
134 } 142 }
  143 +
  144 + @override
  145 + Future<V2ActivityTarget?> getActivityGoal({bool refresh = true}) async {
  146 + return null;
  147 + }
135 } 148 }
136 149
137 // ohos 150 // ohos
@@ -149,6 +162,11 @@ class OhosHealthRawDataSource implements HealthRawDataSource { @@ -149,6 +162,11 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
149 final OhosHealthRawDataSyncService _syncService; 162 final OhosHealthRawDataSyncService _syncService;
150 final bool _syncBeforeRead; 163 final bool _syncBeforeRead;
151 164
  165 + static const String _activityLogMarker = '[OHOS_HEALTH_ACTIVITY_SOURCE]';
  166 + static const double _defaultActiveEnergyBurnedGoal = 300.0;
  167 + static const double _defaultExerciseTimeGoalSeconds = 1800.0;
  168 + static const double _defaultStandHoursGoal = 12.0;
  169 +
152 Future<List<OhosHealthRawDataSyncResult>> syncCalculationRawData({ 170 Future<List<OhosHealthRawDataSyncResult>> syncCalculationRawData({
153 int? startTime, 171 int? startTime,
154 int? endTime, 172 int? endTime,
@@ -174,6 +192,11 @@ class OhosHealthRawDataSource implements HealthRawDataSource { @@ -174,6 +192,11 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
174 } 192 }
175 193
176 @override 194 @override
  195 + Future<V2ActivityTarget?> getActivityGoal({bool refresh = true}) {
  196 + return _syncService.getActivityGoal(refresh: refresh);
  197 + }
  198 +
  199 + @override
177 Future<bool> hasHealthData() async { 200 Future<bool> hasHealthData() async {
178 // TODO: Query the OHOS local raw-data database when its schema is ready. 201 // TODO: Query the OHOS local raw-data database when its schema is ready.
179 return false; 202 return false;
@@ -243,7 +266,7 @@ class OhosHealthRawDataSource implements HealthRawDataSource { @@ -243,7 +266,7 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
243 if (_syncBeforeRead) { 266 if (_syncBeforeRead) {
244 await Future.wait( 267 await Future.wait(
245 <int>[ 268 <int>[
246 - OhosHealthRawDataType.activitySummary, 269 + HuaweiHealthDataType.activity.dataType,
247 HuaweiHealthDataType.exerciseDuration.dataType, 270 HuaweiHealthDataType.exerciseDuration.dataType,
248 HuaweiHealthDataType.standingDuration.dataType, 271 HuaweiHealthDataType.standingDuration.dataType,
249 HuaweiHealthDataType.stepCount.dataType, 272 HuaweiHealthDataType.stepCount.dataType,
@@ -258,7 +281,7 @@ class OhosHealthRawDataSource implements HealthRawDataSource { @@ -258,7 +281,7 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
258 ); 281 );
259 } 282 }
260 final activity = await _syncService.queryRawData( 283 final activity = await _syncService.queryRawData(
261 - dataType: OhosHealthRawDataType.activitySummary, 284 + dataType: HuaweiHealthDataType.activity.dataType,
262 startTime: startTime, 285 startTime: startTime,
263 endTime: endTime, 286 endTime: endTime,
264 ); 287 );
@@ -274,28 +297,27 @@ class OhosHealthRawDataSource implements HealthRawDataSource { @@ -274,28 +297,27 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
274 ); 297 );
275 final byTime = <int, HealthKitRawActivityDataPoint>{}; 298 final byTime = <int, HealthKitRawActivityDataPoint>{};
276 for (final item in activity) { 299 for (final item in activity) {
277 - final point = byTime.putIfAbsent(  
278 - item.dataTime,  
279 - () => HealthKitRawActivityDataPoint(endTime: item.dataTime),  
280 - ); 300 + final point = _activityPoint(byTime, item);
281 point.activeEnergyBurned = _doublePayload(item.payload, 'value'); 301 point.activeEnergyBurned = _doublePayload(item.payload, 'value');
282 } 302 }
283 for (final item in exercise) { 303 for (final item in exercise) {
284 - final point = byTime.putIfAbsent(  
285 - item.dataTime,  
286 - () => HealthKitRawActivityDataPoint(endTime: item.dataTime),  
287 - );  
288 - point.appleExerciseTime = _doublePayload(item.payload, 'value'); 304 + final point = _activityPoint(byTime, item);
  305 + final minutes = _doublePayload(item.payload, 'value');
  306 + point.appleExerciseTime =
  307 + minutes == null ? null : minutes * Duration.secondsPerMinute;
289 } 308 }
290 for (final item in standing) { 309 for (final item in standing) {
291 - final point = byTime.putIfAbsent(  
292 - item.dataTime,  
293 - () => HealthKitRawActivityDataPoint(endTime: item.dataTime),  
294 - ); 310 + final point = _activityPoint(byTime, item);
295 point.appleStandHours = _doublePayload(item.payload, 'value'); 311 point.appleStandHours = _doublePayload(item.payload, 'value');
296 } 312 }
297 - return byTime.values.toList(growable: false) 313 + final points = byTime.values.toList(growable: false)
298 ..sort((a, b) => a.endTime.compareTo(b.endTime)); 314 ..sort((a, b) => a.endTime.compareTo(b.endTime));
  315 + debugPrint(
  316 + '$_activityLogMarker query startTime=$startTime endTime=$endTime '
  317 + 'activity=${activity.length} exercise=${exercise.length} '
  318 + 'standing=${standing.length} points=${points.length}',
  319 + );
  320 + return points;
299 } 321 }
300 322
301 @override 323 @override
@@ -337,6 +359,25 @@ class OhosHealthRawDataSource implements HealthRawDataSource { @@ -337,6 +359,25 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
337 ); 359 );
338 } 360 }
339 361
  362 + HealthKitRawActivityDataPoint _activityPoint(
  363 + Map<int, HealthKitRawActivityDataPoint> byTime,
  364 + OhosHealthRawDataItem item,
  365 + ) {
  366 + final point = byTime.putIfAbsent(
  367 + item.dataTime,
  368 + () => HealthKitRawActivityDataPoint(
  369 + endTime: item.dataTime,
  370 + activeEnergyBurnedGoal: _defaultActiveEnergyBurnedGoal,
  371 + exerciseTimeGoal: _defaultExerciseTimeGoalSeconds,
  372 + standHoursGoal: _defaultStandHoursGoal,
  373 + ),
  374 + );
  375 + point.activeEnergyBurnedGoal ??= _defaultActiveEnergyBurnedGoal;
  376 + point.exerciseTimeGoal ??= _defaultExerciseTimeGoalSeconds;
  377 + point.standHoursGoal ??= _defaultStandHoursGoal;
  378 + return point;
  379 + }
  380 +
340 int _appleSleepType(int ohosSleepType) { 381 int _appleSleepType(int ohosSleepType) {
341 return switch (ohosSleepType) { 382 return switch (ohosSleepType) {
342 1 => 4, // deep 383 1 => 4, // deep
@@ -1820,6 +1820,20 @@ class HealthRawStressLocalStore { @@ -1820,6 +1820,20 @@ class HealthRawStressLocalStore {
1820 return rows.map(HealthRawHrvStressPoint.fromDb).toList(); 1820 return rows.map(HealthRawHrvStressPoint.fromDb).toList();
1821 } 1821 }
1822 1822
  1823 + Future<List<HealthRawHrvStressPoint>> queryPendingHrvStressPoints({
  1824 + required int userId,
  1825 + int limit = 200,
  1826 + }) async {
  1827 + final db = await _database(userId);
  1828 + final rows = await db.query(
  1829 + hrvResultsTable,
  1830 + where: 'uploaded IS NULL OR uploaded != 1',
  1831 + orderBy: 'raw_end_time ASC',
  1832 + limit: limit,
  1833 + );
  1834 + return rows.map(HealthRawHrvStressPoint.fromDb).toList();
  1835 + }
  1836 +
1823 Future<List<HealthRawRealtimeStressPoint>> queryRealtimeStressPoints({ 1837 Future<List<HealthRawRealtimeStressPoint>> queryRealtimeStressPoints({
1824 required int userId, 1838 required int userId,
1825 required int startTime, 1839 required int startTime,
@@ -1835,6 +1849,20 @@ class HealthRawStressLocalStore { @@ -1835,6 +1849,20 @@ class HealthRawStressLocalStore {
1835 return rows.map(HealthRawRealtimeStressPoint.fromDb).toList(); 1849 return rows.map(HealthRawRealtimeStressPoint.fromDb).toList();
1836 } 1850 }
1837 1851
  1852 + Future<List<HealthRawRealtimeStressPoint>> queryPendingRealtimeStressPoints({
  1853 + required int userId,
  1854 + int limit = 200,
  1855 + }) async {
  1856 + final db = await _database(userId);
  1857 + final rows = await db.query(
  1858 + realtimeStressResultsTable,
  1859 + where: 'uploaded IS NULL OR uploaded != 1',
  1860 + orderBy: 'raw_end_time ASC',
  1861 + limit: limit,
  1862 + );
  1863 + return rows.map(HealthRawRealtimeStressPoint.fromDb).toList();
  1864 + }
  1865 +
1838 Future<List<HealthRawDailyStressPoint>> queryDailyStressPoints({ 1866 Future<List<HealthRawDailyStressPoint>> queryDailyStressPoints({
1839 required int userId, 1867 required int userId,
1840 required int startDate, 1868 required int startDate,
@@ -1850,6 +1878,20 @@ class HealthRawStressLocalStore { @@ -1850,6 +1878,20 @@ class HealthRawStressLocalStore {
1850 return rows.map(HealthRawDailyStressPoint.fromDb).toList(); 1878 return rows.map(HealthRawDailyStressPoint.fromDb).toList();
1851 } 1879 }
1852 1880
  1881 + Future<List<HealthRawDailyStressPoint>> queryPendingDailyStressPoints({
  1882 + required int userId,
  1883 + int limit = 200,
  1884 + }) async {
  1885 + final db = await _database(userId);
  1886 + final rows = await db.query(
  1887 + dailyStressResultsTable,
  1888 + where: 'uploaded IS NULL OR uploaded != 1',
  1889 + orderBy: 'date ASC',
  1890 + limit: limit,
  1891 + );
  1892 + return rows.map(HealthRawDailyStressPoint.fromDb).toList();
  1893 + }
  1894 +
1853 Future<List<HealthRawSleepResult>> querySleepResults({ 1895 Future<List<HealthRawSleepResult>> querySleepResults({
1854 required int userId, 1896 required int userId,
1855 required int startTime, 1897 required int startTime,
@@ -1865,6 +1907,20 @@ class HealthRawStressLocalStore { @@ -1865,6 +1907,20 @@ class HealthRawStressLocalStore {
1865 return rows.map(HealthRawSleepResult.fromDb).toList(); 1907 return rows.map(HealthRawSleepResult.fromDb).toList();
1866 } 1908 }
1867 1909
  1910 + Future<List<HealthRawSleepResult>> queryPendingSleepResults({
  1911 + required int userId,
  1912 + int limit = 200,
  1913 + }) async {
  1914 + final db = await _database(userId);
  1915 + final rows = await db.query(
  1916 + sleepResultsTable,
  1917 + where: 'uploaded IS NULL OR uploaded != 1',
  1918 + orderBy: 'date ASC',
  1919 + limit: limit,
  1920 + );
  1921 + return rows.map(HealthRawSleepResult.fromDb).toList();
  1922 + }
  1923 +
1868 Future<HealthRawStressDbRows> debugQueryAllRows(int userId) async { 1924 Future<HealthRawStressDbRows> debugQueryAllRows(int userId) async {
1869 final db = await _database(userId); 1925 final db = await _database(userId);
1870 final hrvRows = await db.query( 1926 final hrvRows = await db.query(
@@ -3,6 +3,7 @@ import 'package:doublefeel_flutter/core/result/app_result.dart'; @@ -3,6 +3,7 @@ import 'package:doublefeel_flutter/core/result/app_result.dart';
3 import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart'; 3 import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart';
4 import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart'; 4 import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
5 import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart'; 5 import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart';
  6 +import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
6 import 'package:get/get.dart'; 7 import 'package:get/get.dart';
7 8
8 import 'huawei_health_data_type.dart'; 9 import 'huawei_health_data_type.dart';
@@ -49,4 +50,9 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { @@ -49,4 +50,9 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient {
49 Future<AppResult<HmWorkoutData>> getWorkoutData(int startTime, int endTime) { 50 Future<AppResult<HmWorkoutData>> getWorkoutData(int startTime, int endTime) {
50 return _harmonyApi.getWorkoutData(startTime, endTime); 51 return _harmonyApi.getWorkoutData(startTime, endTime);
51 } 52 }
  53 +
  54 + @override
  55 + Future<AppResult<V2ActivityTarget>> getActivityGoal() {
  56 + return _harmonyApi.getActivityGoal();
  57 + }
52 } 58 }
@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
8 import 'package:get/get.dart'; 8 import 'package:get/get.dart';
9 9
10 import '../../../../data/models/enums/app_enums.dart'; 10 import '../../../../data/models/enums/app_enums.dart';
  11 +import '../../../../data/models/health/health_v2_models.dart';
11 import '../../../../pigeon/health_kit_raw_data_api.g.dart'; 12 import '../../../../pigeon/health_kit_raw_data_api.g.dart';
12 import '../../../config/app_environment_config.dart'; 13 import '../../../config/app_environment_config.dart';
13 import '../../../logging/app_logger.dart'; 14 import '../../../logging/app_logger.dart';
@@ -20,6 +21,7 @@ import '../health_raw_stress_calculator.dart'; @@ -20,6 +21,7 @@ import '../health_raw_stress_calculator.dart';
20 import '../health_sleep_calculator.dart'; 21 import '../health_sleep_calculator.dart';
21 import '../platform_ios/apple_health_raw_data_core_service.dart'; 22 import '../platform_ios/apple_health_raw_data_core_service.dart';
22 import 'ohos_health_raw_data_sync_service.dart'; 23 import 'ohos_health_raw_data_sync_service.dart';
  24 +import 'ohos_health_raw_result_upload_service.dart';
23 25
24 class OHOSHealthRawDataCoreService { 26 class OHOSHealthRawDataCoreService {
25 static const int defaultLookbackDays = 183; 27 static const int defaultLookbackDays = 183;
@@ -34,6 +36,7 @@ class OHOSHealthRawDataCoreService { @@ -34,6 +36,7 @@ class OHOSHealthRawDataCoreService {
34 HealthApi? serverHealthApi, 36 HealthApi? serverHealthApi,
35 HarmonyApi? harmonyApi, 37 HarmonyApi? harmonyApi,
36 Future<bool> Function()? healthReadAuthorizationChecker, 38 Future<bool> Function()? healthReadAuthorizationChecker,
  39 + OhosHealthRawResultUploadService? resultUploadService,
37 }) : _rawDataSource = rawDataSource ?? 40 }) : _rawDataSource = rawDataSource ??
38 OhosHealthRawDataSource( 41 OhosHealthRawDataSource(
39 userIdProvider: userIdProvider, 42 userIdProvider: userIdProvider,
@@ -47,7 +50,8 @@ class OHOSHealthRawDataCoreService { @@ -47,7 +50,8 @@ class OHOSHealthRawDataCoreService {
47 _serverHealthApi = serverHealthApi, 50 _serverHealthApi = serverHealthApi,
48 _harmonyApi = harmonyApi ?? 51 _harmonyApi = harmonyApi ??
49 (Get.isRegistered<HarmonyApi>() ? Get.find<HarmonyApi>() : null), 52 (Get.isRegistered<HarmonyApi>() ? Get.find<HarmonyApi>() : null),
50 - _healthReadAuthorizationChecker = healthReadAuthorizationChecker; 53 + _healthReadAuthorizationChecker = healthReadAuthorizationChecker,
  54 + _resultUploadService = resultUploadService;
51 55
52 final HealthRawDataSource _rawDataSource; 56 final HealthRawDataSource _rawDataSource;
53 final HealthRawStressLocalStore _localStore; 57 final HealthRawStressLocalStore _localStore;
@@ -57,6 +61,7 @@ class OHOSHealthRawDataCoreService { @@ -57,6 +61,7 @@ class OHOSHealthRawDataCoreService {
57 final HealthApi? _serverHealthApi; 61 final HealthApi? _serverHealthApi;
58 final HarmonyApi? _harmonyApi; 62 final HarmonyApi? _harmonyApi;
59 final Future<bool> Function()? _healthReadAuthorizationChecker; 63 final Future<bool> Function()? _healthReadAuthorizationChecker;
  64 + OhosHealthRawResultUploadService? _resultUploadService;
60 final StreamController<HealthRawDataUpdatedEvent> 65 final StreamController<HealthRawDataUpdatedEvent>
61 _healthDataUpdatedController = 66 _healthDataUpdatedController =
62 StreamController<HealthRawDataUpdatedEvent>.broadcast(); 67 StreamController<HealthRawDataUpdatedEvent>.broadcast();
@@ -84,11 +89,12 @@ class OHOSHealthRawDataCoreService { @@ -84,11 +89,12 @@ class OHOSHealthRawDataCoreService {
84 return _rawDataSource.hasHealthData(); 89 return _rawDataSource.hasHealthData();
85 } 90 }
86 91
87 - Future<bool> performHealthDataUpload() async {  
88 - // TODO: Trigger OHOS raw-data upload when the OHOS upload API is ready.  
89 - return false; 92 + Future<V2ActivityTarget?> queryActivityGoal({bool refresh = true}) {
  93 + return _rawDataSource.getActivityGoal(refresh: refresh);
90 } 94 }
91 95
  96 + Future<bool> performHealthDataUpload() => _uploadResults();
  97 +
92 Future<bool> syncDatabaseToNativeIfNeeded() async { 98 Future<bool> syncDatabaseToNativeIfNeeded() async {
93 // OHOS must not call the Apple Pigeon database sync path. 99 // OHOS must not call the Apple Pigeon database sync path.
94 return false; 100 return false;
@@ -547,10 +553,24 @@ class OHOSHealthRawDataCoreService { @@ -547,10 +553,24 @@ class OHOSHealthRawDataCoreService {
547 ); 553 );
548 } 554 }
549 555
550 - Future<void> _uploadResults() async {  
551 - // TODO: Upload OHOS result rows through OHOS/backend APIs and mark uploaded  
552 - // rows in _localStore after the contract is available.  
553 - if (_serverHealthApi == null) return; 556 + Future<bool> _uploadResults() async {
  557 + final serverHealthApi = _serverHealthApi;
  558 + if (serverHealthApi == null) {
  559 + _logInfo('upload results skipped: HealthApi missing');
  560 + return false;
  561 + }
  562 + final uploader = _resultUploadService ??= OhosHealthRawResultUploadService(
  563 + healthApi: serverHealthApi,
  564 + localStore: _localStore,
  565 + );
  566 + try {
  567 + final summary = await uploader.uploadPendingResults(userId: _userId);
  568 + _logInfo('upload results finish: $summary');
  569 + return summary.success;
  570 + } catch (error, stackTrace) {
  571 + _logError('upload results failed', error, stackTrace);
  572 + return false;
  573 + }
554 } 574 }
555 575
556 Future<List<OhosHealthRawDataSyncResult>> _syncCalculationRawDataIfNeeded({ 576 Future<List<OhosHealthRawDataSyncResult>> _syncCalculationRawDataIfNeeded({
1 import 'dart:io'; 1 import 'dart:io';
2 2
3 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 3 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
  4 +import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
4 import 'package:flutter/foundation.dart'; 5 import 'package:flutter/foundation.dart';
5 import 'package:path_provider/path_provider.dart'; 6 import 'package:path_provider/path_provider.dart';
6 import 'package:sqflite/sqflite.dart'; 7 import 'package:sqflite/sqflite.dart';
@@ -21,6 +22,7 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore { @@ -21,6 +22,7 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore {
21 22
22 static const rawDataTable = 'ohos_raw_data'; 23 static const rawDataTable = 'ohos_raw_data';
23 static const sleepDataTable = 'ohos_sleep_data'; 24 static const sleepDataTable = 'ohos_sleep_data';
  25 + static const activityGoalTable = 'ohos_activity_goal';
24 static const logMarker = '[OHOS_RAW_DATA_DB]'; 26 static const logMarker = '[OHOS_RAW_DATA_DB]';
25 27
26 final int Function()? _userIdProvider; 28 final int Function()? _userIdProvider;
@@ -203,6 +205,54 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore { @@ -203,6 +205,54 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore {
203 .toList(growable: false); 205 .toList(growable: false);
204 } 206 }
205 207
  208 + @override
  209 + Future<void> upsertActivityGoal(V2ActivityTarget goal) async {
  210 + final db = await _database(_userId);
  211 + await _createActivityGoalTable(db);
  212 + final now = _nowProvider().millisecondsSinceEpoch ~/ 1000;
  213 + await db.insert(
  214 + activityGoalTable,
  215 + <String, Object?>{
  216 + 'id': 1,
  217 + 'move': goal.move,
  218 + 'step': goal.step,
  219 + 'exercise': goal.exercise,
  220 + 'stand': goal.stand,
  221 + 'sleep_target_duration': goal.sleepTargetDuration,
  222 + 'create_time': goal.createTime ?? now,
  223 + 'update_time': now,
  224 + },
  225 + conflictAlgorithm: ConflictAlgorithm.replace,
  226 + );
  227 + _log(
  228 + 'activity_goal_stored move=${goal.move} step=${goal.step} '
  229 + 'exercise=${goal.exercise} stand=${goal.stand}',
  230 + );
  231 + }
  232 +
  233 + @override
  234 + Future<V2ActivityTarget?> queryActivityGoal() async {
  235 + final db = await _database(_userId);
  236 + await _createActivityGoalTable(db);
  237 + final rows = await db.query(
  238 + activityGoalTable,
  239 + orderBy: 'update_time DESC',
  240 + limit: 1,
  241 + );
  242 + if (rows.isEmpty) return null;
  243 + final row = rows.first;
  244 + return V2ActivityTarget(
  245 + userId: _userId,
  246 + move: row['move'] as int?,
  247 + step: row['step'] as int?,
  248 + stand: row['stand'] as int?,
  249 + exercise: row['exercise'] as int?,
  250 + createTime: row['create_time'] as int?,
  251 + updateTime: row['update_time'] as int?,
  252 + sleepTargetDuration: row['sleep_target_duration'] as int?,
  253 + );
  254 + }
  255 +
206 Future<void> close({int? userId}) async { 256 Future<void> close({int? userId}) async {
207 if (userId != null) { 257 if (userId != null) {
208 final db = _opened.remove(userId); 258 final db = _opened.remove(userId);
@@ -264,6 +314,22 @@ CREATE TABLE IF NOT EXISTS $sleepDataTable ( @@ -264,6 +314,22 @@ CREATE TABLE IF NOT EXISTS $sleepDataTable (
264 'CREATE INDEX IF NOT EXISTS idx_ohos_sleep_data_time ' 314 'CREATE INDEX IF NOT EXISTS idx_ohos_sleep_data_time '
265 'ON $sleepDataTable(to_time)', 315 'ON $sleepDataTable(to_time)',
266 ); 316 );
  317 + await _createActivityGoalTable(db);
  318 + }
  319 +
  320 + Future<void> _createActivityGoalTable(DatabaseExecutor db) async {
  321 + await db.execute('''
  322 +CREATE TABLE IF NOT EXISTS $activityGoalTable (
  323 + id INTEGER PRIMARY KEY,
  324 + move INTEGER,
  325 + step INTEGER,
  326 + exercise INTEGER,
  327 + stand INTEGER,
  328 + sleep_target_duration INTEGER,
  329 + create_time INTEGER NOT NULL,
  330 + update_time INTEGER NOT NULL
  331 +)
  332 +''');
267 } 333 }
268 334
269 Future<void> _createRawDataTypeTable( 335 Future<void> _createRawDataTypeTable(
@@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; @@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
4 import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart'; 4 import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart';
5 import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart'; 5 import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
6 import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart'; 6 import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart';
  7 +import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
7 import 'package:flutter/foundation.dart'; 8 import 'package:flutter/foundation.dart';
8 9
9 import 'huawei_health_data_type.dart'; 10 import 'huawei_health_data_type.dart';
@@ -153,6 +154,34 @@ class OhosHealthRawDataSyncService { @@ -153,6 +154,34 @@ class OhosHealthRawDataSyncService {
153 ); 154 );
154 } 155 }
155 156
  157 + Future<V2ActivityTarget?> getActivityGoal({bool refresh = true}) async {
  158 + if (refresh) {
  159 + try {
  160 + final remoteGoal = await _remoteDataSource.fetchActivityGoal();
  161 + if (remoteGoal != null) {
  162 + await _localStore.upsertActivityGoal(remoteGoal);
  163 + _log(
  164 + 'activity_goal_remote_success '
  165 + 'move=${remoteGoal.move} step=${remoteGoal.step} '
  166 + 'exercise=${remoteGoal.exercise} stand=${remoteGoal.stand}',
  167 + );
  168 + return remoteGoal;
  169 + }
  170 + } catch (error, stackTrace) {
  171 + _log(
  172 + 'activity_goal_remote_failed error=$error stackTrace=$stackTrace',
  173 + );
  174 + }
  175 + }
  176 + final localGoal = await _localStore.queryActivityGoal();
  177 + _log(
  178 + 'activity_goal_local_fallback found=${localGoal != null} '
  179 + 'move=${localGoal?.move} step=${localGoal?.step} '
  180 + 'exercise=${localGoal?.exercise} stand=${localGoal?.stand}',
  181 + );
  182 + return localGoal;
  183 + }
  184 +
156 Future<OhosHealthRawDataSyncResult> _syncResolvedRawData({ 185 Future<OhosHealthRawDataSyncResult> _syncResolvedRawData({
157 required int dataType, 186 required int dataType,
158 required int startTime, 187 required int startTime,
@@ -334,6 +363,8 @@ abstract class OhosHealthRawDataRemoteDataSource { @@ -334,6 +363,8 @@ abstract class OhosHealthRawDataRemoteDataSource {
334 required int endTime, 363 required int endTime,
335 String? pageToken, 364 String? pageToken,
336 }); 365 });
  366 +
  367 + Future<V2ActivityTarget?> fetchActivityGoal();
337 } 368 }
338 369
339 abstract class OhosHealthRawDataLocalStore { 370 abstract class OhosHealthRawDataLocalStore {
@@ -349,6 +380,10 @@ abstract class OhosHealthRawDataLocalStore { @@ -349,6 +380,10 @@ abstract class OhosHealthRawDataLocalStore {
349 required int startTime, 380 required int startTime,
350 required int endTime, 381 required int endTime,
351 }); 382 });
  383 +
  384 + Future<void> upsertActivityGoal(V2ActivityTarget goal);
  385 +
  386 + Future<V2ActivityTarget?> queryActivityGoal();
352 } 387 }
353 388
354 abstract class OhosHarmonyRawDataClient { 389 abstract class OhosHarmonyRawDataClient {
@@ -367,6 +402,8 @@ abstract class OhosHarmonyRawDataClient { @@ -367,6 +402,8 @@ abstract class OhosHarmonyRawDataClient {
367 int startTime, 402 int startTime,
368 int endTime, 403 int endTime,
369 ); 404 );
  405 +
  406 + Future<AppResult<V2ActivityTarget>> getActivityGoal();
370 } 407 }
371 408
372 class OhosHarmonyHealthRawDataRemoteDataSource 409 class OhosHarmonyHealthRawDataRemoteDataSource
@@ -435,6 +472,17 @@ class OhosHarmonyHealthRawDataRemoteDataSource @@ -435,6 +472,17 @@ class OhosHarmonyHealthRawDataRemoteDataSource
435 }; 472 };
436 } 473 }
437 474
  475 + @override
  476 + Future<V2ActivityTarget?> fetchActivityGoal() async {
  477 + final result = await _client.getActivityGoal();
  478 + return switch (result) {
  479 + AppSuccess<V2ActivityTarget>(:final data) => data,
  480 + AppFailure<V2ActivityTarget>(:final error) => throw StateError(
  481 + 'OHOS activity goal fetch failed: $error',
  482 + ),
  483 + };
  484 + }
  485 +
438 HuaweiHealthDataType _resolveHuaweiDataType(int dataType) { 486 HuaweiHealthDataType _resolveHuaweiDataType(int dataType) {
439 if (dataType == OhosHealthRawDataType.activitySummary) { 487 if (dataType == OhosHealthRawDataType.activitySummary) {
440 return HuaweiHealthDataType.activity; 488 return HuaweiHealthDataType.activity;
@@ -521,6 +569,12 @@ class TodoOhosHealthRawDataRemoteDataSource @@ -521,6 +569,12 @@ class TodoOhosHealthRawDataRemoteDataSource
521 // are available. 569 // are available.
522 return const OhosHealthRawDataPage(items: <OhosHealthRawDataItem>[]); 570 return const OhosHealthRawDataPage(items: <OhosHealthRawDataItem>[]);
523 } 571 }
  572 +
  573 + @override
  574 + Future<V2ActivityTarget?> fetchActivityGoal() async {
  575 + // TODO: Call the OHOS activity-goal API after HarmonyApi is registered.
  576 + return null;
  577 + }
524 } 578 }
525 579
526 class TodoOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { 580 class TodoOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
@@ -549,6 +603,16 @@ class TodoOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { @@ -549,6 +603,16 @@ class TodoOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
549 }) async { 603 }) async {
550 return const <OhosHealthRawDataItem>[]; 604 return const <OhosHealthRawDataItem>[];
551 } 605 }
  606 +
  607 + @override
  608 + Future<void> upsertActivityGoal(V2ActivityTarget goal) async {
  609 + // TODO: Store OHOS activity goal into local database.
  610 + }
  611 +
  612 + @override
  613 + Future<V2ActivityTarget?> queryActivityGoal() async {
  614 + return null;
  615 + }
552 } 616 }
553 617
554 class OhosHealthRawDataPage { 618 class OhosHealthRawDataPage {
  1 +import 'package:flutter/foundation.dart';
  2 +
  3 +import '../../../logging/app_logger.dart';
  4 +import '../../../network/api/health_api.dart';
  5 +import '../../../result/app_result.dart';
  6 +import '../health_raw_models.dart';
  7 +import '../platform_ios/apple_health_raw_data_core_service.dart';
  8 +
  9 +class OhosHealthRawResultUploadService {
  10 + OhosHealthRawResultUploadService({
  11 + required HealthApi healthApi,
  12 + required HealthRawStressLocalStore localStore,
  13 + int batchSize = 200,
  14 + }) : _healthApi = healthApi,
  15 + _localStore = localStore,
  16 + _batchSize = batchSize;
  17 +
  18 + static const logMarker = '[OHOS_HEALTH_RESULT_UPLOAD]';
  19 +
  20 + final HealthApi _healthApi;
  21 + final HealthRawStressLocalStore _localStore;
  22 + final int _batchSize;
  23 + Future<OhosHealthRawResultUploadSummary>? _runningUpload;
  24 +
  25 + Future<OhosHealthRawResultUploadSummary> uploadPendingResults({
  26 + required int userId,
  27 + }) async {
  28 + final running = _runningUpload;
  29 + if (running != null) {
  30 + _log('upload_duplicate_join userId=$userId');
  31 + return running;
  32 + }
  33 + final task = _uploadPendingResults(userId: userId);
  34 + _runningUpload = task;
  35 + try {
  36 + return await task;
  37 + } finally {
  38 + if (identical(_runningUpload, task)) {
  39 + _runningUpload = null;
  40 + }
  41 + }
  42 + }
  43 +
  44 + Future<OhosHealthRawResultUploadSummary> _uploadPendingResults({
  45 + required int userId,
  46 + }) async {
  47 + _log('upload_start userId=$userId');
  48 + final summary = OhosHealthRawResultUploadSummary();
  49 +
  50 + await _uploadKind(
  51 + kind: 'hrv',
  52 + summary: summary,
  53 + query: () => _localStore.queryPendingHrvStressPoints(
  54 + userId: userId,
  55 + limit: _batchSize,
  56 + ),
  57 + toBody: (points) => points.map(_hrvBody).toList(growable: false),
  58 + upload: _healthApi.uploadV2HrvTrendResults,
  59 + markUploadedUntil: (points) => _localStore.markHrvStressUploadedUntil(
  60 + userId: userId,
  61 + rawEndTime: points.last.rawEndTime,
  62 + ),
  63 + latestTime: (points) => points.last.rawEndTime,
  64 + );
  65 + await _uploadKind(
  66 + kind: 'realtimeStress',
  67 + summary: summary,
  68 + query: () => _localStore.queryPendingRealtimeStressPoints(
  69 + userId: userId,
  70 + limit: _batchSize,
  71 + ),
  72 + toBody: (points) => points.map(_realtimeBody).toList(growable: false),
  73 + upload: _healthApi.uploadV2RealtimeStressResults,
  74 + markUploadedUntil: (points) =>
  75 + _localStore.markRealtimeStressUploadedUntil(
  76 + userId: userId,
  77 + rawEndTime: points.last.rawEndTime,
  78 + ),
  79 + latestTime: (points) => points.last.rawEndTime,
  80 + );
  81 + await _uploadKind(
  82 + kind: 'dailyStress',
  83 + summary: summary,
  84 + query: () => _localStore.queryPendingDailyStressPoints(
  85 + userId: userId,
  86 + limit: _batchSize,
  87 + ),
  88 + toBody: (points) => points.map(_dailyStressBody).toList(growable: false),
  89 + upload: _healthApi.uploadV2DailyStressResults,
  90 + markUploadedUntil: (points) => _localStore.markDailyStressUploadedUntil(
  91 + userId: userId,
  92 + date: points.last.date,
  93 + ),
  94 + latestTime: (points) => points.last.date,
  95 + );
  96 + await _uploadKind(
  97 + kind: 'sleepScore',
  98 + summary: summary,
  99 + query: () => _localStore.queryPendingSleepResults(
  100 + userId: userId,
  101 + limit: _batchSize,
  102 + ),
  103 + toBody: (points) => points.map(_sleepBody).toList(growable: false),
  104 + upload: _healthApi.uploadV2SleepScoreResults,
  105 + markUploadedUntil: (points) => _localStore.markSleepResultsUploadedUntil(
  106 + userId: userId,
  107 + date: points.last.date,
  108 + ),
  109 + latestTime: (points) => points.last.date,
  110 + );
  111 +
  112 + _log(
  113 + 'upload_finish userId=$userId success=${summary.success} '
  114 + 'hrv=${summary.hrvCount} realtime=${summary.realtimeStressCount} '
  115 + 'daily=${summary.dailyStressCount} sleep=${summary.sleepScoreCount} '
  116 + 'errors=${summary.errors.length}',
  117 + );
  118 + return summary;
  119 + }
  120 +
  121 + Future<void> _uploadKind<T>({
  122 + required String kind,
  123 + required OhosHealthRawResultUploadSummary summary,
  124 + required Future<List<T>> Function() query,
  125 + required List<Map<String, Object?>> Function(List<T>) toBody,
  126 + required Future<AppResult<void>> Function(List<Map<String, Object?>>)
  127 + upload,
  128 + required Future<void> Function(List<T>) markUploadedUntil,
  129 + required int Function(List<T>) latestTime,
  130 + }) async {
  131 + while (true) {
  132 + final points = await query();
  133 + if (points.isEmpty) {
  134 + _log('upload_skip kind=$kind reason=no_pending');
  135 + return;
  136 + }
  137 + final body = toBody(points);
  138 + final until = latestTime(points);
  139 + _log('upload_batch_start kind=$kind count=${points.length} until=$until');
  140 + final result = await upload(body);
  141 + switch (result) {
  142 + case AppSuccess():
  143 + await markUploadedUntil(points);
  144 + summary.add(kind, points.length);
  145 + _log(
  146 + 'upload_batch_success kind=$kind count=${points.length} '
  147 + 'until=$until',
  148 + );
  149 + case AppFailure(:final error):
  150 + summary.addError(kind, error.toString());
  151 + _log(
  152 + 'upload_batch_failed kind=$kind count=${points.length} '
  153 + 'until=$until error=$error',
  154 + );
  155 + return;
  156 + }
  157 + }
  158 + }
  159 +
  160 + Map<String, Object?> _hrvBody(HealthRawHrvStressPoint point) {
  161 + return <String, Object?>{
  162 + 'data_time': point.rawEndTime,
  163 + 'raw_hrv': point.rawHrv,
  164 + 'trend_hrv': point.result,
  165 + 'state': point.state.value,
  166 + 'hr_baseline': point.baselineRestingHr,
  167 + 'hrv_baseline': point.baselineHrv,
  168 + 'is_asleep': point.flags.isSleepLikely ? 1 : 0,
  169 + 'is_workout': point.flags.isWorkout ? 1 : 0,
  170 + 'is_workout_recovery': point.flags.isWorkoutRecovery ? 1 : 0,
  171 + 'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
  172 + };
  173 + }
  174 +
  175 + Map<String, Object?> _realtimeBody(HealthRawRealtimeStressPoint point) {
  176 + return <String, Object?>{
  177 + 'data_time': point.rawEndTime,
  178 + 'hr_value': point.rawHr,
  179 + 'stress_value': point.result,
  180 + 'state': point.state.value,
  181 + 'is_asleep': point.flags.isSleepLikely ? 1 : 0,
  182 + 'is_workout': point.flags.isWorkout ? 1 : 0,
  183 + 'is_workout_recovery': point.flags.isWorkoutRecovery ? 1 : 0,
  184 + 'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
  185 + };
  186 + }
  187 +
  188 + Map<String, Object?> _dailyStressBody(HealthRawDailyStressPoint point) {
  189 + return <String, Object?>{
  190 + 'stress_value': point.stressValue,
  191 + 'stress_score': point.stressScore,
  192 + 'state': point.state.value,
  193 + 'data_time': point.dataTime,
  194 + };
  195 + }
  196 +
  197 + Map<String, Object?> _sleepBody(HealthRawSleepResult result) {
  198 + return <String, Object?>{
  199 + 'date': result.date,
  200 + 'sleep_score': result.sleepScore,
  201 + 'sleep_state': result.sleepState,
  202 + 'in_bed_minutes': result.inBedMinutes,
  203 + 'awak_minutes': result.awakMinutes,
  204 + 'sleep_minutes': result.sleepMinutes,
  205 + };
  206 + }
  207 +
  208 + void _log(String message) {
  209 + final taggedMessage = '$logMarker $message';
  210 + debugPrint(taggedMessage);
  211 + try {
  212 + AppLogger.i(taggedMessage);
  213 + } catch (_) {
  214 + // AppLogger may be unavailable in early bootstrap/test contexts.
  215 + }
  216 + }
  217 +}
  218 +
  219 +class OhosHealthRawResultUploadSummary {
  220 + int hrvCount = 0;
  221 + int realtimeStressCount = 0;
  222 + int dailyStressCount = 0;
  223 + int sleepScoreCount = 0;
  224 + final Map<String, String> errors = <String, String>{};
  225 +
  226 + bool get success => errors.isEmpty;
  227 +
  228 + void add(String kind, int count) {
  229 + switch (kind) {
  230 + case 'hrv':
  231 + hrvCount += count;
  232 + case 'realtimeStress':
  233 + realtimeStressCount += count;
  234 + case 'dailyStress':
  235 + dailyStressCount += count;
  236 + case 'sleepScore':
  237 + sleepScoreCount += count;
  238 + }
  239 + }
  240 +
  241 + void addError(String kind, String error) {
  242 + errors[kind] = error;
  243 + }
  244 +
  245 + @override
  246 + String toString() {
  247 + return 'OhosHealthRawResultUploadSummary('
  248 + 'success=$success, '
  249 + 'hrvCount=$hrvCount, '
  250 + 'realtimeStressCount=$realtimeStressCount, '
  251 + 'dailyStressCount=$dailyStressCount, '
  252 + 'sleepScoreCount=$sleepScoreCount, '
  253 + 'errors=$errors'
  254 + ')';
  255 + }
  256 +}
@@ -8,6 +8,7 @@ import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; @@ -8,6 +8,7 @@ import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
8 import 'package:doublefeel_flutter/data/models/health/hrv/hrv_statistics_data.dart'; 8 import 'package:doublefeel_flutter/data/models/health/hrv/hrv_statistics_data.dart';
9 import 'package:doublefeel_flutter/data/models/health/sleep/sleep_statistics_data.dart'; 9 import 'package:doublefeel_flutter/data/models/health/sleep/sleep_statistics_data.dart';
10 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; 10 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
  11 +import 'package:flutter/foundation.dart';
11 12
12 import 'health_datasource.dart'; 13 import 'health_datasource.dart';
13 import 'health_local_data_convert.dart'; 14 import 'health_local_data_convert.dart';
@@ -17,6 +18,12 @@ class LocalHealthDataSource implements HealthDataSource { @@ -17,6 +18,12 @@ class LocalHealthDataSource implements HealthDataSource {
17 18
18 final HealthRawDataCoreService coreService; 19 final HealthRawDataCoreService coreService;
19 20
  21 + static const int _ohosDefaultMoveGoal = 300;
  22 + static const int _ohosDefaultStepGoal = 8000;
  23 + static const int _ohosDefaultStandGoal = 12;
  24 + static const int _ohosDefaultExerciseGoalSeconds = 30 * 60;
  25 + static const int _ohosDefaultSleepTargetSeconds = 8 * 60 * 60;
  26 +
20 @override 27 @override
21 Future<AppResult<SleepStatisticsData>> getSleepStatistics( 28 Future<AppResult<SleepStatisticsData>> getSleepStatistics(
22 int dateRangeType, 29 int dateRangeType,
@@ -84,6 +91,11 @@ class LocalHealthDataSource implements HealthDataSource { @@ -84,6 +91,11 @@ class LocalHealthDataSource implements HealthDataSource {
84 startTime: queryStart, 91 startTime: queryStart,
85 endTime: queryEnd, 92 endTime: queryEnd,
86 ); 93 );
  94 + final resolvedActivity = await _resolveOhosActivityGoalsIfNeeded(
  95 + activity: activity,
  96 + userId: queryUserId ?? 0,
  97 + fallbackUpdateTime: queryEnd,
  98 + );
87 final heartRate = await coreService.queryRawDataPoints( 99 final heartRate = await coreService.queryRawDataPoints(
88 dataType: HealthDataUploadType.heartRate.type, 100 dataType: HealthDataUploadType.heartRate.type,
89 startTime: queryStart, 101 startTime: queryStart,
@@ -94,7 +106,7 @@ class LocalHealthDataSource implements HealthDataSource { @@ -94,7 +106,7 @@ class LocalHealthDataSource implements HealthDataSource {
94 LocalHealthDataConvert.activityBurnStatistics( 106 LocalHealthDataConvert.activityBurnStatistics(
95 days: days, 107 days: days,
96 previousDays: previousDays, 108 previousDays: previousDays,
97 - activity: activity, 109 + activity: resolvedActivity,
98 heartRate: heartRate, 110 heartRate: heartRate,
99 sleepIntervals: [], 111 sleepIntervals: [],
100 ), 112 ),
@@ -181,6 +193,17 @@ class LocalHealthDataSource implements HealthDataSource { @@ -181,6 +193,17 @@ class LocalHealthDataSource implements HealthDataSource {
181 endTime: queryEnd, 193 endTime: queryEnd,
182 ); 194 );
183 final resolvedActivity = _resolveActivityTarget(activity); 195 final resolvedActivity = _resolveActivityTarget(activity);
  196 + if (_isOhosPlatform) {
  197 + final activityGoal = await coreService.queryOhosActivityGoal();
  198 + return AppSuccess(
  199 + _ohosActivityTarget(
  200 + userId: queryUserId ?? 0,
  201 + remoteOrCachedTarget: activityGoal,
  202 + activity: resolvedActivity,
  203 + fallbackUpdateTime: queryEnd,
  204 + ),
  205 + );
  206 + }
184 return AppSuccess( 207 return AppSuccess(
185 LocalHealthDataConvert.v2ActivityTarget( 208 LocalHealthDataConvert.v2ActivityTarget(
186 userId: queryUserId ?? 0, 209 userId: queryUserId ?? 0,
@@ -212,6 +235,11 @@ class LocalHealthDataSource implements HealthDataSource { @@ -212,6 +235,11 @@ class LocalHealthDataSource implements HealthDataSource {
212 startTime: dayStart, 235 startTime: dayStart,
213 endTime: dayEnd, 236 endTime: dayEnd,
214 ); 237 );
  238 + final resolvedActivity = await _resolveOhosActivityGoalsIfNeeded(
  239 + activity: activity,
  240 + userId: queryUserId ?? 0,
  241 + fallbackUpdateTime: dayEnd,
  242 + );
215 final sleepResults = await coreService.querySleepResults( 243 final sleepResults = await coreService.querySleepResults(
216 startTime: dayStart, 244 startTime: dayStart,
217 endTime: dayEnd, 245 endTime: dayEnd,
@@ -242,7 +270,7 @@ class LocalHealthDataSource implements HealthDataSource { @@ -242,7 +270,7 @@ class LocalHealthDataSource implements HealthDataSource {
242 LocalHealthDataConvert.v2HealthDataFromSleepResult( 270 LocalHealthDataConvert.v2HealthDataFromSleepResult(
243 day: day, 271 day: day,
244 hrvPoints: hrvPoints, 272 hrvPoints: hrvPoints,
245 - activity: activity, 273 + activity: resolvedActivity,
246 sleepingHeartRate: sleepingHeartRate, 274 sleepingHeartRate: sleepingHeartRate,
247 restingHeartRate: restingHeartRate, 275 restingHeartRate: restingHeartRate,
248 sleepResult: sleepResult, 276 sleepResult: sleepResult,
@@ -392,4 +420,76 @@ class LocalHealthDataSource implements HealthDataSource { @@ -392,4 +420,76 @@ class LocalHealthDataSource implements HealthDataSource {
392 } 420 }
393 421
394 bool _isPositive(num? value) => value != null && value > 0; 422 bool _isPositive(num? value) => value != null && value > 0;
  423 +
  424 + bool get _isOhosPlatform => defaultTargetPlatform.name == 'ohos';
  425 +
  426 + Future<List<HealthKitRawActivityDataPoint>>
  427 + _resolveOhosActivityGoalsIfNeeded({
  428 + required List<HealthKitRawActivityDataPoint> activity,
  429 + required int userId,
  430 + required int fallbackUpdateTime,
  431 + }) async {
  432 + if (!_isOhosPlatform) return activity;
  433 + final target = _ohosActivityTarget(
  434 + userId: userId,
  435 + remoteOrCachedTarget: await coreService.queryOhosActivityGoal(),
  436 + activity: _resolveActivityTarget(activity),
  437 + fallbackUpdateTime: fallbackUpdateTime,
  438 + );
  439 + if (activity.isEmpty) {
  440 + return [
  441 + _activityPointWithTarget(
  442 + HealthKitRawActivityDataPoint(endTime: fallbackUpdateTime),
  443 + target,
  444 + ),
  445 + ];
  446 + }
  447 + return activity
  448 + .map((item) => _activityPointWithTarget(item, target))
  449 + .toList(growable: false);
  450 + }
  451 +
  452 + HealthKitRawActivityDataPoint _activityPointWithTarget(
  453 + HealthKitRawActivityDataPoint item,
  454 + V2ActivityTarget target,
  455 + ) {
  456 + return HealthKitRawActivityDataPoint(
  457 + endTime: item.endTime,
  458 + activityMoveMode: item.activityMoveMode,
  459 + activeEnergyBurned: item.activeEnergyBurned,
  460 + activeEnergyBurnedGoal: target.move?.toDouble(),
  461 + appleMoveTime: item.appleMoveTime,
  462 + appleMoveTimeGoal: item.appleMoveTimeGoal,
  463 + appleExerciseTime: item.appleExerciseTime,
  464 + exerciseTimeGoal: target.exercise?.toDouble(),
  465 + appleStandHours: item.appleStandHours,
  466 + standHoursGoal: target.stand?.toDouble(),
  467 + );
  468 + }
  469 +
  470 + V2ActivityTarget _ohosActivityTarget({
  471 + required int userId,
  472 + required V2ActivityTarget? remoteOrCachedTarget,
  473 + required HealthKitRawActivityDataPoint? activity,
  474 + required int fallbackUpdateTime,
  475 + }) {
  476 + return V2ActivityTarget(
  477 + userId: userId,
  478 + move: remoteOrCachedTarget?.move ??
  479 + activity?.activeEnergyBurnedGoal?.ceil() ??
  480 + _ohosDefaultMoveGoal,
  481 + step: remoteOrCachedTarget?.step ?? _ohosDefaultStepGoal,
  482 + stand: remoteOrCachedTarget?.stand ??
  483 + activity?.standHoursGoal?.round() ??
  484 + _ohosDefaultStandGoal,
  485 + exercise: remoteOrCachedTarget?.exercise ??
  486 + activity?.exerciseTimeGoal?.round() ??
  487 + _ohosDefaultExerciseGoalSeconds,
  488 + updateTime: remoteOrCachedTarget?.updateTime ??
  489 + activity?.endTime ??
  490 + fallbackUpdateTime,
  491 + sleepTargetDuration: remoteOrCachedTarget?.sleepTargetDuration ??
  492 + _ohosDefaultSleepTargetSeconds,
  493 + );
  494 + }
395 } 495 }
@@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/ @@ -6,6 +6,7 @@ import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/
6 import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart'; 6 import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart';
7 import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart'; 7 import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
8 import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart'; 8 import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart';
  9 +import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
9 import 'package:flutter_test/flutter_test.dart'; 10 import 'package:flutter_test/flutter_test.dart';
10 11
11 void main() { 12 void main() {
@@ -438,6 +439,29 @@ void main() { @@ -438,6 +439,29 @@ void main() {
438 expect(page.items.single.payload['end_time'], 1787870000); 439 expect(page.items.single.payload['end_time'], 1787870000);
439 expect(page.items.single.payload['activity_type'], 90); 440 expect(page.items.single.payload['activity_type'], 90);
440 }); 441 });
  442 +
  443 + test('getActivityGoal fetches remote goal and stores local cache', () async {
  444 + const goal = V2ActivityTarget(
  445 + move: 400,
  446 + step: 9000,
  447 + exercise: 1800,
  448 + stand: 12,
  449 + );
  450 + final remote = _FakeOhosHealthRawDataRemoteDataSource(
  451 + const <OhosHealthRawDataPage>[],
  452 + activityGoal: goal,
  453 + );
  454 + final local = _FakeOhosHealthRawDataLocalStore();
  455 + final service = OhosHealthRawDataSyncService(
  456 + remoteDataSource: remote,
  457 + localStore: local,
  458 + );
  459 +
  460 + final result = await service.getActivityGoal();
  461 +
  462 + expect(result, same(goal));
  463 + expect(local.activityGoal, same(goal));
  464 + });
441 } 465 }
442 466
443 class _FakeOhosHealthRawDataRemoteDataSource 467 class _FakeOhosHealthRawDataRemoteDataSource
@@ -446,12 +470,15 @@ class _FakeOhosHealthRawDataRemoteDataSource @@ -446,12 +470,15 @@ class _FakeOhosHealthRawDataRemoteDataSource
446 this._pages, { 470 this._pages, {
447 Completer<void>? gate, 471 Completer<void>? gate,
448 Set<int> failingDataTypes = const <int>{}, 472 Set<int> failingDataTypes = const <int>{},
  473 + V2ActivityTarget? activityGoal,
449 }) : _gate = gate, 474 }) : _gate = gate,
450 - _failingDataTypes = failingDataTypes; 475 + _failingDataTypes = failingDataTypes,
  476 + _activityGoal = activityGoal;
451 477
452 final List<OhosHealthRawDataPage> _pages; 478 final List<OhosHealthRawDataPage> _pages;
453 final Completer<void>? _gate; 479 final Completer<void>? _gate;
454 final Set<int> _failingDataTypes; 480 final Set<int> _failingDataTypes;
  481 + final V2ActivityTarget? _activityGoal;
455 final List<_FetchCall> calls = <_FetchCall>[]; 482 final List<_FetchCall> calls = <_FetchCall>[];
456 483
457 @override 484 @override
@@ -481,6 +508,11 @@ class _FakeOhosHealthRawDataRemoteDataSource @@ -481,6 +508,11 @@ class _FakeOhosHealthRawDataRemoteDataSource
481 } 508 }
482 return _pages[pageIndex]; 509 return _pages[pageIndex];
483 } 510 }
  511 +
  512 + @override
  513 + Future<V2ActivityTarget?> fetchActivityGoal() async {
  514 + return _activityGoal;
  515 + }
484 } 516 }
485 517
486 class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { 518 class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
@@ -490,6 +522,7 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { @@ -490,6 +522,7 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
490 final int? _latestDataTime; 522 final int? _latestDataTime;
491 final List<List<OhosHealthRawDataItem>> storedBatches = 523 final List<List<OhosHealthRawDataItem>> storedBatches =
492 <List<OhosHealthRawDataItem>>[]; 524 <List<OhosHealthRawDataItem>>[];
  525 + V2ActivityTarget? activityGoal;
493 526
494 @override 527 @override
495 Future<int?> latestDataTime({required int dataType}) async { 528 Future<int?> latestDataTime({required int dataType}) async {
@@ -521,6 +554,16 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { @@ -521,6 +554,16 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
521 ) 554 )
522 .toList(growable: false); 555 .toList(growable: false);
523 } 556 }
  557 +
  558 + @override
  559 + Future<void> upsertActivityGoal(V2ActivityTarget goal) async {
  560 + activityGoal = goal;
  561 + }
  562 +
  563 + @override
  564 + Future<V2ActivityTarget?> queryActivityGoal() async {
  565 + return activityGoal;
  566 + }
524 } 567 }
525 568
526 class _FetchCall { 569 class _FetchCall {
@@ -542,13 +585,16 @@ class _FakeOhosHarmonyRawDataClient implements OhosHarmonyRawDataClient { @@ -542,13 +585,16 @@ class _FakeOhosHarmonyRawDataClient implements OhosHarmonyRawDataClient {
542 HmHealthData? healthData, 585 HmHealthData? healthData,
543 HmSleepData? sleepData, 586 HmSleepData? sleepData,
544 HmWorkoutData? workoutData, 587 HmWorkoutData? workoutData,
  588 + V2ActivityTarget? activityGoal,
545 }) : _healthData = healthData ?? HmHealthData(), 589 }) : _healthData = healthData ?? HmHealthData(),
546 _sleepData = sleepData ?? HmSleepData(), 590 _sleepData = sleepData ?? HmSleepData(),
547 - _workoutData = workoutData ?? HmWorkoutData(); 591 + _workoutData = workoutData ?? HmWorkoutData(),
  592 + _activityGoal = activityGoal ?? const V2ActivityTarget();
548 593
549 final HmHealthData _healthData; 594 final HmHealthData _healthData;
550 final HmSleepData _sleepData; 595 final HmSleepData _sleepData;
551 final HmWorkoutData _workoutData; 596 final HmWorkoutData _workoutData;
  597 + final V2ActivityTarget _activityGoal;
552 final List<_HealthCall> healthCalls = <_HealthCall>[]; 598 final List<_HealthCall> healthCalls = <_HealthCall>[];
553 final List<_SleepCall> sleepCalls = <_SleepCall>[]; 599 final List<_SleepCall> sleepCalls = <_SleepCall>[];
554 final List<_SleepCall> workoutCalls = <_SleepCall>[]; 600 final List<_SleepCall> workoutCalls = <_SleepCall>[];
@@ -596,6 +642,11 @@ class _FakeOhosHarmonyRawDataClient implements OhosHarmonyRawDataClient { @@ -596,6 +642,11 @@ class _FakeOhosHarmonyRawDataClient implements OhosHarmonyRawDataClient {
596 ); 642 );
597 return AppSuccess(_workoutData); 643 return AppSuccess(_workoutData);
598 } 644 }
  645 +
  646 + @override
  647 + Future<AppResult<V2ActivityTarget>> getActivityGoal() async {
  648 + return AppSuccess(_activityGoal);
  649 + }
599 } 650 }
600 651
601 class _HealthCall { 652 class _HealthCall {