Showing
7 changed files
with
310 additions
and
25 deletions
| @@ -191,6 +191,10 @@ class OhosHealthRawDataSource implements HealthRawDataSource { | @@ -191,6 +191,10 @@ class OhosHealthRawDataSource implements HealthRawDataSource { | ||
| 191 | ); | 191 | ); |
| 192 | } | 192 | } |
| 193 | 193 | ||
| 194 | + Future<int?> latestDataTime({required int dataType}) { | ||
| 195 | + return _syncService.latestDataTime(dataType: dataType); | ||
| 196 | + } | ||
| 197 | + | ||
| 194 | @override | 198 | @override |
| 195 | Future<V2ActivityTarget?> getActivityGoal({bool refresh = true}) { | 199 | Future<V2ActivityTarget?> getActivityGoal({bool refresh = true}) { |
| 196 | return _syncService.getActivityGoal(refresh: refresh); | 200 | return _syncService.getActivityGoal(refresh: refresh); |
| @@ -243,11 +243,17 @@ class OHOSHealthRawDataCoreService { | @@ -243,11 +243,17 @@ class OHOSHealthRawDataCoreService { | ||
| 243 | throw ArgumentError.value(endTime, 'endTime'); | 243 | throw ArgumentError.value(endTime, 'endTime'); |
| 244 | } | 244 | } |
| 245 | final hasAuthorization = await _hasHealthReadAuthorizationSafely(); | 245 | final hasAuthorization = await _hasHealthReadAuthorizationSafely(); |
| 246 | + final willSyncRawData = | ||
| 247 | + hasAuthorization && _rawDataSource is OhosHealthRawDataSource; | ||
| 248 | + final syncStartTime = willSyncRawData ? DateTime.now() : null; | ||
| 246 | final syncResults = await _syncCalculationRawDataSafely( | 249 | final syncResults = await _syncCalculationRawDataSafely( |
| 247 | hasAuthorization: hasAuthorization, | 250 | hasAuthorization: hasAuthorization, |
| 248 | startTime: forceStartTime, | 251 | startTime: forceStartTime, |
| 249 | endTime: effectiveEndTime, | 252 | endTime: effectiveEndTime, |
| 250 | ); | 253 | ); |
| 254 | + final syncElapsed = syncStartTime == null | ||
| 255 | + ? Duration.zero | ||
| 256 | + : DateTime.now().difference(syncStartTime); | ||
| 251 | final syncedRawStartTime = _earliestStoredTime(syncResults); | 257 | final syncedRawStartTime = _earliestStoredTime(syncResults); |
| 252 | _logInfo( | 258 | _logInfo( |
| 253 | 'calculate_sync_finish startTime=$requestedStartTime ' | 259 | 'calculate_sync_finish startTime=$requestedStartTime ' |
| @@ -255,6 +261,7 @@ class OHOSHealthRawDataCoreService { | @@ -255,6 +261,7 @@ class OHOSHealthRawDataCoreService { | ||
| 255 | 'syncResults=$syncResults', | 261 | 'syncResults=$syncResults', |
| 256 | ); | 262 | ); |
| 257 | 263 | ||
| 264 | + final calculationStartTime = DateTime.now(); | ||
| 258 | final storedResult = await _calculateAndStoreSafely( | 265 | final storedResult = await _calculateAndStoreSafely( |
| 259 | userId: userId, | 266 | userId: userId, |
| 260 | requestedStartTime: requestedStartTime, | 267 | requestedStartTime: requestedStartTime, |
| @@ -263,12 +270,17 @@ class OHOSHealthRawDataCoreService { | @@ -263,12 +270,17 @@ class OHOSHealthRawDataCoreService { | ||
| 263 | syncedRawStartTime: syncedRawStartTime, | 270 | syncedRawStartTime: syncedRawStartTime, |
| 264 | readChunkDays: readChunkDays, | 271 | readChunkDays: readChunkDays, |
| 265 | ); | 272 | ); |
| 273 | + final calculationElapsed = DateTime.now().difference(calculationStartTime); | ||
| 266 | _scheduleResultUpload(); | 274 | _scheduleResultUpload(); |
| 267 | await _sendLocalNotificationsAfterCalculationSafely( | 275 | await _sendLocalNotificationsAfterCalculationSafely( |
| 268 | result: storedResult.result, | 276 | result: storedResult.result, |
| 269 | hasExistingHrv: storedResult.hasExistingHrv, | 277 | hasExistingHrv: storedResult.hasExistingHrv, |
| 270 | hasExistingSleep: storedResult.hasExistingSleep, | 278 | hasExistingSleep: storedResult.hasExistingSleep, |
| 271 | ); | 279 | ); |
| 280 | + _showDebugTimingToast( | ||
| 281 | + syncElapsed: syncElapsed, | ||
| 282 | + calculationElapsed: calculationElapsed, | ||
| 283 | + ); | ||
| 272 | return storedResult.result; | 284 | return storedResult.result; |
| 273 | } | 285 | } |
| 274 | 286 | ||
| @@ -293,6 +305,15 @@ class OHOSHealthRawDataCoreService { | @@ -293,6 +305,15 @@ class OHOSHealthRawDataCoreService { | ||
| 293 | final latestSleepResultTime = await _localStore.latestSleepResultTime( | 305 | final latestSleepResultTime = await _localStore.latestSleepResultTime( |
| 294 | userId, | 306 | userId, |
| 295 | ); | 307 | ); |
| 308 | + final latestRawHrvDataTime = await _latestOhosRawDataTime( | ||
| 309 | + HealthDataUploadType.hrv.type, | ||
| 310 | + ); | ||
| 311 | + final latestRawHrDataTime = await _latestOhosRawDataTime( | ||
| 312 | + HealthDataUploadType.heartRate.type, | ||
| 313 | + ); | ||
| 314 | + final latestRawSleepDataTime = await _latestOhosRawDataTime( | ||
| 315 | + OhosHealthRawDataType.sleepAnalysis, | ||
| 316 | + ); | ||
| 296 | hasExistingHrv = latestHrvRawEndTime != null; | 317 | hasExistingHrv = latestHrvRawEndTime != null; |
| 297 | hasExistingSleep = latestSleepResultTime != null; | 318 | hasExistingSleep = latestSleepResultTime != null; |
| 298 | 319 | ||
| @@ -301,13 +322,30 @@ class OHOSHealthRawDataCoreService { | @@ -301,13 +322,30 @@ class OHOSHealthRawDataCoreService { | ||
| 301 | requestedStartTime, | 322 | requestedStartTime, |
| 302 | earliestStartTime, | 323 | earliestStartTime, |
| 303 | ); | 324 | ); |
| 325 | + final hrvRawAnchorStart = _rawBackfillStartTime(latestRawHrvDataTime); | ||
| 326 | + final realtimeRawAnchorStart = _rawBackfillStartTime( | ||
| 327 | + latestRawHrDataTime, | ||
| 328 | + ); | ||
| 329 | + final sleepRawAnchorStart = _rawBackfillStartTime( | ||
| 330 | + latestRawSleepDataTime, | ||
| 331 | + ); | ||
| 304 | final hrvStartTime = math.max( | 332 | final hrvStartTime = math.max( |
| 305 | - _minNullable(hrvContextStart, calculationStartTime) ?? | 333 | + _minNullableValues([ |
| 334 | + hrvContextStart, | ||
| 335 | + hrvRawAnchorStart, | ||
| 336 | + syncedRawStartTime, | ||
| 337 | + requestedStartTime, | ||
| 338 | + ]) ?? | ||
| 306 | calculationStartTime, | 339 | calculationStartTime, |
| 307 | earliestStartTime, | 340 | earliestStartTime, |
| 308 | ); | 341 | ); |
| 309 | final realtimeStartTime = math.max( | 342 | final realtimeStartTime = math.max( |
| 310 | - _minNullable(realtimeContextStart, calculationStartTime) ?? | 343 | + _minNullableValues([ |
| 344 | + realtimeContextStart, | ||
| 345 | + realtimeRawAnchorStart, | ||
| 346 | + syncedRawStartTime, | ||
| 347 | + requestedStartTime, | ||
| 348 | + ]) ?? | ||
| 311 | calculationStartTime, | 349 | calculationStartTime, |
| 312 | earliestStartTime, | 350 | earliestStartTime, |
| 313 | ); | 351 | ); |
| @@ -319,7 +357,21 @@ class OHOSHealthRawDataCoreService { | @@ -319,7 +357,21 @@ class OHOSHealthRawDataCoreService { | ||
| 319 | ? calculationStartTime | 357 | ? calculationStartTime |
| 320 | : latestSleepResultTime - Duration.secondsPerDay; | 358 | : latestSleepResultTime - Duration.secondsPerDay; |
| 321 | final sleepStartTime = math.max( | 359 | final sleepStartTime = math.max( |
| 322 | - _minNullable(sleepAnchorTime, syncedRawStartTime) ?? sleepAnchorTime, | 360 | + _minNullableValues([ |
| 361 | + sleepAnchorTime, | ||
| 362 | + sleepRawAnchorStart, | ||
| 363 | + syncedRawStartTime, | ||
| 364 | + requestedStartTime, | ||
| 365 | + ]) ?? | ||
| 366 | + sleepAnchorTime, | ||
| 367 | + earliestStartTime, | ||
| 368 | + ); | ||
| 369 | + final hrvRecomputeStartTime = _boundedNullableStartTime( | ||
| 370 | + _minNullable(syncedRawStartTime, hrvRawAnchorStart), | ||
| 371 | + earliestStartTime, | ||
| 372 | + ); | ||
| 373 | + final realtimeRecomputeStartTime = _boundedNullableStartTime( | ||
| 374 | + _minNullable(syncedRawStartTime, realtimeRawAnchorStart), | ||
| 323 | earliestStartTime, | 375 | earliestStartTime, |
| 324 | ); | 376 | ); |
| 325 | _logInfo( | 377 | _logInfo( |
| @@ -331,8 +383,18 @@ class OHOSHealthRawDataCoreService { | @@ -331,8 +383,18 @@ class OHOSHealthRawDataCoreService { | ||
| 331 | 'latestHrvRawEndTime=$latestHrvRawEndTime ' | 383 | 'latestHrvRawEndTime=$latestHrvRawEndTime ' |
| 332 | 'latestRealtimeRawEndTime=$latestRealtimeRawEndTime ' | 384 | 'latestRealtimeRawEndTime=$latestRealtimeRawEndTime ' |
| 333 | 'latestSleepResultTime=$latestSleepResultTime ' | 385 | 'latestSleepResultTime=$latestSleepResultTime ' |
| 334 | - 'hrvStartTime=$hrvStartTime heartRateStartTime=$heartRateStartTime ' | ||
| 335 | - 'sleepStartTime=$sleepStartTime', | 386 | + 'latestRawHrvDataTime=$latestRawHrvDataTime ' |
| 387 | + 'latestRawHrDataTime=$latestRawHrDataTime ' | ||
| 388 | + 'latestRawSleepDataTime=$latestRawSleepDataTime ' | ||
| 389 | + 'hrvRawAnchorStart=$hrvRawAnchorStart ' | ||
| 390 | + 'realtimeRawAnchorStart=$realtimeRawAnchorStart ' | ||
| 391 | + 'sleepRawAnchorStart=$sleepRawAnchorStart ' | ||
| 392 | + 'hrvStartTime=$hrvStartTime ' | ||
| 393 | + 'realtimeStartTime=$realtimeStartTime ' | ||
| 394 | + 'heartRateStartTime=$heartRateStartTime ' | ||
| 395 | + 'sleepStartTime=$sleepStartTime ' | ||
| 396 | + 'hrvRecomputeStartTime=$hrvRecomputeStartTime ' | ||
| 397 | + 'realtimeRecomputeStartTime=$realtimeRecomputeStartTime', | ||
| 336 | ); | 398 | ); |
| 337 | 399 | ||
| 338 | final hrvPoints = await _fetchRawDataInChunks( | 400 | final hrvPoints = await _fetchRawDataInChunks( |
| @@ -402,12 +464,12 @@ class OHOSHealthRawDataCoreService { | @@ -402,12 +464,12 @@ class OHOSHealthRawDataCoreService { | ||
| 402 | hrvStressPoints: _filterNewHrvStressPoints( | 464 | hrvStressPoints: _filterNewHrvStressPoints( |
| 403 | result.hrvStressPoints, | 465 | result.hrvStressPoints, |
| 404 | latestHrvRawEndTime, | 466 | latestHrvRawEndTime, |
| 405 | - recomputeStartTime: syncedRawStartTime, | 467 | + recomputeStartTime: hrvRecomputeStartTime, |
| 406 | ), | 468 | ), |
| 407 | realtimeStressPoints: _filterNewRealtimeStressPoints( | 469 | realtimeStressPoints: _filterNewRealtimeStressPoints( |
| 408 | result.realtimeStressPoints, | 470 | result.realtimeStressPoints, |
| 409 | latestRealtimeRawEndTime, | 471 | latestRealtimeRawEndTime, |
| 410 | - recomputeStartTime: syncedRawStartTime, | 472 | + recomputeStartTime: realtimeRecomputeStartTime, |
| 411 | ), | 473 | ), |
| 412 | ); | 474 | ); |
| 413 | _logInfo( | 475 | _logInfo( |
| @@ -947,6 +1009,30 @@ class OHOSHealthRawDataCoreService { | @@ -947,6 +1009,30 @@ class OHOSHealthRawDataCoreService { | ||
| 947 | } | 1009 | } |
| 948 | } | 1010 | } |
| 949 | 1011 | ||
| 1012 | + Future<int?> _latestOhosRawDataTime(int dataType) async { | ||
| 1013 | + final rawDataSource = _rawDataSource; | ||
| 1014 | + if (rawDataSource is! OhosHealthRawDataSource) { | ||
| 1015 | + return null; | ||
| 1016 | + } | ||
| 1017 | + return rawDataSource.latestDataTime(dataType: dataType); | ||
| 1018 | + } | ||
| 1019 | + | ||
| 1020 | + int? _rawBackfillStartTime(int? latestDataTime) { | ||
| 1021 | + if (latestDataTime == null) return null; | ||
| 1022 | + final backfillTime = math.max( | ||
| 1023 | + 0, | ||
| 1024 | + latestDataTime - | ||
| 1025 | + OhosHealthRawDataSyncService.incrementalBackfillDays * | ||
| 1026 | + Duration.secondsPerDay, | ||
| 1027 | + ); | ||
| 1028 | + return _localDay(backfillTime).millisecondsSinceEpoch ~/ 1000; | ||
| 1029 | + } | ||
| 1030 | + | ||
| 1031 | + int? _boundedNullableStartTime(int? startTime, int earliestStartTime) { | ||
| 1032 | + if (startTime == null) return null; | ||
| 1033 | + return math.max(startTime, earliestStartTime); | ||
| 1034 | + } | ||
| 1035 | + | ||
| 950 | Future<bool> _hasHealthReadAuthorizationSafely() async { | 1036 | Future<bool> _hasHealthReadAuthorizationSafely() async { |
| 951 | try { | 1037 | try { |
| 952 | return await _hasHealthReadAuthorization(); | 1038 | return await _hasHealthReadAuthorization(); |
| @@ -1258,6 +1344,30 @@ class OHOSHealthRawDataCoreService { | @@ -1258,6 +1344,30 @@ class OHOSHealthRawDataCoreService { | ||
| 1258 | return math.min(a, b); | 1344 | return math.min(a, b); |
| 1259 | } | 1345 | } |
| 1260 | 1346 | ||
| 1347 | + static int? _minNullableValues(Iterable<int?> values) { | ||
| 1348 | + int? minValue; | ||
| 1349 | + for (final value in values) { | ||
| 1350 | + if (value == null) continue; | ||
| 1351 | + minValue = minValue == null ? value : math.min(minValue, value); | ||
| 1352 | + } | ||
| 1353 | + return minValue; | ||
| 1354 | + } | ||
| 1355 | + | ||
| 1356 | + void _showDebugTimingToast({ | ||
| 1357 | + required Duration syncElapsed, | ||
| 1358 | + required Duration calculationElapsed, | ||
| 1359 | + }) { | ||
| 1360 | + if (!_isDebug) return; | ||
| 1361 | + _toastSink?.call( | ||
| 1362 | + '【测试】本轮同步耗时${_elapsedSecondsText(syncElapsed)} s、' | ||
| 1363 | + '计算耗时${_elapsedSecondsText(calculationElapsed)} s', | ||
| 1364 | + ); | ||
| 1365 | + } | ||
| 1366 | + | ||
| 1367 | + String _elapsedSecondsText(Duration elapsed) { | ||
| 1368 | + return (elapsed.inMilliseconds / 1000).toStringAsFixed(1); | ||
| 1369 | + } | ||
| 1370 | + | ||
| 1261 | static int _dateKeyFromUnixSeconds(int seconds) { | 1371 | static int _dateKeyFromUnixSeconds(int seconds) { |
| 1262 | final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000); | 1372 | final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000); |
| 1263 | return _dateKeyFromDateTime(date); | 1373 | return _dateKeyFromDateTime(date); |
| @@ -50,22 +50,24 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore { | @@ -50,22 +50,24 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore { | ||
| 50 | if (dataType == OhosHealthRawDataType.sleepAnalysis) { | 50 | if (dataType == OhosHealthRawDataType.sleepAnalysis) { |
| 51 | final rows = await db.query( | 51 | final rows = await db.query( |
| 52 | sleepDataTable, | 52 | sleepDataTable, |
| 53 | - columns: ['from_time'], | ||
| 54 | - orderBy: 'from_time DESC', | 53 | + columns: ['to_time'], |
| 54 | + where: 'to_time > 0', | ||
| 55 | + orderBy: 'to_time DESC', | ||
| 55 | limit: 1, | 56 | limit: 1, |
| 56 | ); | 57 | ); |
| 57 | - return rows.isEmpty ? null : rows.first['from_time'] as int; | 58 | + return rows.isEmpty ? null : rows.first['to_time'] as int; |
| 58 | } | 59 | } |
| 59 | if (dataType == OhosHealthRawDataType.workout) { | 60 | if (dataType == OhosHealthRawDataType.workout) { |
| 60 | final table = _rawDataTable(dataType); | 61 | final table = _rawDataTable(dataType); |
| 61 | await _createRawIntervalDataTypeTable(db, table); | 62 | await _createRawIntervalDataTypeTable(db, table); |
| 62 | final rows = await db.query( | 63 | final rows = await db.query( |
| 63 | table, | 64 | table, |
| 64 | - columns: ['from_time'], | ||
| 65 | - orderBy: 'from_time DESC', | 65 | + columns: ['to_time'], |
| 66 | + where: 'to_time > 0', | ||
| 67 | + orderBy: 'to_time DESC', | ||
| 66 | limit: 1, | 68 | limit: 1, |
| 67 | ); | 69 | ); |
| 68 | - return rows.isEmpty ? null : rows.first['from_time'] as int; | 70 | + return rows.isEmpty ? null : rows.first['to_time'] as int; |
| 69 | } | 71 | } |
| 70 | 72 | ||
| 71 | final storedDataType = _storedHealthDataType(dataType); | 73 | final storedDataType = _storedHealthDataType(dataType); |
| @@ -74,6 +76,7 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore { | @@ -74,6 +76,7 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore { | ||
| 74 | final rows = await db.query( | 76 | final rows = await db.query( |
| 75 | table, | 77 | table, |
| 76 | columns: ['time'], | 78 | columns: ['time'], |
| 79 | + where: 'time > 0 AND value IS NOT NULL', | ||
| 77 | orderBy: 'time DESC', | 80 | orderBy: 'time DESC', |
| 78 | limit: 1, | 81 | limit: 1, |
| 79 | ); | 82 | ); |
| @@ -99,6 +99,10 @@ class OhosHealthRawDataSyncService { | @@ -99,6 +99,10 @@ class OhosHealthRawDataSyncService { | ||
| 99 | } | 99 | } |
| 100 | } | 100 | } |
| 101 | 101 | ||
| 102 | + Future<int?> latestDataTime({required int dataType}) { | ||
| 103 | + return _localStore.latestDataTime(dataType: dataType); | ||
| 104 | + } | ||
| 105 | + | ||
| 102 | Future<List<OhosHealthRawDataSyncResult>> syncCalculationRawData({ | 106 | Future<List<OhosHealthRawDataSyncResult>> syncCalculationRawData({ |
| 103 | int? startTime, | 107 | int? startTime, |
| 104 | int? endTime, | 108 | int? endTime, |
| @@ -221,18 +221,18 @@ packages: | @@ -221,18 +221,18 @@ packages: | ||
| 221 | dependency: "direct main" | 221 | dependency: "direct main" |
| 222 | description: | 222 | description: |
| 223 | name: dio | 223 | name: dio |
| 224 | - sha256: "0df44ebba85e503958eb75d07eedd3c86275a58c1d3eda2f2ce8f0a2c3abbb3c" | 224 | + sha256: "852ec3b48cc431ac04fff978413c541502b67ffc3e26921e74e3d994694192c1" |
| 225 | url: "https://pub.flutter-io.cn" | 225 | url: "https://pub.flutter-io.cn" |
| 226 | source: hosted | 226 | source: hosted |
| 227 | - version: "5.11.0" | 227 | + version: "5.11.1" |
| 228 | dio_web_adapter: | 228 | dio_web_adapter: |
| 229 | dependency: transitive | 229 | dependency: transitive |
| 230 | description: | 230 | description: |
| 231 | name: dio_web_adapter | 231 | name: dio_web_adapter |
| 232 | - sha256: "0786d0b7295a373de356fc0af4f6f1d0ab2844ed31b19dfc5e7556b70e24212c" | 232 | + sha256: "3a1b2cd7be71086f38504956e3ebcd2837288d231ff454bafa78021244102bfc" |
| 233 | url: "https://pub.flutter-io.cn" | 233 | url: "https://pub.flutter-io.cn" |
| 234 | source: hosted | 234 | source: hosted |
| 235 | - version: "2.2.1" | 235 | + version: "2.2.2" |
| 236 | equatable: | 236 | equatable: |
| 237 | dependency: transitive | 237 | dependency: transitive |
| 238 | description: | 238 | description: |
| @@ -3,6 +3,7 @@ import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_mod | @@ -3,6 +3,7 @@ import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_mod | ||
| 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/core/services/raw_data_service/platform_ohos/ohos_health_raw_data_sync_service.dart'; |
| 6 | +import 'package:doublefeel_flutter/data/models/enums/app_enums.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'; |
| 7 | import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; | 8 | import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; |
| 8 | import 'package:flutter_test/flutter_test.dart'; | 9 | import 'package:flutter_test/flutter_test.dart'; |
| @@ -248,6 +249,116 @@ void main() { | @@ -248,6 +249,116 @@ void main() { | ||
| 248 | store.dailyStressPoints.map((point) => point.date), contains(20260830)); | 249 | store.dailyStressPoints.map((point) => point.date), contains(20260830)); |
| 249 | }); | 250 | }); |
| 250 | 251 | ||
| 252 | + test('OHOS core backfills missing daily stress from local raw data anchor', | ||
| 253 | + () async { | ||
| 254 | + final backfillDay = DateTime(2026, 8, 30); | ||
| 255 | + final backfillBase = | ||
| 256 | + backfillDay.add(const Duration(hours: 10)).millisecondsSinceEpoch ~/ | ||
| 257 | + 1000; | ||
| 258 | + final latestBase = DateTime(2026, 9, 3, 10).millisecondsSinceEpoch ~/ 1000; | ||
| 259 | + final rawLocalStore = _FakeOhosHealthRawDataLocalStore( | ||
| 260 | + latestDataTimeByType: { | ||
| 261 | + HealthDataUploadType.hrv.type: latestBase, | ||
| 262 | + HealthDataUploadType.heartRate.type: latestBase, | ||
| 263 | + }, | ||
| 264 | + itemsByDataType: { | ||
| 265 | + HealthDataUploadType.hrv.type: [ | ||
| 266 | + _rawItem(HealthDataUploadType.hrv.type, backfillBase, 45), | ||
| 267 | + _rawItem(HealthDataUploadType.hrv.type, latestBase, 50), | ||
| 268 | + ], | ||
| 269 | + HealthDataUploadType.heartRate.type: [ | ||
| 270 | + for (var index = 0; index < 10; index++) | ||
| 271 | + _rawItem( | ||
| 272 | + HealthDataUploadType.heartRate.type, | ||
| 273 | + backfillBase + index * 6 * 60, | ||
| 274 | + 100, | ||
| 275 | + ), | ||
| 276 | + _rawItem(HealthDataUploadType.heartRate.type, latestBase, 80), | ||
| 277 | + ], | ||
| 278 | + }, | ||
| 279 | + ); | ||
| 280 | + final remote = _HistoricalBackfillRemoteDataSource( | ||
| 281 | + hrvItems: const <OhosHealthRawDataItem>[], | ||
| 282 | + heartRateItems: const <OhosHealthRawDataItem>[], | ||
| 283 | + ); | ||
| 284 | + final store = _FakeHealthRawStressLocalStore() | ||
| 285 | + ..hrvStressPoints.add(_existingHrvResult(latestBase)) | ||
| 286 | + ..realtimeStressPoints.add(_existingRealtimeResult(latestBase)); | ||
| 287 | + final service = OHOSHealthRawDataCoreService( | ||
| 288 | + rawDataSource: OhosHealthRawDataSource( | ||
| 289 | + syncService: OhosHealthRawDataSyncService( | ||
| 290 | + remoteDataSource: remote, | ||
| 291 | + localStore: rawLocalStore, | ||
| 292 | + ), | ||
| 293 | + ), | ||
| 294 | + localStore: store, | ||
| 295 | + userIdProvider: () => 42, | ||
| 296 | + uploadResultsAfterCalculation: false, | ||
| 297 | + healthReadAuthorizationChecker: () async => true, | ||
| 298 | + ); | ||
| 299 | + | ||
| 300 | + final result = await service.startCoreCaculate( | ||
| 301 | + endTime: latestBase + Duration.secondsPerHour, | ||
| 302 | + readChunkDays: 1, | ||
| 303 | + ); | ||
| 304 | + | ||
| 305 | + expect( | ||
| 306 | + remote.calls | ||
| 307 | + .where((call) => | ||
| 308 | + call.dataType == HealthDataUploadType.hrv.type || | ||
| 309 | + call.dataType == HealthDataUploadType.heartRate.type) | ||
| 310 | + .map((call) => _dateKey(call.startTime)), | ||
| 311 | + everyElement(lessThanOrEqualTo(20260827)), | ||
| 312 | + ); | ||
| 313 | + expect( | ||
| 314 | + store.hrvStressPoints.any((point) => point.rawEndTime == backfillBase), | ||
| 315 | + isTrue, | ||
| 316 | + ); | ||
| 317 | + expect( | ||
| 318 | + store.realtimeStressPoints.any( | ||
| 319 | + (point) => _dateKey(point.rawEndTime) == 20260830, | ||
| 320 | + ), | ||
| 321 | + isTrue, | ||
| 322 | + ); | ||
| 323 | + expect(result.dailyStressPoints.map((point) => point.date), | ||
| 324 | + contains(20260830)); | ||
| 325 | + }); | ||
| 326 | + | ||
| 327 | + test('OHOS core shows debug timing toast after sync and calculation', | ||
| 328 | + () async { | ||
| 329 | + final base = DateTime.now() | ||
| 330 | + .subtract(const Duration(days: 2)) | ||
| 331 | + .millisecondsSinceEpoch ~/ | ||
| 332 | + 1000; | ||
| 333 | + final toasts = <String>[]; | ||
| 334 | + final service = OHOSHealthRawDataCoreService( | ||
| 335 | + rawDataSource: _FakeHealthRawDataSource( | ||
| 336 | + pointsByDataType: { | ||
| 337 | + HealthDataUploadType.hrv.type: [_point(1, base, 60)], | ||
| 338 | + HealthDataUploadType.heartRate.type: [_point(2, base, 80)], | ||
| 339 | + }, | ||
| 340 | + ), | ||
| 341 | + localStore: _FakeHealthRawStressLocalStore(), | ||
| 342 | + userIdProvider: () => 42, | ||
| 343 | + uploadResultsAfterCalculation: false, | ||
| 344 | + healthReadAuthorizationChecker: () async => false, | ||
| 345 | + debugModeProvider: () => true, | ||
| 346 | + toastSink: toasts.add, | ||
| 347 | + ); | ||
| 348 | + | ||
| 349 | + await service.syncAndStore( | ||
| 350 | + startTime: base - Duration.secondsPerHour, | ||
| 351 | + endTime: base + Duration.secondsPerHour, | ||
| 352 | + readChunkDays: 1, | ||
| 353 | + ); | ||
| 354 | + | ||
| 355 | + expect(toasts, hasLength(1)); | ||
| 356 | + expect( | ||
| 357 | + toasts.single, | ||
| 358 | + matches(RegExp(r'^【测试】本轮同步耗时0\.0 s、计算耗时\d+\.\d s$')), | ||
| 359 | + ); | ||
| 360 | + }); | ||
| 361 | + | ||
| 251 | test('OHOS core aborts calculation and shows debug toast when sync fails', | 362 | test('OHOS core aborts calculation and shows debug toast when sync fails', |
| 252 | () async { | 363 | () async { |
| 253 | final store = _FakeHealthRawStressLocalStore(); | 364 | final store = _FakeHealthRawStressLocalStore(); |
| @@ -352,15 +463,23 @@ class _RemoteRawCall { | @@ -352,15 +463,23 @@ class _RemoteRawCall { | ||
| 352 | } | 463 | } |
| 353 | 464 | ||
| 354 | class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { | 465 | class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { |
| 355 | - _FakeOhosHealthRawDataLocalStore({int? latestDataTime}) | ||
| 356 | - : _latestDataTime = latestDataTime; | 466 | + _FakeOhosHealthRawDataLocalStore({ |
| 467 | + int? latestDataTime, | ||
| 468 | + Map<int, int>? latestDataTimeByType, | ||
| 469 | + Map<int, List<OhosHealthRawDataItem>>? itemsByDataType, | ||
| 470 | + }) : _latestDataTime = latestDataTime, | ||
| 471 | + _latestDataTimeByType = latestDataTimeByType ?? const <int, int>{}, | ||
| 472 | + _itemsByDataType = | ||
| 473 | + itemsByDataType ?? const <int, List<OhosHealthRawDataItem>>{}; | ||
| 357 | 474 | ||
| 358 | final int? _latestDataTime; | 475 | final int? _latestDataTime; |
| 476 | + final Map<int, int> _latestDataTimeByType; | ||
| 477 | + final Map<int, List<OhosHealthRawDataItem>> _itemsByDataType; | ||
| 359 | final storedBatches = <List<OhosHealthRawDataItem>>[]; | 478 | final storedBatches = <List<OhosHealthRawDataItem>>[]; |
| 360 | 479 | ||
| 361 | @override | 480 | @override |
| 362 | Future<int?> latestDataTime({required int dataType}) async { | 481 | Future<int?> latestDataTime({required int dataType}) async { |
| 363 | - return _latestDataTime; | 482 | + return _latestDataTimeByType[dataType] ?? _latestDataTime; |
| 364 | } | 483 | } |
| 365 | 484 | ||
| 366 | @override | 485 | @override |
| @@ -369,8 +488,10 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { | @@ -369,8 +488,10 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { | ||
| 369 | required int startTime, | 488 | required int startTime, |
| 370 | required int endTime, | 489 | required int endTime, |
| 371 | }) async { | 490 | }) async { |
| 372 | - return storedBatches | ||
| 373 | - .expand((batch) => batch) | 491 | + return [ |
| 492 | + ...(_itemsByDataType[dataType] ?? const <OhosHealthRawDataItem>[]), | ||
| 493 | + ...storedBatches.expand((batch) => batch), | ||
| 494 | + ] | ||
| 374 | .where( | 495 | .where( |
| 375 | (item) => | 496 | (item) => |
| 376 | item.dataType == dataType && | 497 | item.dataType == dataType && |
| @@ -77,6 +77,45 @@ void main() { | @@ -77,6 +77,45 @@ void main() { | ||
| 77 | expect(result.earliestStoredTime, 1788019232); | 77 | expect(result.earliestStoredTime, 1788019232); |
| 78 | }); | 78 | }); |
| 79 | 79 | ||
| 80 | + test('syncRawData uses data type specific latest anchors', () async { | ||
| 81 | + final latestHrv = _unixSeconds(DateTime(2026, 9, 3, 10)); | ||
| 82 | + final latestSleep = _unixSeconds(DateTime(2026, 9, 2, 23)); | ||
| 83 | + final latestWorkout = _unixSeconds(DateTime(2026, 9, 1, 8)); | ||
| 84 | + final remote = _FakeOhosHealthRawDataRemoteDataSource([ | ||
| 85 | + const OhosHealthRawDataPage(items: <OhosHealthRawDataItem>[]), | ||
| 86 | + const OhosHealthRawDataPage(items: <OhosHealthRawDataItem>[]), | ||
| 87 | + const OhosHealthRawDataPage(items: <OhosHealthRawDataItem>[]), | ||
| 88 | + ]); | ||
| 89 | + final local = _FakeOhosHealthRawDataLocalStore( | ||
| 90 | + latestDataTimeByType: { | ||
| 91 | + HuaweiHealthDataType.hrv.dataType: latestHrv, | ||
| 92 | + OhosHealthRawDataType.sleepAnalysis: latestSleep, | ||
| 93 | + OhosHealthRawDataType.workout: latestWorkout, | ||
| 94 | + }, | ||
| 95 | + ); | ||
| 96 | + final service = OhosHealthRawDataSyncService( | ||
| 97 | + remoteDataSource: remote, | ||
| 98 | + localStore: local, | ||
| 99 | + ); | ||
| 100 | + | ||
| 101 | + await service.syncRawData( | ||
| 102 | + dataType: HuaweiHealthDataType.hrv.dataType, | ||
| 103 | + endTime: _unixSeconds(DateTime(2026, 9, 4)), | ||
| 104 | + ); | ||
| 105 | + await service.syncRawData( | ||
| 106 | + dataType: OhosHealthRawDataType.sleepAnalysis, | ||
| 107 | + endTime: _unixSeconds(DateTime(2026, 9, 4)), | ||
| 108 | + ); | ||
| 109 | + await service.syncRawData( | ||
| 110 | + dataType: OhosHealthRawDataType.workout, | ||
| 111 | + endTime: _unixSeconds(DateTime(2026, 9, 4)), | ||
| 112 | + ); | ||
| 113 | + | ||
| 114 | + expect(_dateKey(remote.calls[0].startTime), 20260827); | ||
| 115 | + expect(_dateKey(remote.calls[1].startTime), 20260826); | ||
| 116 | + expect(_dateKey(remote.calls[2].startTime), 20260825); | ||
| 117 | + }); | ||
| 118 | + | ||
| 80 | test('syncRawData falls back to half-year lookback when local data is empty', | 119 | test('syncRawData falls back to half-year lookback when local data is empty', |
| 81 | () async { | 120 | () async { |
| 82 | final now = DateTime(2026, 8, 13, 12); | 121 | final now = DateTime(2026, 8, 13, 12); |
| @@ -668,17 +707,21 @@ class _FakeOhosHealthRawDataRemoteDataSource | @@ -668,17 +707,21 @@ class _FakeOhosHealthRawDataRemoteDataSource | ||
| 668 | } | 707 | } |
| 669 | 708 | ||
| 670 | class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { | 709 | class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { |
| 671 | - _FakeOhosHealthRawDataLocalStore({int? latestDataTime}) | ||
| 672 | - : _latestDataTime = latestDataTime; | 710 | + _FakeOhosHealthRawDataLocalStore({ |
| 711 | + int? latestDataTime, | ||
| 712 | + Map<int, int>? latestDataTimeByType, | ||
| 713 | + }) : _latestDataTime = latestDataTime, | ||
| 714 | + _latestDataTimeByType = latestDataTimeByType ?? const <int, int>{}; | ||
| 673 | 715 | ||
| 674 | final int? _latestDataTime; | 716 | final int? _latestDataTime; |
| 717 | + final Map<int, int> _latestDataTimeByType; | ||
| 675 | final List<List<OhosHealthRawDataItem>> storedBatches = | 718 | final List<List<OhosHealthRawDataItem>> storedBatches = |
| 676 | <List<OhosHealthRawDataItem>>[]; | 719 | <List<OhosHealthRawDataItem>>[]; |
| 677 | V2ActivityTarget? activityGoal; | 720 | V2ActivityTarget? activityGoal; |
| 678 | 721 | ||
| 679 | @override | 722 | @override |
| 680 | Future<int?> latestDataTime({required int dataType}) async { | 723 | Future<int?> latestDataTime({required int dataType}) async { |
| 681 | - return _latestDataTime; | 724 | + return _latestDataTimeByType[dataType] ?? _latestDataTime; |
| 682 | } | 725 | } |
| 683 | 726 | ||
| 684 | @override | 727 | @override |
-
Please register or login to post a comment