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'; |
-
Please register or login to post a comment