Showing
8 changed files
with
408 additions
and
95 deletions
| @@ -22,6 +22,7 @@ import '../../core/services/push_service.dart'; | @@ -22,6 +22,7 @@ import '../../core/services/push_service.dart'; | ||
| 22 | import '../../core/services/thinking_data_service.dart'; | 22 | import '../../core/services/thinking_data_service.dart'; |
| 23 | import '../../core/services/user_state_service.dart'; | 23 | import '../../core/services/user_state_service.dart'; |
| 24 | import '../../core/services/web_socket_service.dart'; | 24 | import '../../core/services/web_socket_service.dart'; |
| 25 | +import '../../core/util/app_toast.dart'; | ||
| 25 | import '../../data/local/local_storage.dart'; | 26 | import '../../data/local/local_storage.dart'; |
| 26 | import '../../data/local/user_account_storage.dart'; | 27 | import '../../data/local/user_account_storage.dart'; |
| 27 | import '../../data/local/user_preferences_storage.dart'; | 28 | import '../../data/local/user_preferences_storage.dart'; |
| @@ -74,6 +75,7 @@ void registerUserSessionDeps() { | @@ -74,6 +75,7 @@ void registerUserSessionDeps() { | ||
| 74 | environmentConfig: environmentConfig, | 75 | environmentConfig: environmentConfig, |
| 75 | userIdProvider: () => userPrefs.preferences.value.meUserInfo?.id ?? 0, | 76 | userIdProvider: () => userPrefs.preferences.value.meUserInfo?.id ?? 0, |
| 76 | serverHealthApi: HealthApi(dioClient), | 77 | serverHealthApi: HealthApi(dioClient), |
| 78 | + ohosSyncFailureToastSink: AppToast.show, | ||
| 77 | ), | 79 | ), |
| 78 | permanent: true, | 80 | permanent: true, |
| 79 | ); | 81 | ); |
| @@ -24,6 +24,7 @@ class HealthRawDataCoreService { | @@ -24,6 +24,7 @@ class HealthRawDataCoreService { | ||
| 24 | int Function()? userIdProvider, | 24 | int Function()? userIdProvider, |
| 25 | bool uploadResultsAfterCalculation = true, | 25 | bool uploadResultsAfterCalculation = true, |
| 26 | HealthApi? serverHealthApi, | 26 | HealthApi? serverHealthApi, |
| 27 | + void Function(String message)? ohosSyncFailureToastSink, | ||
| 27 | }) : _appleService = appleService ?? | 28 | }) : _appleService = appleService ?? |
| 28 | AppleHealthRawDataCoreService( | 29 | AppleHealthRawDataCoreService( |
| 29 | environmentConfig: environmentConfig, | 30 | environmentConfig: environmentConfig, |
| @@ -37,6 +38,7 @@ class HealthRawDataCoreService { | @@ -37,6 +38,7 @@ class HealthRawDataCoreService { | ||
| 37 | userIdProvider: userIdProvider, | 38 | userIdProvider: userIdProvider, |
| 38 | uploadResultsAfterCalculation: uploadResultsAfterCalculation, | 39 | uploadResultsAfterCalculation: uploadResultsAfterCalculation, |
| 39 | serverHealthApi: serverHealthApi, | 40 | serverHealthApi: serverHealthApi, |
| 41 | + toastSink: ohosSyncFailureToastSink, | ||
| 40 | ), | 42 | ), |
| 41 | _targetPlatform = targetPlatform; | 43 | _targetPlatform = targetPlatform; |
| 42 | 44 |
| @@ -1484,9 +1484,8 @@ class AppleHealthRawDataCoreService { | @@ -1484,9 +1484,8 @@ class AppleHealthRawDataCoreService { | ||
| 1484 | }) async { | 1484 | }) async { |
| 1485 | if (sleepIntervals.isEmpty) return const <HealthRawSleepResult>[]; | 1485 | if (sleepIntervals.isEmpty) return const <HealthRawSleepResult>[]; |
| 1486 | final days = { | 1486 | final days = { |
| 1487 | - for (final interval in sleepIntervals) | ||
| 1488 | - DateTime.fromMillisecondsSinceEpoch(interval.endTime * 1000) | ||
| 1489 | - }.map((date) => DateTime(date.year, date.month, date.day)).toList() | 1487 | + for (final interval in sleepIntervals) _localDay(interval.endTime), |
| 1488 | + }.toList() | ||
| 1490 | ..sort((a, b) => a.compareTo(b)); | 1489 | ..sort((a, b) => a.compareTo(b)); |
| 1491 | final results = <HealthRawSleepResult>[]; | 1490 | final results = <HealthRawSleepResult>[]; |
| 1492 | for (final day in days) { | 1491 | for (final day in days) { |
| @@ -1608,6 +1607,11 @@ class AppleHealthRawDataCoreService { | @@ -1608,6 +1607,11 @@ class AppleHealthRawDataCoreService { | ||
| 1608 | return date.year * 10000 + date.month * 100 + date.day; | 1607 | return date.year * 10000 + date.month * 100 + date.day; |
| 1609 | } | 1608 | } |
| 1610 | 1609 | ||
| 1610 | + static DateTime _localDay(int seconds) { | ||
| 1611 | + final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000); | ||
| 1612 | + return DateTime(date.year, date.month, date.day); | ||
| 1613 | + } | ||
| 1614 | + | ||
| 1611 | static (int startTime, int endTime) _dayRangeFromDateKey(int dateKey) { | 1615 | static (int startTime, int endTime) _dayRangeFromDateKey(int dateKey) { |
| 1612 | final year = dateKey ~/ 10000; | 1616 | final year = dateKey ~/ 10000; |
| 1613 | final month = (dateKey ~/ 100) % 100; | 1617 | final month = (dateKey ~/ 100) % 100; |
| @@ -43,6 +43,8 @@ class OHOSHealthRawDataCoreService { | @@ -43,6 +43,8 @@ class OHOSHealthRawDataCoreService { | ||
| 43 | Future<bool> Function()? healthReadAuthorizationChecker, | 43 | Future<bool> Function()? healthReadAuthorizationChecker, |
| 44 | OhosHealthRawResultUploadService? resultUploadService, | 44 | OhosHealthRawResultUploadService? resultUploadService, |
| 45 | HealthRawLocalNotificationDispatcher? localNotificationDispatcher, | 45 | HealthRawLocalNotificationDispatcher? localNotificationDispatcher, |
| 46 | + bool Function()? debugModeProvider, | ||
| 47 | + void Function(String message)? toastSink, | ||
| 46 | }) : _rawDataSource = rawDataSource ?? | 48 | }) : _rawDataSource = rawDataSource ?? |
| 47 | OhosHealthRawDataSource( | 49 | OhosHealthRawDataSource( |
| 48 | userIdProvider: userIdProvider, | 50 | userIdProvider: userIdProvider, |
| @@ -58,6 +60,8 @@ class OHOSHealthRawDataCoreService { | @@ -58,6 +60,8 @@ class OHOSHealthRawDataCoreService { | ||
| 58 | (Get.isRegistered<HarmonyApi>() ? Get.find<HarmonyApi>() : null), | 60 | (Get.isRegistered<HarmonyApi>() ? Get.find<HarmonyApi>() : null), |
| 59 | _healthReadAuthorizationChecker = healthReadAuthorizationChecker, | 61 | _healthReadAuthorizationChecker = healthReadAuthorizationChecker, |
| 60 | _resultUploadService = resultUploadService, | 62 | _resultUploadService = resultUploadService, |
| 63 | + _debugModeProvider = debugModeProvider, | ||
| 64 | + _toastSink = toastSink, | ||
| 61 | _localNotificationDispatcher = localNotificationDispatcher ?? | 65 | _localNotificationDispatcher = localNotificationDispatcher ?? |
| 62 | HealthRawLocalNotificationDispatcher(); | 66 | HealthRawLocalNotificationDispatcher(); |
| 63 | 67 | ||
| @@ -70,13 +74,16 @@ class OHOSHealthRawDataCoreService { | @@ -70,13 +74,16 @@ class OHOSHealthRawDataCoreService { | ||
| 70 | final HarmonyApi? _harmonyApi; | 74 | final HarmonyApi? _harmonyApi; |
| 71 | final Future<bool> Function()? _healthReadAuthorizationChecker; | 75 | final Future<bool> Function()? _healthReadAuthorizationChecker; |
| 72 | OhosHealthRawResultUploadService? _resultUploadService; | 76 | OhosHealthRawResultUploadService? _resultUploadService; |
| 77 | + final bool Function()? _debugModeProvider; | ||
| 78 | + final void Function(String message)? _toastSink; | ||
| 73 | final HealthRawLocalNotificationDispatcher _localNotificationDispatcher; | 79 | final HealthRawLocalNotificationDispatcher _localNotificationDispatcher; |
| 74 | final StreamController<HealthRawDataUpdatedEvent> | 80 | final StreamController<HealthRawDataUpdatedEvent> |
| 75 | _healthDataUpdatedController = | 81 | _healthDataUpdatedController = |
| 76 | StreamController<HealthRawDataUpdatedEvent>.broadcast(); | 82 | StreamController<HealthRawDataUpdatedEvent>.broadcast(); |
| 77 | Future<HealthRawStressCalculationResult>? _coreCalculation; | 83 | Future<HealthRawStressCalculationResult>? _coreCalculation; |
| 78 | 84 | ||
| 79 | - bool get _isDebug => _environmentConfig?.isDebug ?? false; | 85 | + bool get _isDebug => |
| 86 | + _debugModeProvider?.call() ?? _environmentConfig?.isDebug ?? false; | ||
| 80 | 87 | ||
| 81 | int get _userId { | 88 | int get _userId { |
| 82 | final userId = _userIdProvider?.call() ?? 0; | 89 | final userId = _userIdProvider?.call() ?? 0; |
| @@ -267,13 +274,6 @@ class OHOSHealthRawDataCoreService { | @@ -267,13 +274,6 @@ class OHOSHealthRawDataCoreService { | ||
| 267 | }) async { | 274 | }) async { |
| 268 | var hasExistingHrv = false; | 275 | var hasExistingHrv = false; |
| 269 | var hasExistingSleep = false; | 276 | var hasExistingSleep = false; |
| 270 | - final emptyResult = HealthRawStressCalculationResult( | ||
| 271 | - userId: userId, | ||
| 272 | - hrvStressPoints: const <HealthRawHrvStressPoint>[], | ||
| 273 | - realtimeStressPoints: const <HealthRawRealtimeStressPoint>[], | ||
| 274 | - dailyStressPoints: const <HealthRawDailyStressPoint>[], | ||
| 275 | - sleepResults: const <HealthRawSleepResult>[], | ||
| 276 | - ); | ||
| 277 | try { | 277 | try { |
| 278 | final hrvContextStart = | 278 | final hrvContextStart = |
| 279 | await _localStore.latestHrvSourceStartTime(userId); | 279 | await _localStore.latestHrvSourceStartTime(userId); |
| @@ -433,11 +433,7 @@ class OHOSHealthRawDataCoreService { | @@ -433,11 +433,7 @@ class OHOSHealthRawDataCoreService { | ||
| 433 | error, | 433 | error, |
| 434 | stackTrace, | 434 | stackTrace, |
| 435 | ); | 435 | ); |
| 436 | - return _OhosStoredCalculationResult( | ||
| 437 | - result: emptyResult, | ||
| 438 | - hasExistingHrv: hasExistingHrv, | ||
| 439 | - hasExistingSleep: hasExistingSleep, | ||
| 440 | - ); | 436 | + rethrow; |
| 441 | } | 437 | } |
| 442 | } | 438 | } |
| 443 | 439 | ||
| @@ -895,7 +891,6 @@ class OHOSHealthRawDataCoreService { | @@ -895,7 +891,6 @@ class OHOSHealthRawDataCoreService { | ||
| 895 | if (rawDataSource is! OhosHealthRawDataSource) { | 891 | if (rawDataSource is! OhosHealthRawDataSource) { |
| 896 | return const <OhosHealthRawDataSyncResult>[]; | 892 | return const <OhosHealthRawDataSyncResult>[]; |
| 897 | } | 893 | } |
| 898 | - await rawDataSource.getActivityGoal(refresh: true); | ||
| 899 | return rawDataSource.syncCalculationRawData( | 894 | return rawDataSource.syncCalculationRawData( |
| 900 | startTime: startTime, | 895 | startTime: startTime, |
| 901 | endTime: endTime, | 896 | endTime: endTime, |
| @@ -921,12 +916,15 @@ class OHOSHealthRawDataCoreService { | @@ -921,12 +916,15 @@ class OHOSHealthRawDataCoreService { | ||
| 921 | ); | 916 | ); |
| 922 | } catch (error, stackTrace) { | 917 | } catch (error, stackTrace) { |
| 923 | _logError( | 918 | _logError( |
| 924 | - '$_calculateLogMarker sync_failed_continue ' | 919 | + '$_calculateLogMarker sync_failed_abort ' |
| 925 | 'startTime=$startTime endTime=$endTime', | 920 | 'startTime=$startTime endTime=$endTime', |
| 926 | error, | 921 | error, |
| 927 | stackTrace, | 922 | stackTrace, |
| 928 | ); | 923 | ); |
| 929 | - return const <OhosHealthRawDataSyncResult>[]; | 924 | + if (_isDebug) { |
| 925 | + _toastSink?.call('数据同步失败,请刷新首页重试'); | ||
| 926 | + } | ||
| 927 | + rethrow; | ||
| 930 | } | 928 | } |
| 931 | } | 929 | } |
| 932 | 930 | ||
| @@ -1070,9 +1068,8 @@ class OHOSHealthRawDataCoreService { | @@ -1070,9 +1068,8 @@ class OHOSHealthRawDataCoreService { | ||
| 1070 | return const <HealthRawSleepResult>[]; | 1068 | return const <HealthRawSleepResult>[]; |
| 1071 | } | 1069 | } |
| 1072 | final days = { | 1070 | final days = { |
| 1073 | - for (final interval in sleepIntervals) | ||
| 1074 | - DateTime.fromMillisecondsSinceEpoch(interval.endTime * 1000) | ||
| 1075 | - }.map((date) => DateTime(date.year, date.month, date.day)).toList() | 1071 | + for (final interval in sleepIntervals) _localDay(interval.endTime), |
| 1072 | + }.toList() | ||
| 1076 | ..sort((a, b) => a.compareTo(b)); | 1073 | ..sort((a, b) => a.compareTo(b)); |
| 1077 | _logInfo( | 1074 | _logInfo( |
| 1078 | '$_sleepCalcLogMarker days userId=$userId ' | 1075 | '$_sleepCalcLogMarker days userId=$userId ' |
| @@ -1224,6 +1221,11 @@ class OHOSHealthRawDataCoreService { | @@ -1224,6 +1221,11 @@ class OHOSHealthRawDataCoreService { | ||
| 1224 | return _dateKeyFromDateTime(date); | 1221 | return _dateKeyFromDateTime(date); |
| 1225 | } | 1222 | } |
| 1226 | 1223 | ||
| 1224 | + static DateTime _localDay(int seconds) { | ||
| 1225 | + final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000); | ||
| 1226 | + return DateTime(date.year, date.month, date.day); | ||
| 1227 | + } | ||
| 1228 | + | ||
| 1227 | static int _dateKeyFromDateTime(DateTime date) { | 1229 | static int _dateKeyFromDateTime(DateTime date) { |
| 1228 | return date.year * 10000 + date.month * 100 + date.day; | 1230 | return date.year * 10000 + date.month * 100 + date.day; |
| 1229 | } | 1231 | } |
| @@ -109,9 +109,10 @@ class OhosHealthRawDataSyncService { | @@ -109,9 +109,10 @@ class OhosHealthRawDataSyncService { | ||
| 109 | ); | 109 | ); |
| 110 | final List<OhosHealthRawDataSyncResult> results; | 110 | final List<OhosHealthRawDataSyncResult> results; |
| 111 | try { | 111 | try { |
| 112 | - results = await Future.wait( | 112 | + final activityGoalFuture = _remoteDataSource.fetchActivityGoal(); |
| 113 | + final rawFetchesFuture = Future.wait( | ||
| 113 | resolvedDataTypes.map( | 114 | resolvedDataTypes.map( |
| 114 | - (dataType) => syncRawData( | 115 | + (dataType) => _fetchRawDataForSync( |
| 115 | dataType: dataType, | 116 | dataType: dataType, |
| 116 | startTime: startTime, | 117 | startTime: startTime, |
| 117 | endTime: resolvedEndTime, | 118 | endTime: resolvedEndTime, |
| @@ -119,6 +120,27 @@ class OhosHealthRawDataSyncService { | @@ -119,6 +120,27 @@ class OhosHealthRawDataSyncService { | ||
| 119 | ), | 120 | ), |
| 120 | eagerError: true, | 121 | eagerError: true, |
| 121 | ); | 122 | ); |
| 123 | + final fetched = await Future.wait<Object?>( | ||
| 124 | + <Future<Object?>>[ | ||
| 125 | + activityGoalFuture, | ||
| 126 | + rawFetchesFuture, | ||
| 127 | + ], | ||
| 128 | + eagerError: true, | ||
| 129 | + ); | ||
| 130 | + final activityGoal = fetched[0] as V2ActivityTarget?; | ||
| 131 | + final rawFetches = fetched[1] as List<_FetchedRawDataSync>; | ||
| 132 | + if (activityGoal != null) { | ||
| 133 | + await _localStore.upsertActivityGoal(activityGoal); | ||
| 134 | + _log( | ||
| 135 | + 'calculation_sync_activity_goal_stored ' | ||
| 136 | + 'move=${activityGoal.move} step=${activityGoal.step} ' | ||
| 137 | + 'exercise=${activityGoal.exercise} stand=${activityGoal.stand}', | ||
| 138 | + ); | ||
| 139 | + } | ||
| 140 | + results = <OhosHealthRawDataSyncResult>[]; | ||
| 141 | + for (final fetch in rawFetches) { | ||
| 142 | + results.add(await _storeFetchedRawData(fetch)); | ||
| 143 | + } | ||
| 122 | } catch (error, stackTrace) { | 144 | } catch (error, stackTrace) { |
| 123 | _log( | 145 | _log( |
| 124 | 'calculation_sync_failed dataTypes=${resolvedDataTypes.join(',')} ' | 146 | 'calculation_sync_failed dataTypes=${resolvedDataTypes.join(',')} ' |
| @@ -142,6 +164,26 @@ class OhosHealthRawDataSyncService { | @@ -142,6 +164,26 @@ class OhosHealthRawDataSyncService { | ||
| 142 | return results; | 164 | return results; |
| 143 | } | 165 | } |
| 144 | 166 | ||
| 167 | + Future<_FetchedRawDataSync> _fetchRawDataForSync({ | ||
| 168 | + required int dataType, | ||
| 169 | + required int? startTime, | ||
| 170 | + required int endTime, | ||
| 171 | + }) async { | ||
| 172 | + final resolvedStartTime = await _resolveStartTime( | ||
| 173 | + dataType: dataType, | ||
| 174 | + requestedStartTime: startTime, | ||
| 175 | + endTime: endTime, | ||
| 176 | + ); | ||
| 177 | + if (endTime < resolvedStartTime) { | ||
| 178 | + throw ArgumentError.value(endTime, 'endTime'); | ||
| 179 | + } | ||
| 180 | + return _fetchResolvedRawData( | ||
| 181 | + dataType: dataType, | ||
| 182 | + startTime: resolvedStartTime, | ||
| 183 | + endTime: endTime, | ||
| 184 | + ); | ||
| 185 | + } | ||
| 186 | + | ||
| 145 | Future<List<OhosHealthRawDataItem>> queryRawData({ | 187 | Future<List<OhosHealthRawDataItem>> queryRawData({ |
| 146 | required int dataType, | 188 | required int dataType, |
| 147 | required int startTime, | 189 | required int startTime, |
| @@ -187,6 +229,19 @@ class OhosHealthRawDataSyncService { | @@ -187,6 +229,19 @@ class OhosHealthRawDataSyncService { | ||
| 187 | required int startTime, | 229 | required int startTime, |
| 188 | required int endTime, | 230 | required int endTime, |
| 189 | }) async { | 231 | }) async { |
| 232 | + final fetched = await _fetchResolvedRawData( | ||
| 233 | + dataType: dataType, | ||
| 234 | + startTime: startTime, | ||
| 235 | + endTime: endTime, | ||
| 236 | + ); | ||
| 237 | + return _storeFetchedRawData(fetched); | ||
| 238 | + } | ||
| 239 | + | ||
| 240 | + Future<_FetchedRawDataSync> _fetchResolvedRawData({ | ||
| 241 | + required int dataType, | ||
| 242 | + required int startTime, | ||
| 243 | + required int endTime, | ||
| 244 | + }) async { | ||
| 190 | final fetchRanges = _splitIntoFetchRanges( | 245 | final fetchRanges = _splitIntoFetchRanges( |
| 191 | dataType: dataType, | 246 | dataType: dataType, |
| 192 | startTime: startTime, | 247 | startTime: startTime, |
| @@ -199,72 +254,105 @@ class OhosHealthRawDataSyncService { | @@ -199,72 +254,105 @@ class OhosHealthRawDataSyncService { | ||
| 199 | 'chunkDays=$fetchChunkDays rangeCount=${fetchRanges.length}', | 254 | 'chunkDays=$fetchChunkDays rangeCount=${fetchRanges.length}', |
| 200 | ); | 255 | ); |
| 201 | 256 | ||
| 257 | + final segments = await Future.wait( | ||
| 258 | + <Future<_FetchedRawDataSegment>>[ | ||
| 259 | + for (var index = 0; index < fetchRanges.length; index++) | ||
| 260 | + _fetchRawDataSegment( | ||
| 261 | + dataType: dataType, | ||
| 262 | + range: fetchRanges[index], | ||
| 263 | + segmentIndex: index, | ||
| 264 | + segmentCount: fetchRanges.length, | ||
| 265 | + fetchChunkDays: fetchChunkDays, | ||
| 266 | + ), | ||
| 267 | + ], | ||
| 268 | + eagerError: true, | ||
| 269 | + ); | ||
| 270 | + segments.sort((a, b) => a.segmentIndex.compareTo(b.segmentIndex)); | ||
| 271 | + return _FetchedRawDataSync( | ||
| 272 | + dataType: dataType, | ||
| 273 | + startTime: startTime, | ||
| 274 | + endTime: endTime, | ||
| 275 | + fetchChunkDays: fetchChunkDays, | ||
| 276 | + segments: segments, | ||
| 277 | + ); | ||
| 278 | + } | ||
| 279 | + | ||
| 280 | + Future<_FetchedRawDataSegment> _fetchRawDataSegment({ | ||
| 281 | + required int dataType, | ||
| 282 | + required OhosHealthRawDataFetchRange range, | ||
| 283 | + required int segmentIndex, | ||
| 284 | + required int segmentCount, | ||
| 285 | + required int fetchChunkDays, | ||
| 286 | + }) async { | ||
| 287 | + _log( | ||
| 288 | + 'segment_start dataType=$dataType ' | ||
| 289 | + 'segment=${segmentIndex + 1}/$segmentCount ' | ||
| 290 | + 'chunkDays=$fetchChunkDays ' | ||
| 291 | + 'startTime=${range.startTime} endTime=${range.endTime} ' | ||
| 292 | + 'startDate=${_dateKeyFromUnixSeconds(range.startTime)} ' | ||
| 293 | + 'endDate=${_dateKeyFromUnixSeconds(range.endTime)}', | ||
| 294 | + ); | ||
| 295 | + | ||
| 296 | + final page = await _remoteDataSource.fetchRawDataPage( | ||
| 297 | + dataType: dataType, | ||
| 298 | + startTime: range.startTime, | ||
| 299 | + endTime: range.endTime, | ||
| 300 | + ); | ||
| 301 | + _log( | ||
| 302 | + 'page_fetched dataType=$dataType ' | ||
| 303 | + 'segment=${segmentIndex + 1}/$segmentCount ' | ||
| 304 | + 'items=${page.items.length}', | ||
| 305 | + ); | ||
| 306 | + | ||
| 307 | + return _FetchedRawDataSegment( | ||
| 308 | + segmentIndex: segmentIndex, | ||
| 309 | + range: range, | ||
| 310 | + page: page, | ||
| 311 | + ); | ||
| 312 | + } | ||
| 313 | + | ||
| 314 | + Future<OhosHealthRawDataSyncResult> _storeFetchedRawData( | ||
| 315 | + _FetchedRawDataSync fetched, | ||
| 316 | + ) async { | ||
| 202 | var pageCount = 0; | 317 | var pageCount = 0; |
| 203 | var storedCount = 0; | 318 | var storedCount = 0; |
| 204 | 319 | ||
| 205 | - for (var rangeIndex = 0; rangeIndex < fetchRanges.length; rangeIndex++) { | ||
| 206 | - final range = fetchRanges[rangeIndex]; | ||
| 207 | - _log( | ||
| 208 | - 'segment_start dataType=$dataType ' | ||
| 209 | - 'segment=${rangeIndex + 1}/${fetchRanges.length} ' | ||
| 210 | - 'chunkDays=$fetchChunkDays ' | ||
| 211 | - 'startTime=${range.startTime} endTime=${range.endTime} ' | ||
| 212 | - 'startDate=${_dateKeyFromUnixSeconds(range.startTime)} ' | ||
| 213 | - 'endDate=${_dateKeyFromUnixSeconds(range.endTime)}', | ||
| 214 | - ); | ||
| 215 | - | ||
| 216 | - String? pageToken; | ||
| 217 | - do { | ||
| 218 | - final page = await _remoteDataSource.fetchRawDataPage( | ||
| 219 | - dataType: dataType, | ||
| 220 | - startTime: range.startTime, | ||
| 221 | - endTime: range.endTime, | ||
| 222 | - pageToken: pageToken, | 320 | + for (final segment in fetched.segments) { |
| 321 | + final page = segment.page; | ||
| 322 | + pageCount += 1; | ||
| 323 | + if (page.items.isNotEmpty) { | ||
| 324 | + final pageStoredCount = await _localStore.upsertRawDataBatch( | ||
| 325 | + dataType: fetched.dataType, | ||
| 326 | + items: page.items, | ||
| 223 | ); | 327 | ); |
| 224 | - pageCount += 1; | 328 | + storedCount += pageStoredCount; |
| 225 | _log( | 329 | _log( |
| 226 | - 'page_fetched dataType=$dataType ' | ||
| 227 | - 'segment=${rangeIndex + 1}/${fetchRanges.length} ' | ||
| 228 | - 'page=$pageCount items=${page.items.length} ' | ||
| 229 | - 'nextPageToken=${page.nextPageToken ?? ''}', | 330 | + 'page_stored dataType=${fetched.dataType} ' |
| 331 | + 'segment=${segment.segmentIndex + 1}/${fetched.segmentCount} ' | ||
| 332 | + 'fetchedItems=${page.items.length} ' | ||
| 333 | + 'storedItems=$pageStoredCount totalStored=$storedCount', | ||
| 230 | ); | 334 | ); |
| 231 | - | ||
| 232 | - if (page.items.isNotEmpty) { | ||
| 233 | - final pageStoredCount = await _localStore.upsertRawDataBatch( | ||
| 234 | - dataType: dataType, | ||
| 235 | - items: page.items, | ||
| 236 | - ); | ||
| 237 | - storedCount += pageStoredCount; | ||
| 238 | - _log( | ||
| 239 | - 'page_stored dataType=$dataType ' | ||
| 240 | - 'segment=${rangeIndex + 1}/${fetchRanges.length} ' | ||
| 241 | - 'fetchedItems=${page.items.length} ' | ||
| 242 | - 'storedItems=$pageStoredCount totalStored=$storedCount', | ||
| 243 | - ); | ||
| 244 | - } | ||
| 245 | - | ||
| 246 | - pageToken = page.nextPageToken; | ||
| 247 | - } while (pageToken != null && pageToken.isNotEmpty); | 335 | + } |
| 248 | 336 | ||
| 249 | _log( | 337 | _log( |
| 250 | - 'segment_finish dataType=$dataType ' | ||
| 251 | - 'segment=${rangeIndex + 1}/${fetchRanges.length}', | 338 | + 'segment_finish dataType=${fetched.dataType} ' |
| 339 | + 'segment=${segment.segmentIndex + 1}/${fetched.segmentCount}', | ||
| 252 | ); | 340 | ); |
| 253 | } | 341 | } |
| 254 | 342 | ||
| 255 | _log( | 343 | _log( |
| 256 | - 'sync_finish dataType=$dataType ' | ||
| 257 | - 'startTime=$startTime endTime=$endTime ' | ||
| 258 | - 'chunkDays=$fetchChunkDays rangeCount=${fetchRanges.length} ' | 344 | + 'sync_finish dataType=${fetched.dataType} ' |
| 345 | + 'startTime=${fetched.startTime} endTime=${fetched.endTime} ' | ||
| 346 | + 'chunkDays=${fetched.fetchChunkDays} rangeCount=${fetched.segmentCount} ' | ||
| 259 | 'pageCount=$pageCount ' | 347 | 'pageCount=$pageCount ' |
| 260 | 'storedCount=$storedCount', | 348 | 'storedCount=$storedCount', |
| 261 | ); | 349 | ); |
| 262 | 350 | ||
| 263 | return OhosHealthRawDataSyncResult( | 351 | return OhosHealthRawDataSyncResult( |
| 264 | - dataType: dataType, | ||
| 265 | - startTime: startTime, | ||
| 266 | - endTime: endTime, | ||
| 267 | - segmentCount: fetchRanges.length, | 352 | + dataType: fetched.dataType, |
| 353 | + startTime: fetched.startTime, | ||
| 354 | + endTime: fetched.endTime, | ||
| 355 | + segmentCount: fetched.segmentCount, | ||
| 268 | pageCount: pageCount, | 356 | pageCount: pageCount, |
| 269 | storedCount: storedCount, | 357 | storedCount: storedCount, |
| 270 | ); | 358 | ); |
| @@ -350,7 +438,6 @@ abstract class OhosHealthRawDataRemoteDataSource { | @@ -350,7 +438,6 @@ abstract class OhosHealthRawDataRemoteDataSource { | ||
| 350 | required int dataType, | 438 | required int dataType, |
| 351 | required int startTime, | 439 | required int startTime, |
| 352 | required int endTime, | 440 | required int endTime, |
| 353 | - String? pageToken, | ||
| 354 | }); | 441 | }); |
| 355 | 442 | ||
| 356 | Future<V2ActivityTarget?> fetchActivityGoal(); | 443 | Future<V2ActivityTarget?> fetchActivityGoal(); |
| @@ -410,7 +497,6 @@ class OhosHarmonyHealthRawDataRemoteDataSource | @@ -410,7 +497,6 @@ class OhosHarmonyHealthRawDataRemoteDataSource | ||
| 410 | required int dataType, | 497 | required int dataType, |
| 411 | required int startTime, | 498 | required int startTime, |
| 412 | required int endTime, | 499 | required int endTime, |
| 413 | - String? pageToken, | ||
| 414 | }) async { | 500 | }) async { |
| 415 | final startDate = _dateKeyFromUnixSeconds(startTime); | 501 | final startDate = _dateKeyFromUnixSeconds(startTime); |
| 416 | final endDate = _endDateKeyFromUnixSeconds(endTime); | 502 | final endDate = _endDateKeyFromUnixSeconds(endTime); |
| @@ -552,7 +638,6 @@ class TodoOhosHealthRawDataRemoteDataSource | @@ -552,7 +638,6 @@ class TodoOhosHealthRawDataRemoteDataSource | ||
| 552 | required int dataType, | 638 | required int dataType, |
| 553 | required int startTime, | 639 | required int startTime, |
| 554 | required int endTime, | 640 | required int endTime, |
| 555 | - String? pageToken, | ||
| 556 | }) async { | 641 | }) async { |
| 557 | // TODO: Call the OHOS raw-data API after the endpoint and response schema | 642 | // TODO: Call the OHOS raw-data API after the endpoint and response schema |
| 558 | // are available. | 643 | // are available. |
| @@ -607,11 +692,9 @@ class TodoOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { | @@ -607,11 +692,9 @@ class TodoOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { | ||
| 607 | class OhosHealthRawDataPage { | 692 | class OhosHealthRawDataPage { |
| 608 | const OhosHealthRawDataPage({ | 693 | const OhosHealthRawDataPage({ |
| 609 | required this.items, | 694 | required this.items, |
| 610 | - this.nextPageToken, | ||
| 611 | }); | 695 | }); |
| 612 | 696 | ||
| 613 | final List<OhosHealthRawDataItem> items; | 697 | final List<OhosHealthRawDataItem> items; |
| 614 | - final String? nextPageToken; | ||
| 615 | } | 698 | } |
| 616 | 699 | ||
| 617 | class OhosHealthRawDataItem { | 700 | class OhosHealthRawDataItem { |
| @@ -636,6 +719,36 @@ class OhosHealthRawDataFetchRange { | @@ -636,6 +719,36 @@ class OhosHealthRawDataFetchRange { | ||
| 636 | final int endTime; | 719 | final int endTime; |
| 637 | } | 720 | } |
| 638 | 721 | ||
| 722 | +class _FetchedRawDataSync { | ||
| 723 | + const _FetchedRawDataSync({ | ||
| 724 | + required this.dataType, | ||
| 725 | + required this.startTime, | ||
| 726 | + required this.endTime, | ||
| 727 | + required this.fetchChunkDays, | ||
| 728 | + required this.segments, | ||
| 729 | + }); | ||
| 730 | + | ||
| 731 | + final int dataType; | ||
| 732 | + final int startTime; | ||
| 733 | + final int endTime; | ||
| 734 | + final int fetchChunkDays; | ||
| 735 | + final List<_FetchedRawDataSegment> segments; | ||
| 736 | + | ||
| 737 | + int get segmentCount => segments.length; | ||
| 738 | +} | ||
| 739 | + | ||
| 740 | +class _FetchedRawDataSegment { | ||
| 741 | + const _FetchedRawDataSegment({ | ||
| 742 | + required this.segmentIndex, | ||
| 743 | + required this.range, | ||
| 744 | + required this.page, | ||
| 745 | + }); | ||
| 746 | + | ||
| 747 | + final int segmentIndex; | ||
| 748 | + final OhosHealthRawDataFetchRange range; | ||
| 749 | + final OhosHealthRawDataPage page; | ||
| 750 | +} | ||
| 751 | + | ||
| 639 | class OhosHealthRawDataSyncResult { | 752 | class OhosHealthRawDataSyncResult { |
| 640 | const OhosHealthRawDataSyncResult({ | 753 | const OhosHealthRawDataSyncResult({ |
| 641 | required this.dataType, | 754 | required this.dataType, |
| @@ -109,10 +109,10 @@ packages: | @@ -109,10 +109,10 @@ packages: | ||
| 109 | dependency: transitive | 109 | dependency: transitive |
| 110 | description: | 110 | description: |
| 111 | name: built_value | 111 | name: built_value |
| 112 | - sha256: "31b24be6615ec7fcf70b3aa5a7469fe35826485e639a16dd7eb83ba30e4cc6a8" | 112 | + sha256: f87ea98192116f7093cb214551ce1929caae0681fdba282b3d8b4462adee7bb7 |
| 113 | url: "https://pub.flutter-io.cn" | 113 | url: "https://pub.flutter-io.cn" |
| 114 | source: hosted | 114 | source: hosted |
| 115 | - version: "8.12.7" | 115 | + version: "8.13.0" |
| 116 | cached_network_image: | 116 | cached_network_image: |
| 117 | dependency: "direct main" | 117 | dependency: "direct main" |
| 118 | description: | 118 | description: |
| @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_dat | @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_dat | ||
| 2 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart'; | 2 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart'; |
| 3 | import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_data_core_service.dart'; | 3 | import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_data_core_service.dart'; |
| 4 | import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/ohos_health_raw_data_core_service.dart'; | 4 | import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/ohos_health_raw_data_core_service.dart'; |
| 5 | +import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/ohos_health_raw_data_sync_service.dart'; | ||
| 5 | import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; | 6 | import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; |
| 6 | import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; | 7 | import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; |
| 7 | import 'package:flutter_test/flutter_test.dart'; | 8 | import 'package:flutter_test/flutter_test.dart'; |
| @@ -54,6 +55,93 @@ void main() { | @@ -54,6 +55,93 @@ void main() { | ||
| 54 | expect(store.dailyStressPoints, hasLength(1)); | 55 | expect(store.dailyStressPoints, hasLength(1)); |
| 55 | expect(store.sleepResults, hasLength(1)); | 56 | expect(store.sleepResults, hasLength(1)); |
| 56 | }); | 57 | }); |
| 58 | + | ||
| 59 | + test('OHOS core aborts calculation and shows debug toast when sync fails', | ||
| 60 | + () async { | ||
| 61 | + final store = _FakeHealthRawStressLocalStore(); | ||
| 62 | + final toasts = <String>[]; | ||
| 63 | + final service = OHOSHealthRawDataCoreService( | ||
| 64 | + rawDataSource: OhosHealthRawDataSource( | ||
| 65 | + syncService: OhosHealthRawDataSyncService( | ||
| 66 | + remoteDataSource: _FailingOhosHealthRawDataRemoteDataSource(), | ||
| 67 | + localStore: _FakeOhosHealthRawDataLocalStore(), | ||
| 68 | + ), | ||
| 69 | + ), | ||
| 70 | + localStore: store, | ||
| 71 | + userIdProvider: () => 42, | ||
| 72 | + uploadResultsAfterCalculation: false, | ||
| 73 | + healthReadAuthorizationChecker: () async => true, | ||
| 74 | + debugModeProvider: () => true, | ||
| 75 | + toastSink: toasts.add, | ||
| 76 | + ); | ||
| 77 | + | ||
| 78 | + await expectLater( | ||
| 79 | + service.syncAndStore( | ||
| 80 | + startTime: DateTime.now() | ||
| 81 | + .subtract(const Duration(days: 1)) | ||
| 82 | + .millisecondsSinceEpoch ~/ | ||
| 83 | + 1000, | ||
| 84 | + endTime: DateTime.now().millisecondsSinceEpoch ~/ 1000, | ||
| 85 | + readChunkDays: 1, | ||
| 86 | + ), | ||
| 87 | + throwsA(isA<StateError>()), | ||
| 88 | + ); | ||
| 89 | + | ||
| 90 | + expect(toasts, ['数据同步失败,请刷新首页重试']); | ||
| 91 | + expect(store.hrvStressPoints, isEmpty); | ||
| 92 | + expect(store.realtimeStressPoints, isEmpty); | ||
| 93 | + expect(store.dailyStressPoints, isEmpty); | ||
| 94 | + expect(store.sleepResults, isEmpty); | ||
| 95 | + }); | ||
| 96 | +} | ||
| 97 | + | ||
| 98 | +class _FailingOhosHealthRawDataRemoteDataSource | ||
| 99 | + implements OhosHealthRawDataRemoteDataSource { | ||
| 100 | + @override | ||
| 101 | + Future<OhosHealthRawDataPage> fetchRawDataPage({ | ||
| 102 | + required int dataType, | ||
| 103 | + required int startTime, | ||
| 104 | + required int endTime, | ||
| 105 | + }) async { | ||
| 106 | + throw StateError('failed dataType=$dataType'); | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + @override | ||
| 110 | + Future<V2ActivityTarget?> fetchActivityGoal() async { | ||
| 111 | + return null; | ||
| 112 | + } | ||
| 113 | +} | ||
| 114 | + | ||
| 115 | +class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { | ||
| 116 | + @override | ||
| 117 | + Future<int?> latestDataTime({required int dataType}) async { | ||
| 118 | + return null; | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + @override | ||
| 122 | + Future<List<OhosHealthRawDataItem>> queryRawData({ | ||
| 123 | + required int dataType, | ||
| 124 | + required int startTime, | ||
| 125 | + required int endTime, | ||
| 126 | + }) async { | ||
| 127 | + return const <OhosHealthRawDataItem>[]; | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + @override | ||
| 131 | + Future<V2ActivityTarget?> queryActivityGoal() async { | ||
| 132 | + return null; | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + @override | ||
| 136 | + Future<void> upsertActivityGoal(V2ActivityTarget goal) async {} | ||
| 137 | + | ||
| 138 | + @override | ||
| 139 | + Future<int> upsertRawDataBatch({ | ||
| 140 | + required int dataType, | ||
| 141 | + required List<OhosHealthRawDataItem> items, | ||
| 142 | + }) async { | ||
| 143 | + return items.length; | ||
| 144 | + } | ||
| 57 | } | 145 | } |
| 58 | 146 | ||
| 59 | class _FakeHealthRawDataSource implements HealthRawDataSource { | 147 | class _FakeHealthRawDataSource implements HealthRawDataSource { |
| @@ -10,7 +10,7 @@ import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; | @@ -10,7 +10,7 @@ import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; | ||
| 10 | import 'package:flutter_test/flutter_test.dart'; | 10 | import 'package:flutter_test/flutter_test.dart'; |
| 11 | 11 | ||
| 12 | void main() { | 12 | void main() { |
| 13 | - test('syncRawData starts from latest local data time and stores each page', | 13 | + test('syncRawData starts from latest local data time and stores fetched data', |
| 14 | () async { | 14 | () async { |
| 15 | final remote = _FakeOhosHealthRawDataRemoteDataSource([ | 15 | final remote = _FakeOhosHealthRawDataRemoteDataSource([ |
| 16 | const OhosHealthRawDataPage( | 16 | const OhosHealthRawDataPage( |
| @@ -20,11 +20,6 @@ void main() { | @@ -20,11 +20,6 @@ void main() { | ||
| 20 | dataTime: 1001, | 20 | dataTime: 1001, |
| 21 | payload: {'value': 70}, | 21 | payload: {'value': 70}, |
| 22 | ), | 22 | ), |
| 23 | - ], | ||
| 24 | - nextPageToken: 'next', | ||
| 25 | - ), | ||
| 26 | - const OhosHealthRawDataPage( | ||
| 27 | - items: [ | ||
| 28 | OhosHealthRawDataItem( | 23 | OhosHealthRawDataItem( |
| 29 | dataType: 2, | 24 | dataType: 2, |
| 30 | dataTime: 1002, | 25 | dataTime: 1002, |
| @@ -46,11 +41,10 @@ void main() { | @@ -46,11 +41,10 @@ void main() { | ||
| 46 | expect(result.startTime, expectedStart); | 41 | expect(result.startTime, expectedStart); |
| 47 | expect(result.endTime, 2000); | 42 | expect(result.endTime, 2000); |
| 48 | expect(result.segmentCount, 1); | 43 | expect(result.segmentCount, 1); |
| 49 | - expect(result.pageCount, 2); | 44 | + expect(result.pageCount, 1); |
| 50 | expect(result.storedCount, 2); | 45 | expect(result.storedCount, 2); |
| 51 | - expect(remote.calls.map((e) => e.pageToken), [null, 'next']); | ||
| 52 | expect(remote.calls.every((e) => e.startTime == expectedStart), isTrue); | 46 | expect(remote.calls.every((e) => e.startTime == expectedStart), isTrue); |
| 53 | - expect(local.storedBatches.map((e) => e.length), [1, 1]); | 47 | + expect(local.storedBatches.map((e) => e.length), [2]); |
| 54 | }); | 48 | }); |
| 55 | 49 | ||
| 56 | test('syncRawData falls back to half-year lookback when local data is empty', | 50 | test('syncRawData falls back to half-year lookback when local data is empty', |
| @@ -212,6 +206,38 @@ void main() { | @@ -212,6 +206,38 @@ void main() { | ||
| 212 | ); | 206 | ); |
| 213 | }); | 207 | }); |
| 214 | 208 | ||
| 209 | + test('syncRawData fetches split ranges concurrently before storing', | ||
| 210 | + () async { | ||
| 211 | + final start = _unixSeconds(DateTime(2026, 1, 30, 10)); | ||
| 212 | + final end = _unixSeconds(DateTime(2026, 2, 22, 9)); | ||
| 213 | + final gate = Completer<void>(); | ||
| 214 | + final remote = _FakeOhosHealthRawDataRemoteDataSource( | ||
| 215 | + const <OhosHealthRawDataPage>[], | ||
| 216 | + gate: gate, | ||
| 217 | + ); | ||
| 218 | + final local = _FakeOhosHealthRawDataLocalStore(); | ||
| 219 | + final service = OhosHealthRawDataSyncService( | ||
| 220 | + remoteDataSource: remote, | ||
| 221 | + localStore: local, | ||
| 222 | + ); | ||
| 223 | + | ||
| 224 | + final sync = service.syncRawData( | ||
| 225 | + dataType: HuaweiHealthDataType.heartRate.dataType, | ||
| 226 | + startTime: start, | ||
| 227 | + endTime: end, | ||
| 228 | + ); | ||
| 229 | + await Future<void>.delayed(Duration.zero); | ||
| 230 | + | ||
| 231 | + expect(remote.calls, hasLength(3)); | ||
| 232 | + expect(local.storedBatches, isEmpty); | ||
| 233 | + | ||
| 234 | + gate.complete(); | ||
| 235 | + final result = await sync; | ||
| 236 | + | ||
| 237 | + expect(result.segmentCount, 3); | ||
| 238 | + expect(result.pageCount, 3); | ||
| 239 | + }); | ||
| 240 | + | ||
| 215 | test('syncRawData joins duplicate in-flight syncs', () async { | 241 | test('syncRawData joins duplicate in-flight syncs', () async { |
| 216 | final gate = Completer<void>(); | 242 | final gate = Completer<void>(); |
| 217 | final remote = _FakeOhosHealthRawDataRemoteDataSource( | 243 | final remote = _FakeOhosHealthRawDataRemoteDataSource( |
| @@ -318,6 +344,81 @@ void main() { | @@ -318,6 +344,81 @@ void main() { | ||
| 318 | expect(logs, contains(contains('calculation_sync_finish'))); | 344 | expect(logs, contains(contains('calculation_sync_finish'))); |
| 319 | }); | 345 | }); |
| 320 | 346 | ||
| 347 | + test('syncCalculationRawData fetches data types concurrently', () async { | ||
| 348 | + final gate = Completer<void>(); | ||
| 349 | + final remote = _FakeOhosHealthRawDataRemoteDataSource( | ||
| 350 | + const <OhosHealthRawDataPage>[], | ||
| 351 | + gate: gate, | ||
| 352 | + ); | ||
| 353 | + final local = _FakeOhosHealthRawDataLocalStore(); | ||
| 354 | + final service = OhosHealthRawDataSyncService( | ||
| 355 | + remoteDataSource: remote, | ||
| 356 | + localStore: local, | ||
| 357 | + ); | ||
| 358 | + | ||
| 359 | + final sync = service.syncCalculationRawData( | ||
| 360 | + startTime: 1000, | ||
| 361 | + endTime: 2000, | ||
| 362 | + dataTypes: const <int>[1, 2, OhosHealthRawDataType.sleepAnalysis], | ||
| 363 | + ); | ||
| 364 | + await Future<void>.delayed(Duration.zero); | ||
| 365 | + | ||
| 366 | + expect(remote.calls.map((call) => call.dataType).toSet(), { | ||
| 367 | + 1, | ||
| 368 | + 2, | ||
| 369 | + OhosHealthRawDataType.sleepAnalysis, | ||
| 370 | + }); | ||
| 371 | + expect(local.storedBatches, isEmpty); | ||
| 372 | + | ||
| 373 | + gate.complete(); | ||
| 374 | + final results = await sync; | ||
| 375 | + | ||
| 376 | + expect(results.map((result) => result.dataType), [ | ||
| 377 | + 1, | ||
| 378 | + 2, | ||
| 379 | + OhosHealthRawDataType.sleepAnalysis, | ||
| 380 | + ]); | ||
| 381 | + }); | ||
| 382 | + | ||
| 383 | + test( | ||
| 384 | + 'syncCalculationRawData does not store raw data when activity goal fails', | ||
| 385 | + () async { | ||
| 386 | + final remote = _FakeOhosHealthRawDataRemoteDataSource( | ||
| 387 | + const <OhosHealthRawDataPage>[ | ||
| 388 | + OhosHealthRawDataPage( | ||
| 389 | + items: [ | ||
| 390 | + OhosHealthRawDataItem( | ||
| 391 | + dataType: 1, | ||
| 392 | + dataTime: 1001, | ||
| 393 | + payload: {'value': 45}, | ||
| 394 | + ), | ||
| 395 | + ], | ||
| 396 | + ), | ||
| 397 | + ], | ||
| 398 | + failingActivityGoal: true, | ||
| 399 | + ); | ||
| 400 | + final local = _FakeOhosHealthRawDataLocalStore(); | ||
| 401 | + final logs = <String>[]; | ||
| 402 | + final service = OhosHealthRawDataSyncService( | ||
| 403 | + remoteDataSource: remote, | ||
| 404 | + localStore: local, | ||
| 405 | + logSink: logs.add, | ||
| 406 | + ); | ||
| 407 | + | ||
| 408 | + await expectLater( | ||
| 409 | + service.syncCalculationRawData( | ||
| 410 | + startTime: 1000, | ||
| 411 | + endTime: 2000, | ||
| 412 | + dataTypes: const <int>[1], | ||
| 413 | + ), | ||
| 414 | + throwsA(isA<StateError>()), | ||
| 415 | + ); | ||
| 416 | + | ||
| 417 | + expect(local.storedBatches, isEmpty); | ||
| 418 | + expect(local.activityGoal, isNull); | ||
| 419 | + expect(logs, contains(contains('calculation_sync_failed'))); | ||
| 420 | + }); | ||
| 421 | + | ||
| 321 | test('Harmony remote data source calls getHealthData and converts items', | 422 | test('Harmony remote data source calls getHealthData and converts items', |
| 322 | () async { | 423 | () async { |
| 323 | final client = _FakeOhosHarmonyRawDataClient( | 424 | final client = _FakeOhosHarmonyRawDataClient( |
| @@ -350,7 +451,6 @@ void main() { | @@ -350,7 +451,6 @@ void main() { | ||
| 350 | expect(client.healthCalls.single.dataType, HuaweiHealthDataType.hrv); | 451 | expect(client.healthCalls.single.dataType, HuaweiHealthDataType.hrv); |
| 351 | expect(client.healthCalls.single.startDate, 20260801); | 452 | expect(client.healthCalls.single.startDate, 20260801); |
| 352 | expect(client.healthCalls.single.endDate, 20260831); | 453 | expect(client.healthCalls.single.endDate, 20260831); |
| 353 | - expect(page.nextPageToken, isNull); | ||
| 354 | expect(page.items, hasLength(1)); | 454 | expect(page.items, hasLength(1)); |
| 355 | expect(page.items.single.dataType, 1); | 455 | expect(page.items.single.dataType, 1); |
| 356 | expect(page.items.single.dataTime, 1787900000); | 456 | expect(page.items.single.dataTime, 1787900000); |
| @@ -470,14 +570,17 @@ class _FakeOhosHealthRawDataRemoteDataSource | @@ -470,14 +570,17 @@ class _FakeOhosHealthRawDataRemoteDataSource | ||
| 470 | Completer<void>? gate, | 570 | Completer<void>? gate, |
| 471 | Set<int> failingDataTypes = const <int>{}, | 571 | Set<int> failingDataTypes = const <int>{}, |
| 472 | V2ActivityTarget? activityGoal, | 572 | V2ActivityTarget? activityGoal, |
| 573 | + bool failingActivityGoal = false, | ||
| 473 | }) : _gate = gate, | 574 | }) : _gate = gate, |
| 474 | _failingDataTypes = failingDataTypes, | 575 | _failingDataTypes = failingDataTypes, |
| 475 | - _activityGoal = activityGoal; | 576 | + _activityGoal = activityGoal, |
| 577 | + _failingActivityGoal = failingActivityGoal; | ||
| 476 | 578 | ||
| 477 | final List<OhosHealthRawDataPage> _pages; | 579 | final List<OhosHealthRawDataPage> _pages; |
| 478 | final Completer<void>? _gate; | 580 | final Completer<void>? _gate; |
| 479 | final Set<int> _failingDataTypes; | 581 | final Set<int> _failingDataTypes; |
| 480 | final V2ActivityTarget? _activityGoal; | 582 | final V2ActivityTarget? _activityGoal; |
| 583 | + final bool _failingActivityGoal; | ||
| 481 | final List<_FetchCall> calls = <_FetchCall>[]; | 584 | final List<_FetchCall> calls = <_FetchCall>[]; |
| 482 | 585 | ||
| 483 | @override | 586 | @override |
| @@ -485,14 +588,12 @@ class _FakeOhosHealthRawDataRemoteDataSource | @@ -485,14 +588,12 @@ class _FakeOhosHealthRawDataRemoteDataSource | ||
| 485 | required int dataType, | 588 | required int dataType, |
| 486 | required int startTime, | 589 | required int startTime, |
| 487 | required int endTime, | 590 | required int endTime, |
| 488 | - String? pageToken, | ||
| 489 | }) async { | 591 | }) async { |
| 490 | calls.add( | 592 | calls.add( |
| 491 | _FetchCall( | 593 | _FetchCall( |
| 492 | dataType: dataType, | 594 | dataType: dataType, |
| 493 | startTime: startTime, | 595 | startTime: startTime, |
| 494 | endTime: endTime, | 596 | endTime: endTime, |
| 495 | - pageToken: pageToken, | ||
| 496 | ), | 597 | ), |
| 497 | ); | 598 | ); |
| 498 | if (_gate != null) { | 599 | if (_gate != null) { |
| @@ -510,6 +611,9 @@ class _FakeOhosHealthRawDataRemoteDataSource | @@ -510,6 +611,9 @@ class _FakeOhosHealthRawDataRemoteDataSource | ||
| 510 | 611 | ||
| 511 | @override | 612 | @override |
| 512 | Future<V2ActivityTarget?> fetchActivityGoal() async { | 613 | Future<V2ActivityTarget?> fetchActivityGoal() async { |
| 614 | + if (_failingActivityGoal) { | ||
| 615 | + throw StateError('failed activity goal'); | ||
| 616 | + } | ||
| 513 | return _activityGoal; | 617 | return _activityGoal; |
| 514 | } | 618 | } |
| 515 | } | 619 | } |
| @@ -570,13 +674,11 @@ class _FetchCall { | @@ -570,13 +674,11 @@ class _FetchCall { | ||
| 570 | required this.dataType, | 674 | required this.dataType, |
| 571 | required this.startTime, | 675 | required this.startTime, |
| 572 | required this.endTime, | 676 | required this.endTime, |
| 573 | - required this.pageToken, | ||
| 574 | }); | 677 | }); |
| 575 | 678 | ||
| 576 | final int dataType; | 679 | final int dataType; |
| 577 | final int startTime; | 680 | final int startTime; |
| 578 | final int endTime; | 681 | final int endTime; |
| 579 | - final String? pageToken; | ||
| 580 | } | 682 | } |
| 581 | 683 | ||
| 582 | class _FakeOhosHarmonyRawDataClient implements OhosHarmonyRawDataClient { | 684 | class _FakeOhosHarmonyRawDataClient implements OhosHarmonyRawDataClient { |
-
Please register or login to post a comment