Commit b2e6377b163a9134e17effc6474fbd53ca183b63

Authored by 权海
1 parent c17ab416

feat(ui):数据获取兼容ohos

Showing 31 changed files with 849 additions and 563 deletions
1 import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; 1 import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
2 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 2 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
3 import 'package:get/get.dart'; 3 import 'package:get/get.dart';
4 4
5 import '../../core/config/app_environment_config.dart'; 5 import '../../core/config/app_environment_config.dart';
@@ -19,7 +19,6 @@ import '../../core/services/obs_upload_service.dart'; @@ -19,7 +19,6 @@ import '../../core/services/obs_upload_service.dart';
19 import '../../core/services/push_service.dart'; 19 import '../../core/services/push_service.dart';
20 import '../../core/services/thinking_data_service.dart'; 20 import '../../core/services/thinking_data_service.dart';
21 import '../../core/services/user_state_service.dart'; 21 import '../../core/services/user_state_service.dart';
22 -import '../../core/services/wear_engine_service.dart';  
23 import '../../data/local/local_storage.dart'; 22 import '../../data/local/local_storage.dart';
24 import '../../data/local/user_account_storage.dart'; 23 import '../../data/local/user_account_storage.dart';
25 import '../../data/local/user_preferences_storage.dart'; 24 import '../../data/local/user_preferences_storage.dart';
@@ -107,14 +106,6 @@ void registerInteractionDeps( @@ -107,14 +106,6 @@ void registerInteractionDeps(
107 UserPreferencesStorage userPrefs, 106 UserPreferencesStorage userPrefs,
108 ) { 107 ) {
109 Get.lazyPut(() => InteractionApi(dioClient), fenix: true); 108 Get.lazyPut(() => InteractionApi(dioClient), fenix: true);
110 - Get.lazyPut(  
111 - () => WearEngineService(  
112 - userPrefs,  
113 - Get.find<HealthApi>(),  
114 - Get.find<InteractionApi>(),  
115 - ),  
116 - fenix: true,  
117 - );  
118 } 109 }
119 110
120 /// Object storage upload API and service. 111 /// Object storage upload API and service.
1 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 1 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
2 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 2 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
3 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 3 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
4 import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart'; 4 import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart';
5 import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; 5 import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
1 import 'package:get/get.dart'; 1 import 'package:get/get.dart';
2 2
3 -import '../../../../core/services/health_raw_data_core_service.dart'; 3 +import '../../../../core/services/raw_data_service/ios/health_raw_data_core_service.dart';
4 import '../../../../data/local/user_preferences_storage.dart'; 4 import '../../../../data/local/user_preferences_storage.dart';
5 import '../controllers/developer_options_controller.dart'; 5 import '../controllers/developer_options_controller.dart';
6 6
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; @@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
2 import 'package:get/get.dart'; 2 import 'package:get/get.dart';
3 3
4 import '../../../../core/logging/app_logger.dart'; 4 import '../../../../core/logging/app_logger.dart';
5 -import '../../../../core/services/health_raw_data_core_service.dart'; 5 +import '../../../../core/services/raw_data_service/ios/health_raw_data_core_service.dart';
6 import '../../../../data/local/user_preferences_storage.dart'; 6 import '../../../../data/local/user_preferences_storage.dart';
7 import '../../../../pigeon/platform_api.g.dart'; 7 import '../../../../pigeon/platform_api.g.dart';
8 import '../../../routes/app_pages.dart'; 8 import '../../../routes/app_pages.dart';
@@ -3,7 +3,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; @@ -3,7 +3,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
3 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 3 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
4 import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; 4 import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
5 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 5 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
6 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 6 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
7 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 7 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
8 import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; 8 import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
9 import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart'; 9 import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
@@ -5,7 +5,7 @@ import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; @@ -5,7 +5,7 @@ import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
5 import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; 5 import 'package:doublefeel_flutter/core/network/api/theme_api.dart';
6 import 'package:doublefeel_flutter/core/network/api/user_api.dart'; 6 import 'package:doublefeel_flutter/core/network/api/user_api.dart';
7 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 7 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
8 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 8 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
9 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 9 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
10 import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; 10 import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
11 import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart'; 11 import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
@@ -4,7 +4,7 @@ import 'package:doublefeel_flutter/core/constants/flutter_bridge_method_name.dar @@ -4,7 +4,7 @@ import 'package:doublefeel_flutter/core/constants/flutter_bridge_method_name.dar
4 import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; 4 import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
5 import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; 5 import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
6 import 'package:doublefeel_flutter/core/result/app_result.dart'; 6 import 'package:doublefeel_flutter/core/result/app_result.dart';
7 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 7 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
8 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; 8 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
9 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 9 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
10 import 'package:doublefeel_flutter/data/local/local_storage.dart'; 10 import 'package:doublefeel_flutter/data/local/local_storage.dart';
@@ -14,7 +14,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; @@ -14,7 +14,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
14 import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; 14 import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
15 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 15 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
16 import 'package:doublefeel_flutter/core/result/app_result.dart'; 16 import 'package:doublefeel_flutter/core/result/app_result.dart';
17 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 17 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
18 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; 18 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
19 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 19 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
20 import 'package:doublefeel_flutter/core/util/app_toast.dart'; 20 import 'package:doublefeel_flutter/core/util/app_toast.dart';
1 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 1 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
2 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 2 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
3 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 3 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
4 import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart'; 4 import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart';
5 import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; 5 import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
1 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 1 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
2 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 2 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
3 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 3 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
4 import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart'; 4 import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart';
5 import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; 5 import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
1 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 1 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
2 import 'package:flutter/scheduler.dart'; 2 import 'package:flutter/scheduler.dart';
3 import 'package:get/get.dart'; 3 import 'package:get/get.dart';
4 4
@@ -9,7 +9,6 @@ import '../../../../core/result/app_result.dart'; @@ -9,7 +9,6 @@ import '../../../../core/result/app_result.dart';
9 import '../../../../core/services/user_state_service.dart'; 9 import '../../../../core/services/user_state_service.dart';
10 import '../../../../data/local/user_preferences_storage.dart'; 10 import '../../../../data/local/user_preferences_storage.dart';
11 import '../../../../data/models/local/user_preferences.dart'; 11 import '../../../../data/models/local/user_preferences.dart';
12 -import '../../../../data/models/user/user_models.dart';  
13 12
14 /// Cold-start routing: restore session or open login. 13 /// Cold-start routing: restore session or open login.
15 class SplashController extends GetxController { 14 class SplashController extends GetxController {
1 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 1 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
2 import 'package:doublefeel_flutter/core/network/api/user_api.dart'; 2 import 'package:doublefeel_flutter/core/network/api/user_api.dart';
3 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 3 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
4 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; 4 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
5 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 5 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
6 import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; 6 import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
@@ -3,9 +3,10 @@ import 'dart:io'; @@ -3,9 +3,10 @@ import 'dart:io';
3 import 'package:path_provider/path_provider.dart'; 3 import 'package:path_provider/path_provider.dart';
4 import 'package:sqflite/sqflite.dart'; 4 import 'package:sqflite/sqflite.dart';
5 5
6 -import '../../data/models/enums/app_enums.dart';  
7 -import '../../pigeon/health_kit_raw_data_api.g.dart';  
8 -import 'health_raw_data_core_service.dart'; 6 +import '../../../data/models/enums/app_enums.dart';
  7 +import '../../../pigeon/health_kit_raw_data_api.g.dart';
  8 +import 'ios/health_raw_data_core_service.dart';
  9 +import 'health_raw_models.dart';
9 import 'health_raw_stress_calculator.dart'; 10 import 'health_raw_stress_calculator.dart';
10 11
11 typedef FriendRawDataPageFetcher = Future<List<HealthKitRawDataPoint>> 12 typedef FriendRawDataPageFetcher = Future<List<HealthKitRawDataPoint>>
  1 +import 'package:flutter/foundation.dart';
  2 +
  3 +import '../../../pigeon/health_kit_raw_data_api.g.dart';
  4 +import 'ohos/ohos_health_raw_data_sync_service.dart';
  5 +
  6 +abstract class HealthRawDataSource {
  7 + Future<bool> hasHealthData();
  8 +
  9 + Future<List<HealthKitRawDataPoint>> getRawData(
  10 + int dataType,
  11 + int startTime,
  12 + int endTime,
  13 + );
  14 +
  15 + Future<List<HealthKitRawSleepDataPoint>> getRawSleepData(
  16 + int startTime,
  17 + int endTime,
  18 + );
  19 +
  20 + Future<List<HealthKitRawActivityDataPoint>> getRawActivityData(
  21 + int startTime,
  22 + int endTime,
  23 + );
  24 +
  25 + Future<List<HealthKitRawWorkoutDataPoint>> getRawWorkoutData(
  26 + int startTime,
  27 + int endTime,
  28 + );
  29 +}
  30 +
  31 +class PlatformHealthRawDataSource implements HealthRawDataSource {
  32 + PlatformHealthRawDataSource({
  33 + HealthRawDataSource? iosDataSource,
  34 + HealthRawDataSource? ohosDataSource,
  35 + TargetPlatform? targetPlatform,
  36 + }) : _iosDataSource = iosDataSource ?? AppleHealthRawDataSource(),
  37 + _ohosDataSource = ohosDataSource ?? OhosHealthRawDataSource(),
  38 + _targetPlatform = targetPlatform;
  39 +
  40 + final HealthRawDataSource _iosDataSource;
  41 + final HealthRawDataSource _ohosDataSource;
  42 + final TargetPlatform? _targetPlatform;
  43 +
  44 + HealthRawDataSource get _currentDataSource {
  45 + final platform = _targetPlatform ?? defaultTargetPlatform;
  46 + return platform == TargetPlatform.iOS ? _iosDataSource : _ohosDataSource;
  47 + }
  48 +
  49 + @override
  50 + Future<bool> hasHealthData() {
  51 + return _currentDataSource.hasHealthData();
  52 + }
  53 +
  54 + @override
  55 + Future<List<HealthKitRawDataPoint>> getRawData(
  56 + int dataType,
  57 + int startTime,
  58 + int endTime,
  59 + ) {
  60 + return _currentDataSource.getRawData(dataType, startTime, endTime);
  61 + }
  62 +
  63 + @override
  64 + Future<List<HealthKitRawSleepDataPoint>> getRawSleepData(
  65 + int startTime,
  66 + int endTime,
  67 + ) {
  68 + return _currentDataSource.getRawSleepData(startTime, endTime);
  69 + }
  70 +
  71 + @override
  72 + Future<List<HealthKitRawActivityDataPoint>> getRawActivityData(
  73 + int startTime,
  74 + int endTime,
  75 + ) {
  76 + return _currentDataSource.getRawActivityData(startTime, endTime);
  77 + }
  78 +
  79 + @override
  80 + Future<List<HealthKitRawWorkoutDataPoint>> getRawWorkoutData(
  81 + int startTime,
  82 + int endTime,
  83 + ) {
  84 + return _currentDataSource.getRawWorkoutData(startTime, endTime);
  85 + }
  86 +}
  87 +
  88 +// IOS
  89 +class AppleHealthRawDataSource implements HealthRawDataSource {
  90 + AppleHealthRawDataSource({
  91 + HealthKitRawDataHostApi? rawDataApi,
  92 + }) : _rawDataApi = rawDataApi ?? HealthKitRawDataHostApi();
  93 +
  94 + final HealthKitRawDataHostApi _rawDataApi;
  95 +
  96 + @override
  97 + Future<bool> hasHealthData() {
  98 + return _rawDataApi.hasHealthData();
  99 + }
  100 +
  101 + @override
  102 + Future<List<HealthKitRawDataPoint>> getRawData(
  103 + int dataType,
  104 + int startTime,
  105 + int endTime,
  106 + ) {
  107 + return _rawDataApi.getHealthKitRawData(dataType, startTime, endTime);
  108 + }
  109 +
  110 + @override
  111 + Future<List<HealthKitRawSleepDataPoint>> getRawSleepData(
  112 + int startTime,
  113 + int endTime,
  114 + ) {
  115 + return _rawDataApi.getHealthKitRawSleepData(startTime, endTime);
  116 + }
  117 +
  118 + @override
  119 + Future<List<HealthKitRawActivityDataPoint>> getRawActivityData(
  120 + int startTime,
  121 + int endTime,
  122 + ) {
  123 + return _rawDataApi.getHealthKitRawActivityData(startTime, endTime);
  124 + }
  125 +
  126 + @override
  127 + Future<List<HealthKitRawWorkoutDataPoint>> getRawWorkoutData(
  128 + int startTime,
  129 + int endTime,
  130 + ) {
  131 + return _rawDataApi.getHealthKitRawWorkoutData(startTime, endTime);
  132 + }
  133 +}
  134 +
  135 +// ohos
  136 +class OhosHealthRawDataSource implements HealthRawDataSource {
  137 + OhosHealthRawDataSource({
  138 + OhosHealthRawDataSyncService? syncService,
  139 + }) : _syncService = syncService ?? OhosHealthRawDataSyncService();
  140 +
  141 + final OhosHealthRawDataSyncService _syncService;
  142 +
  143 + @override
  144 + Future<bool> hasHealthData() async {
  145 + // TODO: Query the OHOS local raw-data database when its schema is ready.
  146 + return false;
  147 + }
  148 +
  149 + @override
  150 + Future<List<HealthKitRawDataPoint>> getRawData(
  151 + int dataType,
  152 + int startTime,
  153 + int endTime,
  154 + ) async {
  155 + await _syncService.syncRawData(
  156 + dataType: dataType,
  157 + startTime: startTime,
  158 + endTime: endTime,
  159 + );
  160 + // TODO: Read synced OHOS raw points from local database.
  161 + return const <HealthKitRawDataPoint>[];
  162 + }
  163 +
  164 + @override
  165 + Future<List<HealthKitRawSleepDataPoint>> getRawSleepData(
  166 + int startTime,
  167 + int endTime,
  168 + ) async {
  169 + await _syncService.syncRawData(
  170 + dataType: OhosHealthRawDataType.sleepAnalysis,
  171 + startTime: startTime,
  172 + endTime: endTime,
  173 + );
  174 + // TODO: Read synced OHOS sleep groups from local database.
  175 + return const <HealthKitRawSleepDataPoint>[];
  176 + }
  177 +
  178 + @override
  179 + Future<List<HealthKitRawActivityDataPoint>> getRawActivityData(
  180 + int startTime,
  181 + int endTime,
  182 + ) async {
  183 + await _syncService.syncRawData(
  184 + dataType: OhosHealthRawDataType.activitySummary,
  185 + startTime: startTime,
  186 + endTime: endTime,
  187 + );
  188 + // TODO: Read synced OHOS activity summaries from local database.
  189 + return const <HealthKitRawActivityDataPoint>[];
  190 + }
  191 +
  192 + @override
  193 + Future<List<HealthKitRawWorkoutDataPoint>> getRawWorkoutData(
  194 + int startTime,
  195 + int endTime,
  196 + ) async {
  197 + await _syncService.syncRawData(
  198 + dataType: OhosHealthRawDataType.workout,
  199 + startTime: startTime,
  200 + endTime: endTime,
  201 + );
  202 + // TODO: Read synced OHOS workout intervals from local database.
  203 + return const <HealthKitRawWorkoutDataPoint>[];
  204 + }
  205 +}
  1 +import 'dart:math' as math;
  2 +
  3 +import '../../../pigeon/health_kit_raw_data_api.g.dart';
  4 +import 'health_sleep_calculator.dart';
  5 +
  6 +class HealthRawStressCalculationResult {
  7 + const HealthRawStressCalculationResult({
  8 + required this.userId,
  9 + required this.hrvStressPoints,
  10 + required this.realtimeStressPoints,
  11 + required this.dailyStressPoints,
  12 + this.sleepResults = const <HealthRawSleepResult>[],
  13 + });
  14 +
  15 + final int userId;
  16 + final List<HealthRawHrvStressPoint> hrvStressPoints;
  17 + final List<HealthRawRealtimeStressPoint> realtimeStressPoints;
  18 + final List<HealthRawDailyStressPoint> dailyStressPoints;
  19 + final List<HealthRawSleepResult> sleepResults;
  20 +
  21 + HealthRawStressCalculationResult copyWith({
  22 + List<HealthRawHrvStressPoint>? hrvStressPoints,
  23 + List<HealthRawRealtimeStressPoint>? realtimeStressPoints,
  24 + List<HealthRawDailyStressPoint>? dailyStressPoints,
  25 + List<HealthRawSleepResult>? sleepResults,
  26 + }) {
  27 + return HealthRawStressCalculationResult(
  28 + userId: userId,
  29 + hrvStressPoints: hrvStressPoints ?? this.hrvStressPoints,
  30 + realtimeStressPoints: realtimeStressPoints ?? this.realtimeStressPoints,
  31 + dailyStressPoints: dailyStressPoints ?? this.dailyStressPoints,
  32 + sleepResults: sleepResults ?? this.sleepResults,
  33 + );
  34 + }
  35 +}
  36 +
  37 +class HealthRawDataUpdatedEvent {
  38 + const HealthRawDataUpdatedEvent({required this.dataTypes});
  39 +
  40 + final List<int> dataTypes;
  41 +}
  42 +
  43 +class HealthRawMarkedDataSet {
  44 + const HealthRawMarkedDataSet({
  45 + required this.hrv,
  46 + required this.heartRate,
  47 + required this.restingHeartRate,
  48 + });
  49 +
  50 + final List<HealthRawMarkedDataPoint> hrv;
  51 + final List<HealthRawMarkedDataPoint> heartRate;
  52 + final List<HealthRawMarkedDataPoint> restingHeartRate;
  53 +}
  54 +
  55 +class HealthRawMarkedDataPoint {
  56 + HealthRawMarkedDataPoint({
  57 + required this.raw,
  58 + required this.flags,
  59 + });
  60 +
  61 + final HealthKitRawDataPoint raw;
  62 + HealthRawPointFlags flags;
  63 +
  64 + int get startTime => raw.startTime;
  65 + int get endTime => raw.endTime;
  66 + double? get value => raw.value;
  67 +}
  68 +
  69 +enum HealthRawPointContext {
  70 + workout,
  71 + workoutRecovery,
  72 + sleep,
  73 + suspectedActivity,
  74 + awake,
  75 +}
  76 +
  77 +class HealthRawPointFlags {
  78 + const HealthRawPointFlags({
  79 + required this.isWorkout,
  80 + required this.isWorkoutRecovery,
  81 + required this.isSleepLikely,
  82 + required this.isSuspectedActivity,
  83 + });
  84 +
  85 + const HealthRawPointFlags.none()
  86 + : isWorkout = false,
  87 + isWorkoutRecovery = false,
  88 + isSleepLikely = false,
  89 + isSuspectedActivity = false;
  90 +
  91 + final bool isWorkout;
  92 + final bool isWorkoutRecovery;
  93 + final bool isSleepLikely;
  94 + final bool isSuspectedActivity;
  95 +
  96 + HealthRawPointContext get context {
  97 + if (isWorkout) return HealthRawPointContext.workout;
  98 + if (isWorkoutRecovery) return HealthRawPointContext.workoutRecovery;
  99 + if (isSleepLikely) return HealthRawPointContext.sleep;
  100 + if (isSuspectedActivity) return HealthRawPointContext.suspectedActivity;
  101 + return HealthRawPointContext.awake;
  102 + }
  103 +
  104 + bool get isAwakeBaselineEligible =>
  105 + !isSleepLikely && !isWorkout && !isWorkoutRecovery;
  106 +
  107 + bool get isSleepHrvBaselineEligible =>
  108 + isSleepLikely && !isWorkout && !isWorkoutRecovery;
  109 +
  110 + bool get isAwakeHrBaselineEligible =>
  111 + !isSleepLikely && !isWorkout && !isWorkoutRecovery;
  112 +
  113 + bool get isSleepHrBaselineEligible =>
  114 + isSleepLikely && !isWorkout && !isWorkoutRecovery;
  115 +
  116 + bool get isCurrentAwakeHrEligible => isAwakeHrBaselineEligible;
  117 +
  118 + bool matchesContext(HealthRawPointContext pointContext) {
  119 + return context == pointContext;
  120 + }
  121 +
  122 + HealthRawPointFlags copyWith({
  123 + bool? isWorkout,
  124 + bool? isWorkoutRecovery,
  125 + bool? isSleepLikely,
  126 + bool? isSuspectedActivity,
  127 + }) {
  128 + return HealthRawPointFlags(
  129 + isWorkout: isWorkout ?? this.isWorkout,
  130 + isWorkoutRecovery: isWorkoutRecovery ?? this.isWorkoutRecovery,
  131 + isSleepLikely: isSleepLikely ?? this.isSleepLikely,
  132 + isSuspectedActivity: isSuspectedActivity ?? this.isSuspectedActivity,
  133 + );
  134 + }
  135 +}
  136 +
  137 +enum HealthRawStressState {
  138 + overload(1),
  139 + attention(2),
  140 + normal(3),
  141 + excellent(4);
  142 +
  143 + const HealthRawStressState(this.value);
  144 +
  145 + final int value;
  146 +}
  147 +
  148 +class HealthRawHrvStressPoint {
  149 + const HealthRawHrvStressPoint({
  150 + required this.userId,
  151 + required this.rawEndTime,
  152 + required this.rawHrv,
  153 + required this.result,
  154 + required this.sourceStartTime,
  155 + required this.sourceEndTime,
  156 + required this.state,
  157 + required this.baselineHrv,
  158 + required this.baselineAwakeHrv,
  159 + required this.baselineSleepHrv,
  160 + required this.baselineRestingHr,
  161 + this.flags = const HealthRawPointFlags.none(),
  162 + this.uploaded = false,
  163 + });
  164 +
  165 + final int userId;
  166 + final int rawEndTime;
  167 + final double rawHrv;
  168 + final double result;
  169 + final int sourceStartTime;
  170 + final int sourceEndTime;
  171 + final HealthRawStressState state;
  172 + final double baselineHrv;
  173 + final double baselineAwakeHrv;
  174 + final double? baselineSleepHrv;
  175 + final double baselineRestingHr;
  176 + final HealthRawPointFlags flags;
  177 + final bool uploaded;
  178 +
  179 + factory HealthRawHrvStressPoint.fromDb(Map<String, Object?> row) {
  180 + final result = (row['result'] as num).toDouble();
  181 + final rawHrv = _positiveDouble(row['raw_hrv']) ?? result;
  182 + final baselineHrv = _positiveDouble(row['baseline_hrv']) ?? result;
  183 + final baselineAwakeHrv =
  184 + _positiveDouble(row['baseline_awake_hrv']) ?? baselineHrv;
  185 + return HealthRawHrvStressPoint(
  186 + userId: row['user_id'] as int,
  187 + rawEndTime: row['raw_end_time'] as int,
  188 + rawHrv: rawHrv,
  189 + result: result,
  190 + sourceStartTime: row['source_start_time'] as int,
  191 + sourceEndTime: row['source_end_time'] as int,
  192 + state: _stressStateFromDb(row['state']) ?? HealthRawStressState.normal,
  193 + baselineHrv: baselineHrv,
  194 + baselineAwakeHrv: baselineAwakeHrv,
  195 + baselineSleepHrv: _positiveDouble(row['baseline_sleep_hrv']),
  196 + baselineRestingHr: _positiveDouble(row['baseline_resting_hr']) ?? 65,
  197 + flags: _flagsFromDb(row),
  198 + uploaded: (row['uploaded'] as int) == 1,
  199 + );
  200 + }
  201 +}
  202 +
  203 +class HealthRawRealtimeStressPoint {
  204 + const HealthRawRealtimeStressPoint({
  205 + required this.userId,
  206 + required this.rawEndTime,
  207 + required this.rawHr,
  208 + required this.result,
  209 + required this.sourceStartTime,
  210 + required this.sourceEndTime,
  211 + this.flags = const HealthRawPointFlags.none(),
  212 + this.uploaded = false,
  213 + });
  214 +
  215 + final int userId;
  216 + final int rawEndTime;
  217 + final double rawHr;
  218 + final double result;
  219 + final int sourceStartTime;
  220 + final int sourceEndTime;
  221 + final HealthRawPointFlags flags;
  222 + final bool uploaded;
  223 +
  224 + HealthRawStressState get state => healthRawRealtimeStressState(result);
  225 + bool get isWorkout => flags.isWorkout;
  226 + bool get isWorkoutRecovery => flags.isWorkoutRecovery;
  227 + bool get isSleepLikely => flags.isSleepLikely;
  228 + bool get isSuspectedActivity => flags.isSuspectedActivity;
  229 +
  230 + factory HealthRawRealtimeStressPoint.fromDb(Map<String, Object?> row) {
  231 + return HealthRawRealtimeStressPoint(
  232 + userId: row['user_id'] as int,
  233 + rawEndTime: row['raw_end_time'] as int,
  234 + rawHr: (row['raw_hr'] as num?)?.toDouble() ?? 0,
  235 + result: (row['result'] as num).toDouble(),
  236 + sourceStartTime: row['source_start_time'] as int,
  237 + sourceEndTime: row['source_end_time'] as int,
  238 + flags: _flagsFromDb(row),
  239 + uploaded: (row['uploaded'] as int) == 1,
  240 + );
  241 + }
  242 +}
  243 +
  244 +class HealthRawDailyStressPoint {
  245 + const HealthRawDailyStressPoint({
  246 + required this.userId,
  247 + required this.date,
  248 + required this.stressValue,
  249 + required this.stressScore,
  250 + required this.state,
  251 + required this.dataTime,
  252 + this.uploaded = false,
  253 + });
  254 +
  255 + final int userId;
  256 + final int date;
  257 + final double stressValue;
  258 + final int stressScore;
  259 + final HealthRawStressState state;
  260 + final int dataTime;
  261 + final bool uploaded;
  262 +
  263 + factory HealthRawDailyStressPoint.fromDb(Map<String, Object?> row) {
  264 + final stressScore = row['stress_score'] as int;
  265 + return HealthRawDailyStressPoint(
  266 + userId: row['user_id'] as int,
  267 + date: row['date'] as int,
  268 + stressValue: (row['stress_value'] as num).toDouble(),
  269 + stressScore: stressScore,
  270 + state: _stressStateFromDb(row['state']) ??
  271 + healthRawRealtimeStressState(stressScore),
  272 + dataTime: row['data_time'] as int,
  273 + uploaded: (row['uploaded'] as int) == 1,
  274 + );
  275 + }
  276 +}
  277 +
  278 +class HealthRawSleepResult {
  279 + const HealthRawSleepResult({
  280 + required this.userId,
  281 + required this.date,
  282 + required this.startDate,
  283 + required this.sleepScore,
  284 + required this.sleepState,
  285 + required this.inBedMinutes,
  286 + required this.awakMinutes,
  287 + required this.sleepMinutes,
  288 + this.uploaded = false,
  289 + });
  290 +
  291 + final int userId;
  292 + final int date;
  293 + final int startDate;
  294 + final int sleepScore;
  295 + final int sleepState;
  296 + final int inBedMinutes;
  297 + final int awakMinutes;
  298 + final int sleepMinutes;
  299 + final bool uploaded;
  300 +
  301 + factory HealthRawSleepResult.fromDb(Map<String, Object?> row) {
  302 + return HealthRawSleepResult(
  303 + userId: row['user_id'] as int,
  304 + date: row['date'] as int,
  305 + startDate: row['start_date'] as int,
  306 + sleepScore: row['sleep_score'] as int,
  307 + sleepState: row['sleep_state'] as int,
  308 + inBedMinutes: row['in_bed_minutes'] as int,
  309 + awakMinutes: row['awak_minutes'] as int,
  310 + sleepMinutes: row['sleep_minutes'] as int,
  311 + uploaded: (row['uploaded'] as int? ?? 0) == 1,
  312 + );
  313 + }
  314 +
  315 + int get sleepStateValue => sleepState;
  316 +
  317 + String? get sleepStateDescription {
  318 + for (final state in HealthSleepState.values) {
  319 + if (state.value == sleepState) return state.description;
  320 + }
  321 + return null;
  322 + }
  323 +}
  324 +
  325 +class HealthRawDailyStressCalculator {
  326 + const HealthRawDailyStressCalculator._();
  327 +
  328 + static const int _confirmedActivityBufferSeconds = 12 * 60;
  329 +
  330 + static HealthRawDailyStressPoint? calculate({
  331 + required int userId,
  332 + required int date,
  333 + required List<HealthRawRealtimeStressPoint> realtimePoints,
  334 + required int dataTime,
  335 + }) {
  336 + final validValuePoints =
  337 + realtimePoints.where((e) => _isValidStressValue(e.result)).toList();
  338 + final activityBuffers = _confirmedActivityBuffers(realtimePoints);
  339 + final values = validValuePoints
  340 + .where((e) => !_isInTimeRanges(e.rawEndTime, activityBuffers))
  341 + .map((e) => e.result)
  342 + .toList()
  343 + ..sort();
  344 + if (values.isEmpty) return null;
  345 + final stressValue = _integerDouble(_average(values));
  346 + final stressScore = _dailyStressScore(values).round().clamp(1, 100).toInt();
  347 + return HealthRawDailyStressPoint(
  348 + userId: userId,
  349 + date: date,
  350 + stressValue: stressValue,
  351 + stressScore: stressScore,
  352 + state: healthRawRealtimeStressState(stressScore),
  353 + dataTime: dataTime,
  354 + );
  355 + }
  356 +
  357 + static double _dailyStressScore(List<double> sortedValues) {
  358 + final median = _median(sortedValues);
  359 + final p75 = _percentile75(sortedValues);
  360 + return _round(_clamp(median * 0.60 + p75 * 0.40, 1, 100));
  361 + }
  362 +
  363 + static List<({int start, int end})> _confirmedActivityBuffers(
  364 + List<HealthRawRealtimeStressPoint> realtimePoints,
  365 + ) {
  366 + final ranges = realtimePoints
  367 + .where((e) =>
  368 + _isValidStressValue(e.result) &&
  369 + (e.isWorkout || e.isWorkoutRecovery))
  370 + .map((e) => (
  371 + start: e.rawEndTime - _confirmedActivityBufferSeconds,
  372 + end: e.rawEndTime + _confirmedActivityBufferSeconds,
  373 + ))
  374 + .toList()
  375 + ..sort((a, b) => a.start.compareTo(b.start));
  376 + if (ranges.isEmpty) return const <({int start, int end})>[];
  377 +
  378 + final merged = <({int start, int end})>[];
  379 + var current = ranges.first;
  380 + for (final range in ranges.skip(1)) {
  381 + if (range.start <= current.end) {
  382 + current = (
  383 + start: current.start,
  384 + end: math.max(current.end, range.end),
  385 + );
  386 + } else {
  387 + merged.add(current);
  388 + current = range;
  389 + }
  390 + }
  391 + merged.add(current);
  392 + return merged;
  393 + }
  394 +
  395 + static bool _isInTimeRanges(int time, List<({int start, int end})> ranges) {
  396 + return ranges.any((e) => time >= e.start && time <= e.end);
  397 + }
  398 +
  399 + static bool _isValidStressValue(num value) => value >= 1 && value <= 100;
  400 +
  401 + static double _average(List<double> values) =>
  402 + values.reduce((a, b) => a + b) / values.length;
  403 +
  404 + static double _median(List<double> sortedValues) {
  405 + final length = sortedValues.length;
  406 + final middle = length ~/ 2;
  407 + if (length.isOdd) return sortedValues[middle];
  408 + return (sortedValues[middle - 1] + sortedValues[middle]) / 2;
  409 + }
  410 +
  411 + static double _percentile75(List<double> sortedValues) {
  412 + final rank = (sortedValues.length * 0.75).ceil().clamp(
  413 + 1,
  414 + sortedValues.length,
  415 + );
  416 + return sortedValues[rank - 1];
  417 + }
  418 +
  419 + static double _clamp(num value, num min, num max) =>
  420 + math.max(min.toDouble(), math.min(max.toDouble(), value.toDouble()));
  421 +
  422 + static double _round(double value) => (value * 10).round() / 10;
  423 +}
  424 +
  425 +HealthRawStressState healthRawRealtimeStressState(num value) {
  426 + if (value >= 81) return HealthRawStressState.overload;
  427 + if (value >= 61) return HealthRawStressState.attention;
  428 + if (value >= 21) return HealthRawStressState.normal;
  429 + return HealthRawStressState.excellent;
  430 +}
  431 +
  432 +HealthRawPointFlags _flagsFromDb(Map<String, Object?> row) {
  433 + return HealthRawPointFlags(
  434 + isWorkout: (row['is_workout'] as int? ?? 0) == 1,
  435 + isWorkoutRecovery: (row['is_workout_recovery'] as int? ?? 0) == 1,
  436 + isSleepLikely: (row['is_sleep_likely'] as int? ?? 0) == 1,
  437 + isSuspectedActivity: (row['is_suspected_activity'] as int? ?? 0) == 1,
  438 + );
  439 +}
  440 +
  441 +double? _positiveDouble(Object? value) {
  442 + final result = (value as num?)?.toDouble();
  443 + if (result == null || result <= 0) return null;
  444 + return result;
  445 +}
  446 +
  447 +double _integerDouble(num value) => value.toInt().toDouble();
  448 +
  449 +HealthRawStressState? _stressStateFromDb(Object? value) {
  450 + final stateValue = value as int?;
  451 + if (stateValue == null) return null;
  452 + for (final state in HealthRawStressState.values) {
  453 + if (state.value == stateValue) return state;
  454 + }
  455 + return null;
  456 +}
1 import 'dart:math' as math; 1 import 'dart:math' as math;
2 2
3 -import '../../pigeon/health_kit_raw_data_api.g.dart';  
4 -import 'health_raw_data_core_service.dart'; 3 +import '../../../pigeon/health_kit_raw_data_api.g.dart';
  4 +import 'health_raw_models.dart';
5 5
6 class HealthRawStressCalculator { 6 class HealthRawStressCalculator {
7 const HealthRawStressCalculator({required this.userId}); 7 const HealthRawStressCalculator({required this.userId});
@@ -567,11 +567,6 @@ class HealthRawStressCalculator { @@ -567,11 +567,6 @@ class HealthRawStressCalculator {
567 return math.min(math.max(value, lower), upper).toDouble(); 567 return math.min(math.max(value, lower), upper).toDouble();
568 } 568 }
569 569
570 - static double _round(double value, int places) {  
571 - final scale = math.pow(10, places).toDouble();  
572 - return (value * scale).round() / scale;  
573 - }  
574 -  
575 static double _integerDouble(num value) => value.toInt().toDouble(); 570 static double _integerDouble(num value) => value.toInt().toDouble();
576 571
577 static List<int> _daysInRange(int startTime, int endTime) { 572 static List<int> _daysInRange(int startTime, int endTime) {
1 import 'dart:math' as math; 1 import 'dart:math' as math;
2 2
3 -import '../../pigeon/health_kit_raw_data_api.g.dart'; 3 +import '../../../pigeon/health_kit_raw_data_api.g.dart';
4 4
5 const healthSleepGoalMinutes = 8 * 60; 5 const healthSleepGoalMinutes = 8 * 60;
6 const _sleepContinuityToleranceSeconds = 1; 6 const _sleepContinuityToleranceSeconds = 1;
@@ -8,18 +8,19 @@ import 'package:flutter/foundation.dart'; @@ -8,18 +8,19 @@ import 'package:flutter/foundation.dart';
8 import 'package:path_provider/path_provider.dart'; 8 import 'package:path_provider/path_provider.dart';
9 import 'package:sqflite/sqflite.dart'; 9 import 'package:sqflite/sqflite.dart';
10 10
11 -import '../../data/models/enums/app_enums.dart';  
12 -import '../../l10n/l10n_extensions.dart';  
13 -import '../../pigeon/health_kit_api.g.dart';  
14 -import '../../pigeon/health_kit_raw_data_api.g.dart';  
15 -import '../config/app_environment_config.dart';  
16 -import '../logging/app_logger.dart';  
17 -import '../network/api/health_api.dart';  
18 -import '../result/app_result.dart'; 11 +import '../../../../data/models/enums/app_enums.dart';
  12 +import '../../../../l10n/l10n_extensions.dart';
  13 +import '../../../../pigeon/health_kit_api.g.dart';
  14 +import '../../../../pigeon/health_kit_raw_data_api.g.dart';
  15 +import '../../../config/app_environment_config.dart';
  16 +import '../../../logging/app_logger.dart';
  17 +import '../../../network/api/health_api.dart';
  18 +import '../../../result/app_result.dart';
  19 +import '../health_raw_models.dart';
19 import 'health_raw_local_notification_debug_store.dart'; 20 import 'health_raw_local_notification_debug_store.dart';
20 import 'health_raw_local_notification.dart'; 21 import 'health_raw_local_notification.dart';
21 -import 'health_raw_stress_calculator.dart';  
22 -import 'health_sleep_calculator.dart'; 22 +import '../health_raw_stress_calculator.dart';
  23 +import '../health_sleep_calculator.dart';
23 24
24 class HealthRawDataCoreService { 25 class HealthRawDataCoreService {
25 static const int defaultLookbackDays = 183; 26 static const int defaultLookbackDays = 183;
@@ -1643,460 +1644,6 @@ class HealthRawDataCoreService { @@ -1643,460 +1644,6 @@ class HealthRawDataCoreService {
1643 } 1644 }
1644 } 1645 }
1645 1646
1646 -class HealthRawStressCalculationResult {  
1647 - const HealthRawStressCalculationResult({  
1648 - required this.userId,  
1649 - required this.hrvStressPoints,  
1650 - required this.realtimeStressPoints,  
1651 - required this.dailyStressPoints,  
1652 - this.sleepResults = const <HealthRawSleepResult>[],  
1653 - });  
1654 -  
1655 - final int userId;  
1656 - final List<HealthRawHrvStressPoint> hrvStressPoints;  
1657 - final List<HealthRawRealtimeStressPoint> realtimeStressPoints;  
1658 - final List<HealthRawDailyStressPoint> dailyStressPoints;  
1659 - final List<HealthRawSleepResult> sleepResults;  
1660 -  
1661 - HealthRawStressCalculationResult copyWith({  
1662 - List<HealthRawHrvStressPoint>? hrvStressPoints,  
1663 - List<HealthRawRealtimeStressPoint>? realtimeStressPoints,  
1664 - List<HealthRawDailyStressPoint>? dailyStressPoints,  
1665 - List<HealthRawSleepResult>? sleepResults,  
1666 - }) {  
1667 - return HealthRawStressCalculationResult(  
1668 - userId: userId,  
1669 - hrvStressPoints: hrvStressPoints ?? this.hrvStressPoints,  
1670 - realtimeStressPoints: realtimeStressPoints ?? this.realtimeStressPoints,  
1671 - dailyStressPoints: dailyStressPoints ?? this.dailyStressPoints,  
1672 - sleepResults: sleepResults ?? this.sleepResults,  
1673 - );  
1674 - }  
1675 -}  
1676 -  
1677 -class HealthRawDataUpdatedEvent {  
1678 - const HealthRawDataUpdatedEvent({required this.dataTypes});  
1679 -  
1680 - final List<int> dataTypes;  
1681 -}  
1682 -  
1683 -class HealthRawMarkedDataSet {  
1684 - const HealthRawMarkedDataSet({  
1685 - required this.hrv,  
1686 - required this.heartRate,  
1687 - required this.restingHeartRate,  
1688 - });  
1689 -  
1690 - final List<HealthRawMarkedDataPoint> hrv;  
1691 - final List<HealthRawMarkedDataPoint> heartRate;  
1692 - final List<HealthRawMarkedDataPoint> restingHeartRate;  
1693 -}  
1694 -  
1695 -class HealthRawMarkedDataPoint {  
1696 - HealthRawMarkedDataPoint({  
1697 - required this.raw,  
1698 - required this.flags,  
1699 - });  
1700 -  
1701 - final HealthKitRawDataPoint raw;  
1702 - HealthRawPointFlags flags;  
1703 -  
1704 - int get startTime => raw.startTime;  
1705 - int get endTime => raw.endTime;  
1706 - double? get value => raw.value;  
1707 -}  
1708 -  
1709 -enum HealthRawPointContext {  
1710 - workout,  
1711 - workoutRecovery,  
1712 - sleep,  
1713 - suspectedActivity,  
1714 - awake,  
1715 -}  
1716 -  
1717 -class HealthRawPointFlags {  
1718 - const HealthRawPointFlags({  
1719 - required this.isWorkout,  
1720 - required this.isWorkoutRecovery,  
1721 - required this.isSleepLikely,  
1722 - required this.isSuspectedActivity,  
1723 - });  
1724 -  
1725 - const HealthRawPointFlags.none()  
1726 - : isWorkout = false,  
1727 - isWorkoutRecovery = false,  
1728 - isSleepLikely = false,  
1729 - isSuspectedActivity = false;  
1730 -  
1731 - final bool isWorkout;  
1732 - final bool isWorkoutRecovery;  
1733 - final bool isSleepLikely;  
1734 - final bool isSuspectedActivity;  
1735 -  
1736 - HealthRawPointContext get context {  
1737 - if (isWorkout) return HealthRawPointContext.workout;  
1738 - if (isWorkoutRecovery) return HealthRawPointContext.workoutRecovery;  
1739 - if (isSleepLikely) return HealthRawPointContext.sleep;  
1740 - if (isSuspectedActivity) return HealthRawPointContext.suspectedActivity;  
1741 - return HealthRawPointContext.awake;  
1742 - }  
1743 -  
1744 - bool get isAwakeBaselineEligible =>  
1745 - !isSleepLikely && !isWorkout && !isWorkoutRecovery;  
1746 -  
1747 - bool get isSleepHrvBaselineEligible =>  
1748 - isSleepLikely && !isWorkout && !isWorkoutRecovery;  
1749 -  
1750 - bool get isAwakeHrBaselineEligible =>  
1751 - !isSleepLikely && !isWorkout && !isWorkoutRecovery;  
1752 -  
1753 - bool get isSleepHrBaselineEligible =>  
1754 - isSleepLikely && !isWorkout && !isWorkoutRecovery;  
1755 -  
1756 - bool get isCurrentAwakeHrEligible => isAwakeHrBaselineEligible;  
1757 -  
1758 - bool matchesContext(HealthRawPointContext pointContext) {  
1759 - return context == pointContext;  
1760 - }  
1761 -  
1762 - HealthRawPointFlags copyWith({  
1763 - bool? isWorkout,  
1764 - bool? isWorkoutRecovery,  
1765 - bool? isSleepLikely,  
1766 - bool? isSuspectedActivity,  
1767 - }) {  
1768 - return HealthRawPointFlags(  
1769 - isWorkout: isWorkout ?? this.isWorkout,  
1770 - isWorkoutRecovery: isWorkoutRecovery ?? this.isWorkoutRecovery,  
1771 - isSleepLikely: isSleepLikely ?? this.isSleepLikely,  
1772 - isSuspectedActivity: isSuspectedActivity ?? this.isSuspectedActivity,  
1773 - );  
1774 - }  
1775 -}  
1776 -  
1777 -enum HealthRawStressState {  
1778 - overload(1),  
1779 - attention(2),  
1780 - normal(3),  
1781 - excellent(4);  
1782 -  
1783 - const HealthRawStressState(this.value);  
1784 -  
1785 - final int value;  
1786 -}  
1787 -  
1788 -class HealthRawHrvStressPoint {  
1789 - const HealthRawHrvStressPoint({  
1790 - required this.userId,  
1791 - required this.rawEndTime,  
1792 - required this.rawHrv,  
1793 - required this.result,  
1794 - required this.sourceStartTime,  
1795 - required this.sourceEndTime,  
1796 - required this.state,  
1797 - required this.baselineHrv,  
1798 - required this.baselineAwakeHrv,  
1799 - required this.baselineSleepHrv,  
1800 - required this.baselineRestingHr,  
1801 - this.flags = const HealthRawPointFlags.none(),  
1802 - this.uploaded = false,  
1803 - });  
1804 -  
1805 - final int userId;  
1806 - final int rawEndTime;  
1807 - final double rawHrv;  
1808 - final double result;  
1809 - final int sourceStartTime;  
1810 - final int sourceEndTime;  
1811 - final HealthRawStressState state;  
1812 - final double baselineHrv;  
1813 - final double baselineAwakeHrv;  
1814 - final double? baselineSleepHrv;  
1815 - final double baselineRestingHr;  
1816 - final HealthRawPointFlags flags;  
1817 - final bool uploaded;  
1818 -  
1819 - factory HealthRawHrvStressPoint.fromDb(Map<String, Object?> row) {  
1820 - final result = (row['result'] as num).toDouble();  
1821 - final rawHrv = _positiveDouble(row['raw_hrv']) ?? result;  
1822 - final baselineHrv = _positiveDouble(row['baseline_hrv']) ?? result;  
1823 - final baselineAwakeHrv =  
1824 - _positiveDouble(row['baseline_awake_hrv']) ?? baselineHrv;  
1825 - return HealthRawHrvStressPoint(  
1826 - userId: row['user_id'] as int,  
1827 - rawEndTime: row['raw_end_time'] as int,  
1828 - rawHrv: rawHrv,  
1829 - result: result,  
1830 - sourceStartTime: row['source_start_time'] as int,  
1831 - sourceEndTime: row['source_end_time'] as int,  
1832 - state: _stressStateFromDb(row['state']) ?? HealthRawStressState.normal,  
1833 - baselineHrv: baselineHrv,  
1834 - baselineAwakeHrv: baselineAwakeHrv,  
1835 - baselineSleepHrv: _positiveDouble(row['baseline_sleep_hrv']),  
1836 - baselineRestingHr: _positiveDouble(row['baseline_resting_hr']) ?? 65,  
1837 - flags: _flagsFromDb(row),  
1838 - uploaded: (row['uploaded'] as int) == 1,  
1839 - );  
1840 - }  
1841 -}  
1842 -  
1843 -class HealthRawRealtimeStressPoint {  
1844 - const HealthRawRealtimeStressPoint({  
1845 - required this.userId,  
1846 - required this.rawEndTime,  
1847 - required this.rawHr,  
1848 - required this.result,  
1849 - required this.sourceStartTime,  
1850 - required this.sourceEndTime,  
1851 - this.flags = const HealthRawPointFlags.none(),  
1852 - this.uploaded = false,  
1853 - });  
1854 -  
1855 - final int userId;  
1856 - final int rawEndTime;  
1857 - final double rawHr;  
1858 - final double result;  
1859 - final int sourceStartTime;  
1860 - final int sourceEndTime;  
1861 - final HealthRawPointFlags flags;  
1862 - final bool uploaded;  
1863 -  
1864 - HealthRawStressState get state => healthRawRealtimeStressState(result);  
1865 - bool get isWorkout => flags.isWorkout;  
1866 - bool get isWorkoutRecovery => flags.isWorkoutRecovery;  
1867 - bool get isSleepLikely => flags.isSleepLikely;  
1868 - bool get isSuspectedActivity => flags.isSuspectedActivity;  
1869 -  
1870 - factory HealthRawRealtimeStressPoint.fromDb(Map<String, Object?> row) {  
1871 - return HealthRawRealtimeStressPoint(  
1872 - userId: row['user_id'] as int,  
1873 - rawEndTime: row['raw_end_time'] as int,  
1874 - rawHr: (row['raw_hr'] as num?)?.toDouble() ?? 0,  
1875 - result: (row['result'] as num).toDouble(),  
1876 - sourceStartTime: row['source_start_time'] as int,  
1877 - sourceEndTime: row['source_end_time'] as int,  
1878 - flags: _flagsFromDb(row),  
1879 - uploaded: (row['uploaded'] as int) == 1,  
1880 - );  
1881 - }  
1882 -}  
1883 -  
1884 -class HealthRawDailyStressPoint {  
1885 - const HealthRawDailyStressPoint({  
1886 - required this.userId,  
1887 - required this.date,  
1888 - required this.stressValue,  
1889 - required this.stressScore,  
1890 - required this.state,  
1891 - required this.dataTime,  
1892 - this.uploaded = false,  
1893 - });  
1894 -  
1895 - final int userId;  
1896 - final int date;  
1897 - final double stressValue;  
1898 - final int stressScore;  
1899 - final HealthRawStressState state;  
1900 - final int dataTime;  
1901 - final bool uploaded;  
1902 -  
1903 - factory HealthRawDailyStressPoint.fromDb(Map<String, Object?> row) {  
1904 - final stressScore = row['stress_score'] as int;  
1905 - return HealthRawDailyStressPoint(  
1906 - userId: row['user_id'] as int,  
1907 - date: row['date'] as int,  
1908 - stressValue: (row['stress_value'] as num).toDouble(),  
1909 - stressScore: stressScore,  
1910 - state: _stressStateFromDb(row['state']) ??  
1911 - healthRawRealtimeStressState(stressScore),  
1912 - dataTime: row['data_time'] as int,  
1913 - uploaded: (row['uploaded'] as int) == 1,  
1914 - );  
1915 - }  
1916 -}  
1917 -  
1918 -class HealthRawSleepResult {  
1919 - const HealthRawSleepResult({  
1920 - required this.userId,  
1921 - required this.date,  
1922 - required this.startDate,  
1923 - required this.sleepScore,  
1924 - required this.sleepState,  
1925 - required this.inBedMinutes,  
1926 - required this.awakMinutes,  
1927 - required this.sleepMinutes,  
1928 - this.uploaded = false,  
1929 - });  
1930 -  
1931 - final int userId;  
1932 - final int date;  
1933 - final int startDate;  
1934 - final int sleepScore;  
1935 - final int sleepState;  
1936 - final int inBedMinutes;  
1937 - final int awakMinutes;  
1938 - final int sleepMinutes;  
1939 - final bool uploaded;  
1940 -  
1941 - factory HealthRawSleepResult.fromDb(Map<String, Object?> row) {  
1942 - return HealthRawSleepResult(  
1943 - userId: row['user_id'] as int,  
1944 - date: row['date'] as int,  
1945 - startDate: row['start_date'] as int,  
1946 - sleepScore: row['sleep_score'] as int,  
1947 - sleepState: row['sleep_state'] as int,  
1948 - inBedMinutes: row['in_bed_minutes'] as int,  
1949 - awakMinutes: row['awak_minutes'] as int,  
1950 - sleepMinutes: row['sleep_minutes'] as int,  
1951 - uploaded: (row['uploaded'] as int? ?? 0) == 1,  
1952 - );  
1953 - }  
1954 -  
1955 - int get sleepStateValue => sleepState;  
1956 -  
1957 - String? get sleepStateDescription {  
1958 - for (final state in HealthSleepState.values) {  
1959 - if (state.value == sleepState) return state.description;  
1960 - }  
1961 - return null;  
1962 - }  
1963 -}  
1964 -  
1965 -class HealthRawDailyStressCalculator {  
1966 - const HealthRawDailyStressCalculator._();  
1967 -  
1968 - static const int _confirmedActivityBufferSeconds = 12 * 60;  
1969 -  
1970 - static HealthRawDailyStressPoint? calculate({  
1971 - required int userId,  
1972 - required int date,  
1973 - required List<HealthRawRealtimeStressPoint> realtimePoints,  
1974 - required int dataTime,  
1975 - }) {  
1976 - final validValuePoints =  
1977 - realtimePoints.where((e) => _isValidStressValue(e.result)).toList();  
1978 - final activityBuffers = _confirmedActivityBuffers(realtimePoints);  
1979 - final values = validValuePoints  
1980 - .where((e) => !_isInTimeRanges(e.rawEndTime, activityBuffers))  
1981 - .map((e) => e.result)  
1982 - .toList()  
1983 - ..sort();  
1984 - if (values.isEmpty) return null;  
1985 - final stressValue = _integerDouble(_average(values));  
1986 - final stressScore = _dailyStressScore(values).round().clamp(1, 100).toInt();  
1987 - return HealthRawDailyStressPoint(  
1988 - userId: userId,  
1989 - date: date,  
1990 - stressValue: stressValue,  
1991 - stressScore: stressScore,  
1992 - state: healthRawRealtimeStressState(stressScore),  
1993 - dataTime: dataTime,  
1994 - );  
1995 - }  
1996 -  
1997 - static double _dailyStressScore(List<double> sortedValues) {  
1998 - final median = _median(sortedValues);  
1999 - final p75 = _percentile75(sortedValues);  
2000 - return _round(_clamp(median * 0.60 + p75 * 0.40, 1, 100));  
2001 - }  
2002 -  
2003 - static List<({int start, int end})> _confirmedActivityBuffers(  
2004 - List<HealthRawRealtimeStressPoint> realtimePoints,  
2005 - ) {  
2006 - final ranges = realtimePoints  
2007 - .where((e) =>  
2008 - _isValidStressValue(e.result) &&  
2009 - (e.isWorkout || e.isWorkoutRecovery))  
2010 - .map((e) => (  
2011 - start: e.rawEndTime - _confirmedActivityBufferSeconds,  
2012 - end: e.rawEndTime + _confirmedActivityBufferSeconds,  
2013 - ))  
2014 - .toList()  
2015 - ..sort((a, b) => a.start.compareTo(b.start));  
2016 - if (ranges.isEmpty) return const <({int start, int end})>[];  
2017 -  
2018 - final merged = <({int start, int end})>[];  
2019 - var current = ranges.first;  
2020 - for (final range in ranges.skip(1)) {  
2021 - if (range.start <= current.end) {  
2022 - current = (  
2023 - start: current.start,  
2024 - end: math.max(current.end, range.end),  
2025 - );  
2026 - } else {  
2027 - merged.add(current);  
2028 - current = range;  
2029 - }  
2030 - }  
2031 - merged.add(current);  
2032 - return merged;  
2033 - }  
2034 -  
2035 - static bool _isInTimeRanges(int time, List<({int start, int end})> ranges) {  
2036 - return ranges.any((e) => time >= e.start && time <= e.end);  
2037 - }  
2038 -  
2039 - static bool _isValidStressValue(num value) => value >= 1 && value <= 100;  
2040 -  
2041 - static double _average(List<double> values) =>  
2042 - values.reduce((a, b) => a + b) / values.length;  
2043 -  
2044 - static double _median(List<double> sortedValues) {  
2045 - final length = sortedValues.length;  
2046 - final middle = length ~/ 2;  
2047 - if (length.isOdd) return sortedValues[middle];  
2048 - return (sortedValues[middle - 1] + sortedValues[middle]) / 2;  
2049 - }  
2050 -  
2051 - static double _percentile75(List<double> sortedValues) {  
2052 - final rank = (sortedValues.length * 0.75).ceil().clamp(  
2053 - 1,  
2054 - sortedValues.length,  
2055 - );  
2056 - return sortedValues[rank - 1];  
2057 - }  
2058 -  
2059 - static double _clamp(num value, num min, num max) =>  
2060 - math.max(min.toDouble(), math.min(max.toDouble(), value.toDouble()));  
2061 -  
2062 - static double _round(double value) => (value * 10).round() / 10;  
2063 -  
2064 - static double _integerDouble(num value) => value.toInt().toDouble();  
2065 -}  
2066 -  
2067 -HealthRawStressState healthRawRealtimeStressState(num value) {  
2068 - if (value >= 81) return HealthRawStressState.overload;  
2069 - if (value >= 61) return HealthRawStressState.attention;  
2070 - if (value >= 21) return HealthRawStressState.normal;  
2071 - return HealthRawStressState.excellent;  
2072 -}  
2073 -  
2074 -HealthRawPointFlags _flagsFromDb(Map<String, Object?> row) {  
2075 - return HealthRawPointFlags(  
2076 - isWorkout: (row['is_workout'] as int? ?? 0) == 1,  
2077 - isWorkoutRecovery: (row['is_workout_recovery'] as int? ?? 0) == 1,  
2078 - isSleepLikely: (row['is_sleep_likely'] as int? ?? 0) == 1,  
2079 - isSuspectedActivity: (row['is_suspected_activity'] as int? ?? 0) == 1,  
2080 - );  
2081 -}  
2082 -  
2083 -double? _positiveDouble(Object? value) {  
2084 - final result = (value as num?)?.toDouble();  
2085 - if (result == null || result <= 0) return null;  
2086 - return result;  
2087 -}  
2088 -  
2089 -double _integerDouble(num value) => value.toInt().toDouble();  
2090 -  
2091 -HealthRawStressState? _stressStateFromDb(Object? value) {  
2092 - final stateValue = value as int?;  
2093 - if (stateValue == null) return null;  
2094 - for (final state in HealthRawStressState.values) {  
2095 - if (state.value == stateValue) return state;  
2096 - }  
2097 - return null;  
2098 -}  
2099 -  
2100 class HealthRawStressDbRows { 1647 class HealthRawStressDbRows {
2101 const HealthRawStressDbRows({ 1648 const HealthRawStressDbRows({
2102 required this.hrvRows, 1649 required this.hrvRows,
@@ -2111,6 +1658,8 @@ class HealthRawStressDbRows { @@ -2111,6 +1658,8 @@ class HealthRawStressDbRows {
2111 final List<Map<String, Object?>> sleepRows; 1658 final List<Map<String, Object?>> sleepRows;
2112 } 1659 }
2113 1660
  1661 +double _integerDouble(num value) => value.toInt().toDouble();
  1662 +
2114 class HealthRawStressLocalStore { 1663 class HealthRawStressLocalStore {
2115 HealthRawStressLocalStore({ 1664 HealthRawStressLocalStore({
2116 Directory? rootDirectory, 1665 Directory? rootDirectory,
@@ -3,9 +3,9 @@ import 'dart:io'; @@ -3,9 +3,9 @@ import 'dart:io';
3 3
4 import 'package:path_provider/path_provider.dart'; 4 import 'package:path_provider/path_provider.dart';
5 5
6 -import '../../l10n/gen/app_localizations.dart';  
7 -import '../../pigeon/platform_api.g.dart';  
8 -import 'health_raw_data_core_service.dart'; 6 +import '../../../../l10n/gen/app_localizations.dart';
  7 +import '../../../../pigeon/platform_api.g.dart';
  8 +import '../health_raw_models.dart';
9 9
10 const healthRawTodayLink = 'doublefeel://flutter/home?tab=today'; 10 const healthRawTodayLink = 'doublefeel://flutter/home?tab=today';
11 const healthRawHrvChangeLink = 11 const healthRawHrvChangeLink =
1 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 1 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
2 import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; 2 import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
3 3
4 import '../logging/app_logger.dart'; 4 import '../logging/app_logger.dart';
1 -import 'dart:convert';  
2 -  
3 -import '../../pigeon/wear_engine_api.g.dart';  
4 -import '../logging/app_logger.dart';  
5 -import '../util/platform.dart';  
6 -import '../../data/local/user_preferences_storage.dart';  
7 -import '../../data/models/enums/app_enums.dart';  
8 -import '../network/api/health_api.dart';  
9 -import '../network/api/interaction_api.dart';  
10 -import '../../data/models/interaction/interaction_models.dart';  
11 -import '../result/app_result.dart';  
12 -  
13 -/// Watch sync and P2P messaging.  
14 -class WearEngineService {  
15 - WearEngineService(  
16 - this._userPrefs,  
17 - this._healthApi,  
18 - this._interactionApi, {  
19 - WearEngineHostApi? wearEngineHost,  
20 - }) : _wearEngineHost = wearEngineHost ?? WearEngineHostApi();  
21 -  
22 - final WearEngineHostApi _wearEngineHost;  
23 - final UserPreferencesStorage _userPrefs;  
24 - final HealthApi _healthApi;  
25 - final InteractionApi _interactionApi;  
26 -  
27 - Future<bool> hasAvailableDevices() async {  
28 - if (!isAndroid) {  
29 - return false;  
30 - }  
31 - return _wearEngineHost.hasAvailableDevices();  
32 - }  
33 -  
34 - Future<bool> prepareWatchConnection() async {  
35 - final device = await _wearEngineHost.checkConnectedDevice();  
36 - if (device == null) {  
37 - return false;  
38 - }  
39 - return _wearEngineHost.registerMessageReceiver();  
40 - }  
41 -  
42 - Future<bool> sendInteractionMessage(int actionType) async {  
43 - final data = InteractionData(  
44 - interactionType: 0,  
45 - actionType: actionType,  
46 - );  
47 - final result = await _interactionApi.sendInteraction(data);  
48 - return result is AppSuccess;  
49 - }  
50 -  
51 -}  
1 import 'dart:math' as math; 1 import 'dart:math' as math;
2 2
3 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';  
4 -import 'package:doublefeel_flutter/core/services/health_sleep_calculator.dart'; 3 +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
  4 +import 'package:doublefeel_flutter/core/services/raw_data_service/health_sleep_calculator.dart';
5 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; 5 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
6 import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart'; 6 import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart';
7 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; 7 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
1 import 'package:doublefeel_flutter/core/error/app_error.dart'; 1 import 'package:doublefeel_flutter/core/error/app_error.dart';
2 import 'package:doublefeel_flutter/core/result/app_result.dart'; 2 import 'package:doublefeel_flutter/core/result/app_result.dart';
3 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 3 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
  4 +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
4 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; 5 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
5 import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart'; 6 import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart';
6 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; 7 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
1 import 'dart:convert'; 1 import 'dart:convert';
2 2
3 import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; 3 import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
4 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 4 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
5 import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; 5 import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
6 import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; 6 import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
7 import 'package:get/get.dart'; 7 import 'package:get/get.dart';
@@ -8,7 +8,7 @@ import 'package:intl/date_symbol_data_local.dart'; @@ -8,7 +8,7 @@ import 'package:intl/date_symbol_data_local.dart';
8 8
9 import 'app/bootstrap/app_bootstrap.dart'; 9 import 'app/bootstrap/app_bootstrap.dart';
10 import 'app/double_feel_app.dart'; 10 import 'app/double_feel_app.dart';
11 -import 'core/services/health_raw_data_core_service.dart'; 11 +import 'core/services/raw_data_service/ios/health_raw_data_core_service.dart';
12 import 'core/theme/app_theme.dart'; 12 import 'core/theme/app_theme.dart';
13 import 'data/local/user_preferences_storage.dart'; 13 import 'data/local/user_preferences_storage.dart';
14 14
1 import 'dart:async'; 1 import 'dart:async';
2 2
3 import 'package:doublefeel_flutter/core/result/app_result.dart'; 3 import 'package:doublefeel_flutter/core/result/app_result.dart';
4 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';  
5 -import 'package:doublefeel_flutter/core/services/health_raw_local_notification.dart'; 4 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
  5 +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
  6 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_local_notification.dart';
6 import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart'; 7 import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart';
7 import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart'; 8 import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
8 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; 9 import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
1 import 'dart:io'; 1 import 'dart:io';
2 2
3 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';  
4 -import 'package:doublefeel_flutter/core/services/health_raw_local_notification.dart'; 3 +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
  4 +import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_local_notification.dart';
5 import 'package:doublefeel_flutter/l10n/gen/app_localizations_zh.dart'; 5 import 'package:doublefeel_flutter/l10n/gen/app_localizations_zh.dart';
6 import 'package:flutter_test/flutter_test.dart'; 6 import 'package:flutter_test/flutter_test.dart';
7 7
1 -import 'package:doublefeel_flutter/core/services/health_sleep_calculator.dart'; 1 +import 'package:doublefeel_flutter/core/services/raw_data_service/health_sleep_calculator.dart';
2 import 'package:flutter_test/flutter_test.dart'; 2 import 'package:flutter_test/flutter_test.dart';
3 3
4 void main() { 4 void main() {
  1 +import 'package:doublefeel_flutter/core/services/raw_data_service/ohos/ohos_health_raw_data_sync_service.dart';
  2 +import 'package:flutter_test/flutter_test.dart';
  3 +
  4 +void main() {
  5 + test('syncRawData starts from latest local data time and stores each page',
  6 + () async {
  7 + final remote = _FakeOhosHealthRawDataRemoteDataSource([
  8 + const OhosHealthRawDataPage(
  9 + items: [
  10 + OhosHealthRawDataItem(
  11 + dataType: 2,
  12 + dataTime: 1001,
  13 + payload: {'value': 70},
  14 + ),
  15 + ],
  16 + nextPageToken: 'next',
  17 + ),
  18 + const OhosHealthRawDataPage(
  19 + items: [
  20 + OhosHealthRawDataItem(
  21 + dataType: 2,
  22 + dataTime: 1002,
  23 + payload: {'value': 71},
  24 + ),
  25 + ],
  26 + ),
  27 + ]);
  28 + final local = _FakeOhosHealthRawDataLocalStore(latestDataTime: 1000);
  29 + final service = OhosHealthRawDataSyncService(
  30 + remoteDataSource: remote,
  31 + localStore: local,
  32 + nowProvider: () => DateTime.fromMillisecondsSinceEpoch(2000 * 1000),
  33 + );
  34 +
  35 + final result = await service.syncRawData(dataType: 2, endTime: 2000);
  36 +
  37 + expect(result.startTime, 1000);
  38 + expect(result.endTime, 2000);
  39 + expect(result.pageCount, 2);
  40 + expect(result.storedCount, 2);
  41 + expect(remote.calls.map((e) => e.pageToken), [null, 'next']);
  42 + expect(remote.calls.every((e) => e.startTime == 1000), isTrue);
  43 + expect(local.storedBatches.map((e) => e.length), [1, 1]);
  44 + });
  45 +
  46 + test('syncRawData falls back to half-year lookback when local data is empty',
  47 + () async {
  48 + final now = DateTime(2026, 8, 13, 12);
  49 + final remote = _FakeOhosHealthRawDataRemoteDataSource([
  50 + const OhosHealthRawDataPage(items: <OhosHealthRawDataItem>[]),
  51 + ]);
  52 + final local = _FakeOhosHealthRawDataLocalStore();
  53 + final service = OhosHealthRawDataSyncService(
  54 + remoteDataSource: remote,
  55 + localStore: local,
  56 + nowProvider: () => now,
  57 + );
  58 +
  59 + final result = await service.syncRawData(dataType: 7);
  60 +
  61 + final expectedStart = now
  62 + .subtract(
  63 + const Duration(
  64 + days: OhosHealthRawDataSyncService.defaultLookbackDays,
  65 + ),
  66 + )
  67 + .millisecondsSinceEpoch ~/
  68 + 1000;
  69 + final expectedEnd = now.millisecondsSinceEpoch ~/ 1000;
  70 +
  71 + expect(result.startTime, expectedStart);
  72 + expect(result.endTime, expectedEnd);
  73 + expect(remote.calls.single.startTime, expectedStart);
  74 + expect(remote.calls.single.endTime, expectedEnd);
  75 + expect(local.storedBatches, isEmpty);
  76 + });
  77 +}
  78 +
  79 +class _FakeOhosHealthRawDataRemoteDataSource
  80 + implements OhosHealthRawDataRemoteDataSource {
  81 + _FakeOhosHealthRawDataRemoteDataSource(this._pages);
  82 +
  83 + final List<OhosHealthRawDataPage> _pages;
  84 + final List<_FetchCall> calls = <_FetchCall>[];
  85 +
  86 + @override
  87 + Future<OhosHealthRawDataPage> fetchRawDataPage({
  88 + required int dataType,
  89 + required int startTime,
  90 + required int endTime,
  91 + String? pageToken,
  92 + }) async {
  93 + calls.add(
  94 + _FetchCall(
  95 + dataType: dataType,
  96 + startTime: startTime,
  97 + endTime: endTime,
  98 + pageToken: pageToken,
  99 + ),
  100 + );
  101 + return _pages[calls.length - 1];
  102 + }
  103 +}
  104 +
  105 +class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
  106 + _FakeOhosHealthRawDataLocalStore({int? latestDataTime})
  107 + : _latestDataTime = latestDataTime;
  108 +
  109 + final int? _latestDataTime;
  110 + final List<List<OhosHealthRawDataItem>> storedBatches =
  111 + <List<OhosHealthRawDataItem>>[];
  112 +
  113 + @override
  114 + Future<int?> latestDataTime({required int dataType}) async {
  115 + return _latestDataTime;
  116 + }
  117 +
  118 + @override
  119 + Future<void> upsertRawDataBatch({
  120 + required int dataType,
  121 + required List<OhosHealthRawDataItem> items,
  122 + }) async {
  123 + storedBatches.add(items);
  124 + }
  125 +}
  126 +
  127 +class _FetchCall {
  128 + const _FetchCall({
  129 + required this.dataType,
  130 + required this.startTime,
  131 + required this.endTime,
  132 + required this.pageToken,
  133 + });
  134 +
  135 + final int dataType;
  136 + final int startTime;
  137 + final int endTime;
  138 + final String? pageToken;
  139 +}
1 -import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; 1 +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
2 import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart'; 2 import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart';
3 import 'package:doublefeel_flutter/data/models/health/sleep/sleep_statistics_data.dart'; 3 import 'package:doublefeel_flutter/data/models/health/sleep/sleep_statistics_data.dart';
4 import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; 4 import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';