Commit a02eddf8e92a4c52d9f4a04290deec1ebcfe9e30

Authored by 权海
1 parent 18d3aed7

feat(ui):优化计算耗时、首页刷新

@@ -23,6 +23,9 @@ import '../health_sleep_calculator.dart'; @@ -23,6 +23,9 @@ import '../health_sleep_calculator.dart';
23 class AppleHealthRawDataCoreService { 23 class AppleHealthRawDataCoreService {
24 static const int defaultLookbackDays = 183; 24 static const int defaultLookbackDays = 183;
25 static const int defaultReadChunkDays = 7; 25 static const int defaultReadChunkDays = 7;
  26 + // Re-read enough history to rebuild the previous completed sleep when
  27 + // HealthKit delivers its stages after a newer sleep result was stored.
  28 + static const int sleepReconciliationDays = 2;
26 static String _executionEngineType = 'main'; 29 static String _executionEngineType = 'main';
27 30
28 static void setExecutionEngineType(String value) { 31 static void setExecutionEngineType(String value) {
@@ -341,9 +344,11 @@ class AppleHealthRawDataCoreService { @@ -341,9 +344,11 @@ class AppleHealthRawDataCoreService {
341 ); 344 );
342 final restingHeartRateStartTime = heartRateStartTime; 345 final restingHeartRateStartTime = heartRateStartTime;
343 final sleepStartTime = math.max( 346 final sleepStartTime = math.max(
344 - latestSleepResultTime == null  
345 - ? requestedStartTime  
346 - : latestSleepResultTime - Duration.secondsPerDay, 347 + forceStartTime ??
  348 + (latestSleepResultTime == null
  349 + ? requestedStartTime
  350 + : latestSleepResultTime -
  351 + sleepReconciliationDays * Duration.secondsPerDay),
347 earliestStartTime, 352 earliestStartTime,
348 ); 353 );
349 354
@@ -434,7 +439,6 @@ class AppleHealthRawDataCoreService { @@ -434,7 +439,6 @@ class AppleHealthRawDataCoreService {
434 final sleepResults = await _calculateAndStoreSleepResults( 439 final sleepResults = await _calculateAndStoreSleepResults(
435 userId: userId, 440 userId: userId,
436 sleepIntervals: sleepIntervals, 441 sleepIntervals: sleepIntervals,
437 - latestSleepResultTime: latestSleepResultTime,  
438 ); 442 );
439 _scheduleResultUpload('upload sleep results', _uploadSleepResults); 443 _scheduleResultUpload('upload sleep results', _uploadSleepResults);
440 final storedResult = newResult.copyWith( 444 final storedResult = newResult.copyWith(
@@ -1814,14 +1818,13 @@ class AppleHealthRawDataCoreService { @@ -1814,14 +1818,13 @@ class AppleHealthRawDataCoreService {
1814 Future<List<HealthRawSleepResult>> _calculateAndStoreSleepResults({ 1818 Future<List<HealthRawSleepResult>> _calculateAndStoreSleepResults({
1815 required int userId, 1819 required int userId,
1816 required List<HealthKitRawDataPoint> sleepIntervals, 1820 required List<HealthKitRawDataPoint> sleepIntervals,
1817 - required int? latestSleepResultTime,  
1818 }) async { 1821 }) async {
1819 if (sleepIntervals.isEmpty) return const <HealthRawSleepResult>[]; 1822 if (sleepIntervals.isEmpty) return const <HealthRawSleepResult>[];
1820 final days = { 1823 final days = {
1821 for (final interval in sleepIntervals) _localDay(interval.endTime), 1824 for (final interval in sleepIntervals) _localDay(interval.endTime),
1822 }.toList() 1825 }.toList()
1823 ..sort((a, b) => a.compareTo(b)); 1826 ..sort((a, b) => a.compareTo(b));
1824 - final results = <HealthRawSleepResult>[]; 1827 + final candidates = <HealthRawSleepResult>[];
1825 for (final day in days) { 1828 for (final day in days) {
1826 final calculation = HealthSleepCalculator.calculateDay( 1829 final calculation = HealthSleepCalculator.calculateDay(
1827 day: day, 1830 day: day,
@@ -1832,11 +1835,7 @@ class AppleHealthRawDataCoreService { @@ -1832,11 +1835,7 @@ class AppleHealthRawDataCoreService {
1832 final state = calculation.state; 1835 final state = calculation.state;
1833 if (merged == null || score == null || state == null) continue; 1836 if (merged == null || score == null || state == null) continue;
1834 if (!calculation.hasValidSleep) continue; 1837 if (!calculation.hasValidSleep) continue;
1835 - if (latestSleepResultTime != null &&  
1836 - merged.endTime <= latestSleepResultTime) {  
1837 - continue;  
1838 - }  
1839 - results.add( 1838 + candidates.add(
1840 HealthRawSleepResult( 1839 HealthRawSleepResult(
1841 userId: userId, 1840 userId: userId,
1842 date: merged.endTime, 1841 date: merged.endTime,
@@ -1850,11 +1849,45 @@ class AppleHealthRawDataCoreService { @@ -1850,11 +1849,45 @@ class AppleHealthRawDataCoreService {
1850 ), 1849 ),
1851 ); 1850 );
1852 } 1851 }
  1852 + if (candidates.isEmpty) return const <HealthRawSleepResult>[];
  1853 + final earliestDate =
  1854 + candidates.map((result) => result.date).reduce(math.min);
  1855 + final latestDate = candidates.map((result) => result.date).reduce(math.max);
  1856 + final existingByDate = {
  1857 + for (final result in await _localStore.querySleepResults(
  1858 + userId: userId,
  1859 + startTime: earliestDate,
  1860 + endTime: latestDate,
  1861 + ))
  1862 + result.date: result,
  1863 + };
  1864 + final changedResults = candidates
  1865 + .where(
  1866 + (candidate) => !_sameSleepResult(
  1867 + existingByDate[candidate.date],
  1868 + candidate,
  1869 + ),
  1870 + )
  1871 + .toList();
1853 await _localStore.upsertSleepResults( 1872 await _localStore.upsertSleepResults(
1854 userId: userId, 1873 userId: userId,
1855 - results: results, 1874 + results: candidates,
1856 ); 1875 );
1857 - return results; 1876 + return changedResults;
  1877 + }
  1878 +
  1879 + bool _sameSleepResult(
  1880 + HealthRawSleepResult? existing,
  1881 + HealthRawSleepResult candidate,
  1882 + ) {
  1883 + return existing != null &&
  1884 + existing.userId == candidate.userId &&
  1885 + existing.startDate == candidate.startDate &&
  1886 + existing.sleepScore == candidate.sleepScore &&
  1887 + existing.sleepState == candidate.sleepState &&
  1888 + existing.inBedMinutes == candidate.inBedMinutes &&
  1889 + existing.awakMinutes == candidate.awakMinutes &&
  1890 + existing.sleepMinutes == candidate.sleepMinutes;
1858 } 1891 }
1859 1892
1860 List<HealthRawHrvStressPoint> _filterNewHrvStressPoints( 1893 List<HealthRawHrvStressPoint> _filterNewHrvStressPoints(
@@ -2242,7 +2275,7 @@ class HealthRawStressLocalStore { @@ -2242,7 +2275,7 @@ class HealthRawStressLocalStore {
2242 final db = await _database(userId); 2275 final db = await _database(userId);
2243 final rows = await db.query( 2276 final rows = await db.query(
2244 hrvResultsTable, 2277 hrvResultsTable,
2245 - where: 'uploaded IS NULL OR uploaded != 1', 2278 + where: 'uploaded = 0',
2246 orderBy: 'raw_end_time ASC', 2279 orderBy: 'raw_end_time ASC',
2247 limit: limit, 2280 limit: limit,
2248 ); 2281 );
@@ -2271,7 +2304,7 @@ class HealthRawStressLocalStore { @@ -2271,7 +2304,7 @@ class HealthRawStressLocalStore {
2271 final db = await _database(userId); 2304 final db = await _database(userId);
2272 final rows = await db.query( 2305 final rows = await db.query(
2273 realtimeStressResultsTable, 2306 realtimeStressResultsTable,
2274 - where: 'uploaded IS NULL OR uploaded != 1', 2307 + where: 'uploaded = 0',
2275 orderBy: 'raw_end_time ASC', 2308 orderBy: 'raw_end_time ASC',
2276 limit: limit, 2309 limit: limit,
2277 ); 2310 );
@@ -2307,7 +2340,7 @@ class HealthRawStressLocalStore { @@ -2307,7 +2340,7 @@ class HealthRawStressLocalStore {
2307 final db = await _database(userId); 2340 final db = await _database(userId);
2308 final rows = await db.query( 2341 final rows = await db.query(
2309 dailyStressResultsTable, 2342 dailyStressResultsTable,
2310 - where: 'uploaded IS NULL OR uploaded != 1', 2343 + where: 'uploaded = 0',
2311 orderBy: 'date ASC', 2344 orderBy: 'date ASC',
2312 limit: limit, 2345 limit: limit,
2313 ); 2346 );
@@ -2336,7 +2369,7 @@ class HealthRawStressLocalStore { @@ -2336,7 +2369,7 @@ class HealthRawStressLocalStore {
2336 final db = await _database(userId); 2369 final db = await _database(userId);
2337 final rows = await db.query( 2370 final rows = await db.query(
2338 sleepResultsTable, 2371 sleepResultsTable,
2339 - where: 'uploaded IS NULL OR uploaded != 1', 2372 + where: 'uploaded = 0',
2340 orderBy: 'date ASC', 2373 orderBy: 'date ASC',
2341 limit: limit, 2374 limit: limit,
2342 ); 2375 );
@@ -2561,7 +2594,7 @@ class HealthRawStressLocalStore { @@ -2561,7 +2594,7 @@ class HealthRawStressLocalStore {
2561 final db = await factory.openDatabase( 2594 final db = await factory.openDatabase(
2562 path, 2595 path,
2563 options: OpenDatabaseOptions( 2596 options: OpenDatabaseOptions(
2564 - version: 10, 2597 + version: 11,
2565 onCreate: (db, version) async { 2598 onCreate: (db, version) async {
2566 await _createTables(db); 2599 await _createTables(db);
2567 }, 2600 },
@@ -2594,6 +2627,9 @@ class HealthRawStressLocalStore { @@ -2594,6 +2627,9 @@ class HealthRawStressLocalStore {
2594 if (oldVersion < 10) { 2627 if (oldVersion < 10) {
2595 await _addPushSendTimeColumns(db); 2628 await _addPushSendTimeColumns(db);
2596 } 2629 }
  2630 + if (oldVersion < 11) {
  2631 + await _createIndexes(db);
  2632 + }
2597 }, 2633 },
2598 ), 2634 ),
2599 ); 2635 );
@@ -2647,6 +2683,7 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( @@ -2647,6 +2683,7 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable (
2647 '''); 2683 ''');
2648 await _createDailyStressTable(db); 2684 await _createDailyStressTable(db);
2649 await _createSleepResultsTable(db); 2685 await _createSleepResultsTable(db);
  2686 + await _createIndexes(db);
2650 } 2687 }
2651 2688
2652 Future<void> _createDailyStressTable(DatabaseExecutor db) async { 2689 Future<void> _createDailyStressTable(DatabaseExecutor db) async {
@@ -2686,6 +2723,39 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2686,6 +2723,39 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2686 '''); 2723 ''');
2687 } 2724 }
2688 2725
  2726 + Future<void> _createIndexes(DatabaseExecutor db) async {
  2727 + // Range reads already use each table's INTEGER PRIMARY KEY. These indexes
  2728 + // cover the pending-upload and latest-notification queries instead.
  2729 + await db.execute(
  2730 + 'CREATE INDEX IF NOT EXISTS idx_hrv_results_pending '
  2731 + 'ON $hrvResultsTable(uploaded, raw_end_time)',
  2732 + );
  2733 + await db.execute(
  2734 + 'CREATE INDEX IF NOT EXISTS idx_realtime_stress_results_pending '
  2735 + 'ON $realtimeStressResultsTable(uploaded, raw_end_time)',
  2736 + );
  2737 + await db.execute(
  2738 + 'CREATE INDEX IF NOT EXISTS idx_daily_stress_results_pending '
  2739 + 'ON $dailyStressResultsTable(uploaded, date)',
  2740 + );
  2741 + await db.execute(
  2742 + 'CREATE INDEX IF NOT EXISTS idx_sleep_results_pending '
  2743 + 'ON $sleepResultsTable(uploaded, date)',
  2744 + );
  2745 + await db.execute(
  2746 + 'CREATE INDEX IF NOT EXISTS idx_hrv_results_push_send_time '
  2747 + 'ON $hrvResultsTable(push_send_time)',
  2748 + );
  2749 + await db.execute(
  2750 + 'CREATE INDEX IF NOT EXISTS idx_realtime_stress_results_push_send_time '
  2751 + 'ON $realtimeStressResultsTable(push_send_time)',
  2752 + );
  2753 + await db.execute(
  2754 + 'CREATE INDEX IF NOT EXISTS idx_sleep_results_push_send_time '
  2755 + 'ON $sleepResultsTable(push_send_time)',
  2756 + );
  2757 + }
  2758 +
2689 Future<void> _addUpdateTimeColumns(DatabaseExecutor db) async { 2759 Future<void> _addUpdateTimeColumns(DatabaseExecutor db) async {
2690 for (final table in const [ 2760 for (final table in const [
2691 hrvResultsTable, 2761 hrvResultsTable,
@@ -2919,7 +2989,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2919,7 +2989,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2919 final rows = await db.query( 2989 final rows = await db.query(
2920 table, 2990 table,
2921 columns: ['uploaded'], 2991 columns: ['uploaded'],
2922 - where: 'uploaded != 1', 2992 + where: 'uploaded = 0',
2923 limit: 1, 2993 limit: 1,
2924 ); 2994 );
2925 return rows.isNotEmpty; 2995 return rows.isNotEmpty;
@@ -3217,6 +3287,8 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -3217,6 +3287,8 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
3217 'sleep_minutes', 3287 'sleep_minutes',
3218 ], 3288 ],
3219 ); 3289 );
  3290 + // Recalculation/backfill must not rewrite unchanged rows or upload cursors.
  3291 + if (!valueChanged && existingRow['date_key'] == row['date_key']) return;
3220 await db.update( 3292 await db.update(
3221 sleepResultsTable, 3293 sleepResultsTable,
3222 <String, Object?>{ 3294 <String, Object?>{
@@ -67,13 +67,10 @@ class HuaweiHealthRawStressCalculator { @@ -67,13 +67,10 @@ class HuaweiHealthRawStressCalculator {
67 userId: userId, 67 userId: userId,
68 hrvStressPoints: hrvStressPoints, 68 hrvStressPoints: hrvStressPoints,
69 realtimeStressPoints: realtimeStressPoints, 69 realtimeStressPoints: realtimeStressPoints,
70 - dailyStressPoints: calculateDailyStressPoints(  
71 - userId: userId,  
72 - realtimePoints: realtimeStressPoints,  
73 - startTime: startTime,  
74 - endTime: endTime,  
75 - dataTime: endTime,  
76 - ), 70 + // OHOS calculates daily stress after realtime points are stored, so it
  71 + // can include existing points for every affected day. Calculating it
  72 + // here would duplicate CPU work and be incomplete for partial syncs.
  73 + dailyStressPoints: const <HealthRawDailyStressPoint>[],
77 ); 74 );
78 } 75 }
79 76
@@ -290,6 +290,23 @@ class OHOSHealthRawDataCoreService { @@ -290,6 +290,23 @@ class OHOSHealthRawDataCoreService {
290 '${syncResults.fold<int>(0, (sum, result) => sum + result.storedCount)} ' 290 '${syncResults.fold<int>(0, (sum, result) => sum + result.storedCount)} '
291 'elapsedMs=${syncElapsed.inMilliseconds}', 291 'elapsedMs=${syncElapsed.inMilliseconds}',
292 ); 292 );
  293 + if (willSyncRawData &&
  294 + syncSnapshot != null &&
  295 + !syncSnapshot.hasNewCalculationData) {
  296 + _logInfo(
  297 + '$_calculateLogMarker skip_no_new_base_data userId=$userId '
  298 + 'checkedDataTypes=hr,hrv,sleep',
  299 + );
  300 + // Calculation and notifications require new base data. Keep the upload
  301 + // retry path active so a previous failed result upload is not stranded.
  302 + _scheduleResultUpload();
  303 + return HealthRawStressCalculationResult(
  304 + userId: userId,
  305 + hrvStressPoints: const <HealthRawHrvStressPoint>[],
  306 + realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
  307 + dailyStressPoints: const <HealthRawDailyStressPoint>[],
  308 + );
  309 + }
293 310
294 final calculationStartTime = DateTime.now(); 311 final calculationStartTime = DateTime.now();
295 _publishCalculationEvent( 312 _publishCalculationEvent(
@@ -394,25 +411,26 @@ class OHOSHealthRawDataCoreService { @@ -394,25 +411,26 @@ class OHOSHealthRawDataCoreService {
394 var hasExistingSleep = false; 411 var hasExistingSleep = false;
395 try { 412 try {
396 final contextStopwatch = Stopwatch()..start(); 413 final contextStopwatch = Stopwatch()..start();
397 - final hrvContextStart =  
398 - await _localStore.latestHrvSourceStartTime(userId);  
399 - final realtimeContextStart =  
400 - await _localStore.latestRealtimeSourceStartTime(userId);  
401 - final latestHrvRawEndTime = await _localStore.latestHrvRawEndTime(userId);  
402 - final latestRealtimeRawEndTime =  
403 - await _localStore.latestRealtimeRawEndTime(userId);  
404 - final latestSleepResultTime = await _localStore.latestSleepResultTime(  
405 - userId,  
406 - );  
407 - final latestRawHrvDataTime = await _latestOhosRawDataTime(  
408 - HealthDataUploadType.hrv.type,  
409 - );  
410 - final latestRawHrDataTime = await _latestOhosRawDataTime(  
411 - HealthDataUploadType.heartRate.type,  
412 - );  
413 - final latestRawSleepDataTime = await _latestOhosRawDataTime(  
414 - OhosHealthRawDataType.sleepAnalysis,  
415 - ); 414 + // These reads have no dependencies. Start them together so separate
  415 + // SQLite databases/platform channels do not extend the critical path.
  416 + final context = await Future.wait<int?>([
  417 + _localStore.latestHrvSourceStartTime(userId),
  418 + _localStore.latestRealtimeSourceStartTime(userId),
  419 + _localStore.latestHrvRawEndTime(userId),
  420 + _localStore.latestRealtimeRawEndTime(userId),
  421 + _localStore.latestSleepResultTime(userId),
  422 + _latestOhosRawDataTime(HealthDataUploadType.hrv.type),
  423 + _latestOhosRawDataTime(HealthDataUploadType.heartRate.type),
  424 + _latestOhosRawDataTime(OhosHealthRawDataType.sleepAnalysis),
  425 + ]);
  426 + final hrvContextStart = context[0];
  427 + final realtimeContextStart = context[1];
  428 + final latestHrvRawEndTime = context[2];
  429 + final latestRealtimeRawEndTime = context[3];
  430 + final latestSleepResultTime = context[4];
  431 + final latestRawHrvDataTime = context[5];
  432 + final latestRawHrDataTime = context[6];
  433 + final latestRawSleepDataTime = context[7];
416 _profileLog( 434 _profileLog(
417 'calculate_contextQuery_finish userId=$userId ' 435 'calculate_contextQuery_finish userId=$userId '
418 'elapsedMs=${contextStopwatch.elapsedMilliseconds}', 436 'elapsedMs=${contextStopwatch.elapsedMilliseconds}',
@@ -500,14 +518,48 @@ class OHOSHealthRawDataCoreService { @@ -500,14 +518,48 @@ class OHOSHealthRawDataCoreService {
500 'realtimeRecomputeStartTime=$realtimeRecomputeStartTime', 518 'realtimeRecomputeStartTime=$realtimeRecomputeStartTime',
501 ); 519 );
502 520
  521 + // Raw types are independent. Create every future before awaiting one so
  522 + // their reads overlap; stress dependency is enforced only below.
503 final hrvFetchStopwatch = Stopwatch()..start(); 523 final hrvFetchStopwatch = Stopwatch()..start();
504 - final hrvPoints = await _fetchRawDataForCalculation( 524 + final heartRateFetchStopwatch = Stopwatch()..start();
  525 + final restingHeartRateFetchStopwatch = Stopwatch()..start();
  526 + final sleepFetchStopwatch = Stopwatch()..start();
  527 + final workoutFetchStopwatch = Stopwatch()..start();
  528 + final hrvPointsFuture = _fetchRawDataForCalculation(
505 HealthDataUploadType.hrv.type, 529 HealthDataUploadType.hrv.type,
506 hrvStartTime, 530 hrvStartTime,
507 effectiveEndTime, 531 effectiveEndTime,
508 rawDataSnapshot: rawDataSnapshot, 532 rawDataSnapshot: rawDataSnapshot,
509 readChunkDays: readChunkDays, 533 readChunkDays: readChunkDays,
510 ); 534 );
  535 + final heartRatePointsFuture = _fetchRawDataForCalculation(
  536 + HealthDataUploadType.heartRate.type,
  537 + heartRateStartTime,
  538 + effectiveEndTime,
  539 + rawDataSnapshot: rawDataSnapshot,
  540 + readChunkDays: readChunkDays,
  541 + );
  542 + final restingHeartRatePointsFuture = _fetchRawDataForCalculation(
  543 + HealthDataUploadType.restingHeartRate.type,
  544 + heartRateStartTime,
  545 + effectiveEndTime,
  546 + rawDataSnapshot: rawDataSnapshot,
  547 + readChunkDays: readChunkDays,
  548 + );
  549 + final sleepIntervalsFuture = _fetchSleepIntervalsForCalculation(
  550 + sleepStartTime,
  551 + effectiveEndTime,
  552 + rawDataSnapshot: rawDataSnapshot,
  553 + readChunkDays: readChunkDays,
  554 + );
  555 + final workoutIntervalsFuture = _fetchWorkoutIntervalsForCalculation(
  556 + heartRateStartTime,
  557 + effectiveEndTime,
  558 + rawDataSnapshot: rawDataSnapshot,
  559 + readChunkDays: readChunkDays,
  560 + );
  561 +
  562 + final hrvPoints = await hrvPointsFuture;
511 _profileLog( 563 _profileLog(
512 'calculate_fetchRaw_finish userId=$userId ' 564 'calculate_fetchRaw_finish userId=$userId '
513 'name=hrv dataType=${HealthDataUploadType.hrv.type} ' 565 'name=hrv dataType=${HealthDataUploadType.hrv.type} '
@@ -515,14 +567,7 @@ class OHOSHealthRawDataCoreService { @@ -515,14 +567,7 @@ class OHOSHealthRawDataCoreService {
515 'count=${hrvPoints.length} ' 567 'count=${hrvPoints.length} '
516 'elapsedMs=${hrvFetchStopwatch.elapsedMilliseconds}', 568 'elapsedMs=${hrvFetchStopwatch.elapsedMilliseconds}',
517 ); 569 );
518 - final heartRateFetchStopwatch = Stopwatch()..start();  
519 - final heartRatePoints = await _fetchRawDataForCalculation(  
520 - HealthDataUploadType.heartRate.type,  
521 - heartRateStartTime,  
522 - effectiveEndTime,  
523 - rawDataSnapshot: rawDataSnapshot,  
524 - readChunkDays: readChunkDays,  
525 - ); 570 + final heartRatePoints = await heartRatePointsFuture;
526 _profileLog( 571 _profileLog(
527 'calculate_fetchRaw_finish userId=$userId ' 572 'calculate_fetchRaw_finish userId=$userId '
528 'name=heartRate dataType=${HealthDataUploadType.heartRate.type} ' 573 'name=heartRate dataType=${HealthDataUploadType.heartRate.type} '
@@ -530,14 +575,7 @@ class OHOSHealthRawDataCoreService { @@ -530,14 +575,7 @@ class OHOSHealthRawDataCoreService {
530 'count=${heartRatePoints.length} ' 575 'count=${heartRatePoints.length} '
531 'elapsedMs=${heartRateFetchStopwatch.elapsedMilliseconds}', 576 'elapsedMs=${heartRateFetchStopwatch.elapsedMilliseconds}',
532 ); 577 );
533 - final restingHeartRateFetchStopwatch = Stopwatch()..start();  
534 - final restingHeartRatePoints = await _fetchRawDataForCalculation(  
535 - HealthDataUploadType.restingHeartRate.type,  
536 - heartRateStartTime,  
537 - effectiveEndTime,  
538 - rawDataSnapshot: rawDataSnapshot,  
539 - readChunkDays: readChunkDays,  
540 - ); 578 + final restingHeartRatePoints = await restingHeartRatePointsFuture;
541 _profileLog( 579 _profileLog(
542 'calculate_fetchRaw_finish userId=$userId ' 580 'calculate_fetchRaw_finish userId=$userId '
543 'name=restingHeartRate ' 581 'name=restingHeartRate '
@@ -546,26 +584,14 @@ class OHOSHealthRawDataCoreService { @@ -546,26 +584,14 @@ class OHOSHealthRawDataCoreService {
546 'count=${restingHeartRatePoints.length} ' 584 'count=${restingHeartRatePoints.length} '
547 'elapsedMs=${restingHeartRateFetchStopwatch.elapsedMilliseconds}', 585 'elapsedMs=${restingHeartRateFetchStopwatch.elapsedMilliseconds}',
548 ); 586 );
549 - final sleepFetchStopwatch = Stopwatch()..start();  
550 - final sleepIntervals = await _fetchSleepIntervalsForCalculation(  
551 - sleepStartTime,  
552 - effectiveEndTime,  
553 - rawDataSnapshot: rawDataSnapshot,  
554 - readChunkDays: readChunkDays,  
555 - ); 587 + final sleepIntervals = await sleepIntervalsFuture;
556 _profileLog( 588 _profileLog(
557 'calculate_fetchRaw_finish userId=$userId name=sleep ' 589 'calculate_fetchRaw_finish userId=$userId name=sleep '
558 'startTime=$sleepStartTime endTime=$effectiveEndTime ' 590 'startTime=$sleepStartTime endTime=$effectiveEndTime '
559 'count=${sleepIntervals.length} ' 591 'count=${sleepIntervals.length} '
560 'elapsedMs=${sleepFetchStopwatch.elapsedMilliseconds}', 592 'elapsedMs=${sleepFetchStopwatch.elapsedMilliseconds}',
561 ); 593 );
562 - final workoutFetchStopwatch = Stopwatch()..start();  
563 - final workoutIntervals = await _fetchWorkoutIntervalsForCalculation(  
564 - heartRateStartTime,  
565 - effectiveEndTime,  
566 - rawDataSnapshot: rawDataSnapshot,  
567 - readChunkDays: readChunkDays,  
568 - ); 594 + final workoutIntervals = await workoutIntervalsFuture;
569 _profileLog( 595 _profileLog(
570 'calculate_fetchRaw_finish userId=$userId name=workout ' 596 'calculate_fetchRaw_finish userId=$userId name=workout '
571 'startTime=$heartRateStartTime endTime=$effectiveEndTime ' 597 'startTime=$heartRateStartTime endTime=$effectiveEndTime '
@@ -589,6 +615,14 @@ class OHOSHealthRawDataCoreService { @@ -589,6 +615,14 @@ class OHOSHealthRawDataCoreService {
589 'needSleep=${_needsNewResult(latestRawSleepTime, latestSleepResultTime)}', 615 'needSleep=${_needsNewResult(latestRawSleepTime, latestSleepResultTime)}',
590 ); 616 );
591 617
  618 + // Sleep has no result dependency on either stress calculation. Run its
  619 + // CPU work alongside the HRV -> realtime-stress isolate; only storage is
  620 + // sequenced later to avoid concurrent writes to the result database.
  621 + final sleepCalculationStopwatch = Stopwatch()..start();
  622 + final sleepCalculationFuture = _calculateSleepResults(
  623 + userId: userId,
  624 + sleepIntervals: sleepIntervals,
  625 + );
592 final isolateStopwatch = Stopwatch()..start(); 626 final isolateStopwatch = Stopwatch()..start();
593 final result = await Isolate.run( 627 final result = await Isolate.run(
594 () => HuaweiHealthRawStressCalculator(userId: userId).calculate( 628 () => HuaweiHealthRawStressCalculator(userId: userId).calculate(
@@ -671,15 +705,14 @@ class OHOSHealthRawDataCoreService { @@ -671,15 +705,14 @@ class OHOSHealthRawDataCoreService {
671 'count=${dailyStressPoints.length} ' 705 'count=${dailyStressPoints.length} '
672 'elapsedMs=${dailyStopwatch.elapsedMilliseconds}', 706 'elapsedMs=${dailyStopwatch.elapsedMilliseconds}',
673 ); 707 );
674 - final sleepStopwatch = Stopwatch()..start();  
675 - final sleepResults = await _calculateAndStoreSleepResults( 708 + final sleepResults = await sleepCalculationFuture;
  709 + await _storeSleepResults(
676 userId: userId, 710 userId: userId,
677 - sleepIntervals: sleepIntervals,  
678 - latestSleepResultTime: latestSleepResultTime, 711 + results: sleepResults,
679 ); 712 );
680 _profileLog( 713 _profileLog(
681 'calculate_sleep_finish userId=$userId count=${sleepResults.length} ' 714 'calculate_sleep_finish userId=$userId count=${sleepResults.length} '
682 - 'elapsedMs=${sleepStopwatch.elapsedMilliseconds}', 715 + 'elapsedMs=${sleepCalculationStopwatch.elapsedMilliseconds}',
683 ); 716 );
684 _logInfo( 717 _logInfo(
685 'calculate_daily_sleep_stored userId=$userId ' 718 'calculate_daily_sleep_stored userId=$userId '
@@ -1440,31 +1473,59 @@ class OHOSHealthRawDataCoreService { @@ -1440,31 +1473,59 @@ class OHOSHealthRawDataCoreService {
1440 return dailyStressPoints; 1473 return dailyStressPoints;
1441 } 1474 }
1442 1475
1443 - Future<List<HealthRawSleepResult>> _calculateAndStoreSleepResults({ 1476 + Future<List<HealthRawSleepResult>> _calculateSleepResults({
1444 required int userId, 1477 required int userId,
1445 required List<HealthKitRawDataPoint> sleepIntervals, 1478 required List<HealthKitRawDataPoint> sleepIntervals,
1446 - required int? latestSleepResultTime,  
1447 }) async { 1479 }) async {
1448 _logInfo( 1480 _logInfo(
1449 '$_sleepCalcLogMarker start userId=$userId ' 1481 '$_sleepCalcLogMarker start userId=$userId '
1450 - 'sleepIntervals=${sleepIntervals.length} '  
1451 - 'latestSleepResultTime=$latestSleepResultTime', 1482 + 'sleepIntervals=${sleepIntervals.length}',
1452 ); 1483 );
1453 if (sleepIntervals.isEmpty) { 1484 if (sleepIntervals.isEmpty) {
1454 _logInfo('$_sleepCalcLogMarker no_raw_sleep userId=$userId'); 1485 _logInfo('$_sleepCalcLogMarker no_raw_sleep userId=$userId');
1455 return const <HealthRawSleepResult>[]; 1486 return const <HealthRawSleepResult>[];
1456 } 1487 }
  1488 + final stopwatch = Stopwatch()..start();
  1489 + final results = await Isolate.run(
  1490 + () => _calculateSleepResultsSync(
  1491 + userId: userId,
  1492 + sleepIntervals: sleepIntervals,
  1493 + ),
  1494 + debugName: 'OHOSHealthSleepCalculator',
  1495 + );
  1496 + _profileLog(
  1497 + 'sleep_isolate_finish userId=$userId '
  1498 + 'input=${sleepIntervals.length} result=${results.length} '
  1499 + 'elapsedMs=${stopwatch.elapsedMilliseconds}',
  1500 + );
  1501 + return results;
  1502 + }
  1503 +
  1504 + Future<void> _storeSleepResults({
  1505 + required int userId,
  1506 + required List<HealthRawSleepResult> results,
  1507 + }) async {
  1508 + final sleepStoreStopwatch = Stopwatch()..start();
  1509 + await _localStore.upsertSleepResults(userId: userId, results: results);
  1510 + _profileLog(
  1511 + 'sleep_store_finish userId=$userId stored=${results.length} '
  1512 + 'elapsedMs=${sleepStoreStopwatch.elapsedMilliseconds}',
  1513 + );
  1514 + _logInfo(
  1515 + '$_sleepCalcLogMarker stored userId=$userId stored=${results.length}',
  1516 + );
  1517 + }
  1518 +
  1519 + static List<HealthRawSleepResult> _calculateSleepResultsSync({
  1520 + required int userId,
  1521 + required List<HealthKitRawDataPoint> sleepIntervals,
  1522 + }) {
1457 final days = { 1523 final days = {
1458 for (final interval in sleepIntervals) _localDay(interval.endTime), 1524 for (final interval in sleepIntervals) _localDay(interval.endTime),
1459 }.toList() 1525 }.toList()
1460 ..sort((a, b) => a.compareTo(b)); 1526 ..sort((a, b) => a.compareTo(b));
1461 - _logInfo(  
1462 - '$_sleepCalcLogMarker days userId=$userId '  
1463 - 'days=${days.map(_dateKeyFromDateTime).toList()}',  
1464 - );  
1465 final results = <HealthRawSleepResult>[]; 1527 final results = <HealthRawSleepResult>[];
1466 for (final day in days) { 1528 for (final day in days) {
1467 - final dayStopwatch = Stopwatch()..start();  
1468 final calculation = HealthSleepCalculator.calculateDay( 1529 final calculation = HealthSleepCalculator.calculateDay(
1469 day: day, 1530 day: day,
1470 sleepIntervals: sleepIntervals, 1531 sleepIntervals: sleepIntervals,
@@ -1473,49 +1534,13 @@ class OHOSHealthRawDataCoreService { @@ -1473,49 +1534,13 @@ class OHOSHealthRawDataCoreService {
1473 final score = calculation.score; 1534 final score = calculation.score;
1474 final state = calculation.state; 1535 final state = calculation.state;
1475 if (merged == null || score == null || state == null) { 1536 if (merged == null || score == null || state == null) {
1476 - _profileLog(  
1477 - 'sleep_day_finish userId=$userId '  
1478 - 'date=${_dateKeyFromDateTime(day)} hasResult=false '  
1479 - 'reason=missing_score_or_state '  
1480 - 'elapsedMs=${dayStopwatch.elapsedMilliseconds}',  
1481 - );  
1482 - _logInfo(  
1483 - '$_sleepCalcLogMarker skip_invalid userId=$userId '  
1484 - 'date=${_dateKeyFromDateTime(day)} '  
1485 - 'reason=missing_score_or_state '  
1486 - 'score=$score state=${state?.value}',  
1487 - );  
1488 continue; 1537 continue;
1489 } 1538 }
1490 if (!calculation.hasValidSleep) { 1539 if (!calculation.hasValidSleep) {
1491 - _profileLog(  
1492 - 'sleep_day_finish userId=$userId '  
1493 - 'date=${_dateKeyFromDateTime(day)} hasResult=false '  
1494 - 'reason=no_valid_sleep '  
1495 - 'elapsedMs=${dayStopwatch.elapsedMilliseconds}',  
1496 - );  
1497 - _logInfo(  
1498 - '$_sleepCalcLogMarker skip_invalid userId=$userId '  
1499 - 'date=${_dateKeyFromDateTime(day)} reason=no_valid_sleep '  
1500 - 'durationSeconds=${calculation.durationSeconds}',  
1501 - );  
1502 - continue;  
1503 - }  
1504 - if (latestSleepResultTime != null &&  
1505 - merged.endTime <= latestSleepResultTime) {  
1506 - _profileLog(  
1507 - 'sleep_day_finish userId=$userId '  
1508 - 'date=${_dateKeyFromDateTime(day)} hasResult=false '  
1509 - 'reason=existing mergedEnd=${merged.endTime} '  
1510 - 'elapsedMs=${dayStopwatch.elapsedMilliseconds}',  
1511 - );  
1512 - _logInfo(  
1513 - '$_sleepCalcLogMarker skip_existing userId=$userId '  
1514 - 'date=${_dateKeyFromDateTime(day)} mergedEnd=${merged.endTime} '  
1515 - 'latestSleepResultTime=$latestSleepResultTime',  
1516 - );  
1517 continue; 1540 continue;
1518 } 1541 }
  1542 + // A newer result does not prove that older days were calculated.
  1543 + // Reconcile every day in the input; the store preserves unchanged uploads.
1519 results.add( 1544 results.add(
1520 HealthRawSleepResult( 1545 HealthRawSleepResult(
1521 userId: userId, 1546 userId: userId,
@@ -1529,28 +1554,7 @@ class OHOSHealthRawDataCoreService { @@ -1529,28 +1554,7 @@ class OHOSHealthRawDataCoreService {
1529 uploaded: false, 1554 uploaded: false,
1530 ), 1555 ),
1531 ); 1556 );
1532 - _profileLog(  
1533 - 'sleep_day_finish userId=$userId '  
1534 - 'date=${_dateKeyFromDateTime(day)} hasResult=true '  
1535 - 'sleepMinutes=${calculation.summary.sleepMinutes} '  
1536 - 'elapsedMs=${dayStopwatch.elapsedMilliseconds}',  
1537 - );  
1538 - _logInfo(  
1539 - '$_sleepCalcLogMarker result userId=$userId '  
1540 - 'date=${_dateKeyFromDateTime(day)} start=${merged.startTime} '  
1541 - 'end=${merged.endTime} score=$score state=${state.value} '  
1542 - 'sleepMinutes=${calculation.summary.sleepMinutes}',  
1543 - );  
1544 } 1557 }
1545 - final sleepStoreStopwatch = Stopwatch()..start();  
1546 - await _localStore.upsertSleepResults(userId: userId, results: results);  
1547 - _profileLog(  
1548 - 'sleep_store_finish userId=$userId stored=${results.length} '  
1549 - 'elapsedMs=${sleepStoreStopwatch.elapsedMilliseconds}',  
1550 - );  
1551 - _logInfo(  
1552 - '$_sleepCalcLogMarker stored userId=$userId stored=${results.length}',  
1553 - );  
1554 return results; 1558 return results;
1555 } 1559 }
1556 1560
@@ -1626,15 +1630,30 @@ class OHOSHealthRawDataCoreService { @@ -1626,15 +1630,30 @@ class OHOSHealthRawDataCoreService {
1626 }) async { 1630 }) async {
1627 final rawDataSource = _rawDataSource; 1631 final rawDataSource = _rawDataSource;
1628 if (rawDataSnapshot != null && rawDataSource is OhosHealthRawDataSource) { 1632 if (rawDataSnapshot != null && rawDataSource is OhosHealthRawDataSource) {
1629 - final points = rawDataSource.getRawDataFromSnapshot(  
1630 - snapshot: rawDataSnapshot,  
1631 - dataType: dataType,  
1632 - startTime: startTime,  
1633 - endTime: endTime,  
1634 - )..sort((a, b) => a.endTime.compareTo(b.endTime)); 1633 + // The snapshot is complete for this fetch, but it does not include the
  1634 + // older local context required for baselines and smoothing. Overlay the
  1635 + // fetched values on the indexed local range before calculating.
  1636 + final localPoints = await rawDataSource.getRawData(
  1637 + dataType,
  1638 + startTime,
  1639 + endTime,
  1640 + );
  1641 + final pointsByTime = <int, HealthKitRawDataPoint>{
  1642 + for (final point in localPoints) point.endTime: point,
  1643 + for (final point in rawDataSource.getRawDataFromSnapshot(
  1644 + snapshot: rawDataSnapshot,
  1645 + dataType: dataType,
  1646 + startTime: startTime,
  1647 + endTime: endTime,
  1648 + ))
  1649 + point.endTime: point,
  1650 + };
  1651 + final points = pointsByTime.values.toList()
  1652 + ..sort((a, b) => a.endTime.compareTo(b.endTime));
1635 _logInfo( 1653 _logInfo(
1636 - '$_calculateLogMarker raw_snapshot_hit dataType=$dataType '  
1637 - 'startTime=$startTime endTime=$endTime count=${points.length}', 1654 + '$_calculateLogMarker raw_local_snapshot_merged '
  1655 + 'dataType=$dataType startTime=$startTime endTime=$endTime '
  1656 + 'local=${localPoints.length} merged=${points.length}',
1638 ); 1657 );
1639 return points; 1658 return points;
1640 } 1659 }
@@ -1669,24 +1688,35 @@ class OHOSHealthRawDataCoreService { @@ -1669,24 +1688,35 @@ class OHOSHealthRawDataCoreService {
1669 required OhosHealthRawDataMemorySnapshot? rawDataSnapshot, 1688 required OhosHealthRawDataMemorySnapshot? rawDataSnapshot,
1670 required int readChunkDays, 1689 required int readChunkDays,
1671 }) async { 1690 }) async {
  1691 + // The snapshot contains only this sync's response, not all persisted sleep.
  1692 + // Read the bounded local range once and overlay the snapshot so calculation
  1693 + // is complete even while the snapshot is still being written in background.
  1694 + final groups = await _rawDataSource.getRawSleepData(startTime, endTime);
  1695 + final pointsByStart = <int, HealthKitRawDataPoint>{
  1696 + for (final point in groups.expand((group) => group.sleepDataPoints))
  1697 + point.startTime: point,
  1698 + };
1672 final rawDataSource = _rawDataSource; 1699 final rawDataSource = _rawDataSource;
1673 if (rawDataSnapshot != null && rawDataSource is OhosHealthRawDataSource) { 1700 if (rawDataSnapshot != null && rawDataSource is OhosHealthRawDataSource) {
1674 - final points = rawDataSource.getRawSleepIntervalsFromSnapshot( 1701 + for (final point in rawDataSource.getRawSleepIntervalsFromSnapshot(
1675 snapshot: rawDataSnapshot, 1702 snapshot: rawDataSnapshot,
1676 startTime: startTime, 1703 startTime: startTime,
1677 endTime: endTime, 1704 endTime: endTime,
1678 - )..sort((a, b) => a.endTime.compareTo(b.endTime));  
1679 - _logInfo(  
1680 - '$_calculateLogMarker sleep_snapshot_hit '  
1681 - 'startTime=$startTime endTime=$endTime count=${points.length}',  
1682 - );  
1683 - return points; 1705 + )) {
  1706 + // OHOS raw sleep uses from_time as its unique key.
  1707 + pointsByStart[point.startTime] = point;
  1708 + }
1684 } 1709 }
1685 - return _fetchSleepIntervalsInChunks(  
1686 - startTime,  
1687 - endTime,  
1688 - readChunkDays: readChunkDays, 1710 + final points = pointsByStart.values
  1711 + .where(
  1712 + (point) => point.endTime >= startTime && point.startTime <= endTime)
  1713 + .toList()
  1714 + ..sort((a, b) => a.endTime.compareTo(b.endTime));
  1715 + _logInfo(
  1716 + '$_calculateLogMarker sleep_local_snapshot_merged '
  1717 + 'startTime=$startTime endTime=$endTime count=${points.length}',
1689 ); 1718 );
  1719 + return points;
1690 } 1720 }
1691 1721
1692 Future<List<HealthKitRawWorkoutDataPoint>> _fetchWorkoutIntervalsInChunks( 1722 Future<List<HealthKitRawWorkoutDataPoint>> _fetchWorkoutIntervalsInChunks(
@@ -1715,14 +1745,25 @@ class OHOSHealthRawDataCoreService { @@ -1715,14 +1745,25 @@ class OHOSHealthRawDataCoreService {
1715 }) async { 1745 }) async {
1716 final rawDataSource = _rawDataSource; 1746 final rawDataSource = _rawDataSource;
1717 if (rawDataSnapshot != null && rawDataSource is OhosHealthRawDataSource) { 1747 if (rawDataSnapshot != null && rawDataSource is OhosHealthRawDataSource) {
1718 - final points = rawDataSource.getRawWorkoutDataFromSnapshot(  
1719 - snapshot: rawDataSnapshot,  
1720 - startTime: startTime,  
1721 - endTime: endTime,  
1722 - )..sort((a, b) => a.endTime.compareTo(b.endTime)); 1748 + final localPoints = await rawDataSource.getRawWorkoutData(
  1749 + startTime,
  1750 + endTime,
  1751 + );
  1752 + final pointsByStart = <int, HealthKitRawWorkoutDataPoint>{
  1753 + for (final point in localPoints) point.startTime: point,
  1754 + for (final point in rawDataSource.getRawWorkoutDataFromSnapshot(
  1755 + snapshot: rawDataSnapshot,
  1756 + startTime: startTime,
  1757 + endTime: endTime,
  1758 + ))
  1759 + point.startTime: point,
  1760 + };
  1761 + final points = pointsByStart.values.toList()
  1762 + ..sort((a, b) => a.endTime.compareTo(b.endTime));
1723 _logInfo( 1763 _logInfo(
1724 - '$_calculateLogMarker workout_snapshot_hit '  
1725 - 'startTime=$startTime endTime=$endTime count=${points.length}', 1764 + '$_calculateLogMarker workout_local_snapshot_merged '
  1765 + 'startTime=$startTime endTime=$endTime '
  1766 + 'local=${localPoints.length} merged=${points.length}',
1726 ); 1767 );
1727 return points; 1768 return points;
1728 } 1769 }
@@ -89,12 +89,16 @@ class OhosHealthRawDataCalculationSyncSnapshot { @@ -89,12 +89,16 @@ class OhosHealthRawDataCalculationSyncSnapshot {
89 const OhosHealthRawDataCalculationSyncSnapshot({ 89 const OhosHealthRawDataCalculationSyncSnapshot({
90 required this.results, 90 required this.results,
91 required this.rawData, 91 required this.rawData,
  92 + required this.newCalculationDataTypes,
92 this.activityGoal, 93 this.activityGoal,
93 this.storeFuture, 94 this.storeFuture,
94 }); 95 });
95 96
96 final List<OhosHealthRawDataSyncResult> results; 97 final List<OhosHealthRawDataSyncResult> results;
97 final OhosHealthRawDataMemorySnapshot rawData; 98 final OhosHealthRawDataMemorySnapshot rawData;
  99 + final Set<int> newCalculationDataTypes;
98 final V2ActivityTarget? activityGoal; 100 final V2ActivityTarget? activityGoal;
99 final Future<void>? storeFuture; 101 final Future<void>? storeFuture;
  102 +
  103 + bool get hasNewCalculationData => newCalculationDataTypes.isNotEmpty;
100 } 104 }
@@ -310,6 +310,7 @@ class OhosHealthRawDataSyncService { @@ -310,6 +310,7 @@ class OhosHealthRawDataSyncService {
310 final rawData = OhosHealthRawDataMemorySnapshot( 310 final rawData = OhosHealthRawDataMemorySnapshot(
311 results: results, 311 results: results,
312 ); 312 );
  313 + final newCalculationDataTypes = await _newCalculationDataTypes(results);
313 final pageCount = results.fold<int>( 314 final pageCount = results.fold<int>(
314 0, 315 0,
315 (sum, result) => sum + result.pageCount, 316 (sum, result) => sum + result.pageCount,
@@ -335,7 +336,8 @@ class OhosHealthRawDataSyncService { @@ -335,7 +336,8 @@ class OhosHealthRawDataSyncService {
335 'calculation_sync_snapshot_finish ' 336 'calculation_sync_snapshot_finish '
336 'dataTypes=${resolvedDataTypes.join(',')} ' 337 'dataTypes=${resolvedDataTypes.join(',')} '
337 'pageCount=$pageCount fetchedCount=$fetchedCount ' 338 'pageCount=$pageCount fetchedCount=$fetchedCount '
338 - 'snapshotCount=${rawData.length}', 339 + 'snapshotCount=${rawData.length} '
  340 + 'newCalculationDataTypes=${newCalculationDataTypes.join(',')}',
339 ); 341 );
340 _log( 342 _log(
341 '$timingLogMarker fetch_all_finish ' 343 '$timingLogMarker fetch_all_finish '
@@ -356,6 +358,7 @@ class OhosHealthRawDataSyncService { @@ -356,6 +358,7 @@ class OhosHealthRawDataSyncService {
356 return OhosHealthRawDataCalculationSyncSnapshot( 358 return OhosHealthRawDataCalculationSyncSnapshot(
357 results: results, 359 results: results,
358 rawData: rawData, 360 rawData: rawData,
  361 + newCalculationDataTypes: newCalculationDataTypes,
359 activityGoal: activityGoal, 362 activityGoal: activityGoal,
360 storeFuture: storeFuture, 363 storeFuture: storeFuture,
361 ); 364 );
@@ -385,6 +388,48 @@ class OhosHealthRawDataSyncService { @@ -385,6 +388,48 @@ class OhosHealthRawDataSyncService {
385 } 388 }
386 } 389 }
387 390
  391 + Future<Set<int>> _newCalculationDataTypes(
  392 + List<OhosHealthRawDataSyncResult> results,
  393 + ) async {
  394 + final triggerDataTypes = <int>{
  395 + HealthDataUploadType.heartRate.type,
  396 + HealthDataUploadType.hrv.type,
  397 + OhosHealthRawDataType.sleepAnalysis,
  398 + };
  399 + final types = <int>{};
  400 + for (final result in results) {
  401 + final dataType = result.dataType;
  402 + if (!triggerDataTypes.contains(dataType) || result.rawItems.isEmpty) {
  403 + continue;
  404 + }
  405 + final incomingTimes = result.rawItems
  406 + .map((item) => _calculationTimestamp(dataType, item))
  407 + .toSet();
  408 + if (incomingTimes.isEmpty) continue;
  409 + final startTime = incomingTimes.reduce(math.min);
  410 + final endTime = incomingTimes.reduce(math.max);
  411 + final localItems = await _localStore.queryRawData(
  412 + dataType: dataType,
  413 + startTime: startTime,
  414 + endTime: endTime,
  415 + );
  416 + final existingTimes = localItems
  417 + .map((item) => _calculationTimestamp(dataType, item))
  418 + .toSet();
  419 + if (incomingTimes.any((time) => !existingTimes.contains(time))) {
  420 + types.add(dataType);
  421 + }
  422 + }
  423 + return Set<int>.unmodifiable(types);
  424 + }
  425 +
  426 + int _calculationTimestamp(int dataType, OhosHealthRawDataItem item) {
  427 + if (dataType == OhosHealthRawDataType.sleepAnalysis) {
  428 + return _numPayload(item.payload, 'to_time') ?? item.dataTime;
  429 + }
  430 + return _numPayload(item.payload, 'time') ?? item.dataTime;
  431 + }
  432 +
388 Future<V2ActivityTarget?> _fetchActivityGoalForCalculationSync() async { 433 Future<V2ActivityTarget?> _fetchActivityGoalForCalculationSync() async {
389 final stopwatch = Stopwatch()..start(); 434 final stopwatch = Stopwatch()..start();
390 try { 435 try {
@@ -705,9 +750,18 @@ class OhosHealthRawDataSyncService { @@ -705,9 +750,18 @@ class OhosHealthRawDataSyncService {
705 for (final result in results) { 750 for (final result in results) {
706 if (result.rawItems.isEmpty) continue; 751 if (result.rawItems.isEmpty) continue;
707 final typeStopwatch = Stopwatch()..start(); 752 final typeStopwatch = Stopwatch()..start();
  753 + // Requests finish out of order. Persist each table chronologically after
  754 + // every type/segment has been fetched, using its SQLite unique time key.
  755 + final sortedItems = [...result.rawItems]..sort((a, b) {
  756 + final keyCompare = _dedupeKeyTime(dataType: result.dataType, item: a)
  757 + .compareTo(_dedupeKeyTime(dataType: result.dataType, item: b));
  758 + return keyCompare != 0
  759 + ? keyCompare
  760 + : a.dataTime.compareTo(b.dataTime);
  761 + });
708 final count = await _localStore.upsertRawDataBatch( 762 final count = await _localStore.upsertRawDataBatch(
709 dataType: result.dataType, 763 dataType: result.dataType,
710 - items: result.rawItems, 764 + items: sortedItems,
711 ); 765 );
712 storedCount += count; 766 storedCount += count;
713 _log( 767 _log(
  1 +import 'dart:async';
  2 +
1 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_source.dart'; 3 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_source.dart';
2 import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart'; 4 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'; 5 import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_data_core_service.dart';
@@ -10,6 +12,66 @@ import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; @@ -10,6 +12,66 @@ import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
10 import 'package:flutter_test/flutter_test.dart'; 12 import 'package:flutter_test/flutter_test.dart';
11 13
12 void main() { 14 void main() {
  15 + for (final hasSnapshotSleep in [false, true]) {
  16 + test(
  17 + 'OHOS repairs September 6 sleep with newer result; '
  18 + 'snapshot=$hasSnapshotSleep and raw write pending', () async {
  19 + int seconds(DateTime date) => date.millisecondsSinceEpoch ~/ 1000;
  20 + final start = seconds(DateTime(2026, 9, 5, 23));
  21 + final middle = seconds(DateTime(2026, 9, 6, 3));
  22 + final end = seconds(DateTime(2026, 9, 6, 7));
  23 + final gate = Completer<void>();
  24 + addTearDown(() {
  25 + if (!gate.isCompleted) gate.complete();
  26 + });
  27 + OhosHealthRawDataItem sleep(int from, int to) => OhosHealthRawDataItem(
  28 + dataType: 4,
  29 + dataTime: to,
  30 + payload: {'from_time': from, 'to_time': to},
  31 + );
  32 + final rawStore = _FakeOhosHealthRawDataLocalStore(
  33 + latestDataTime: end,
  34 + storeGate: gate.future,
  35 + itemsByDataType: {
  36 + OhosHealthRawDataType.sleepAnalysis: [
  37 + sleep(start, hasSnapshotSleep ? middle : end),
  38 + ],
  39 + },
  40 + );
  41 + final store = _FakeHealthRawStressLocalStore()
  42 + ..latestSleepTime = seconds(DateTime(2026, 9, 7, 7));
  43 + final service = OHOSHealthRawDataCoreService(
  44 + rawDataSource: OhosHealthRawDataSource(
  45 + syncService: OhosHealthRawDataSyncService(
  46 + remoteDataSource: _HistoricalBackfillRemoteDataSource(
  47 + hrvItems: const [],
  48 + heartRateItems: const [],
  49 + sleepItems: hasSnapshotSleep ? [sleep(middle, end)] : const [],
  50 + ),
  51 + localStore: rawStore,
  52 + ),
  53 + ),
  54 + localStore: store,
  55 + userIdProvider: () => 42,
  56 + uploadResultsAfterCalculation: false,
  57 + healthReadAuthorizationChecker: () async => true,
  58 + );
  59 +
  60 + final result = await service.startCoreCaculate(
  61 + endTime: seconds(DateTime(2026, 9, 7, 12)),
  62 + );
  63 +
  64 + expect(result.sleepResults, hasLength(1));
  65 + expect(result.sleepResults.single.date, end);
  66 + expect(result.sleepResults.single.startDate, start);
  67 + expect(result.sleepResults.single.sleepMinutes, 8 * 60);
  68 + expect(store.sleepResults.single.date, end);
  69 + expect(rawStore.sleepQueryCount, 1);
  70 + expect(gate.isCompleted, isFalse);
  71 + expect(rawStore.storedBatches, isEmpty);
  72 + });
  73 + }
  74 +
13 test('OHOS core calculates and stores all Huawei result tables', () async { 75 test('OHOS core calculates and stores all Huawei result tables', () async {
14 final day = DateTime.now().subtract(const Duration(days: 7)); 76 final day = DateTime.now().subtract(const Duration(days: 7));
15 final base = 77 final base =
@@ -515,10 +577,12 @@ class _HistoricalBackfillRemoteDataSource @@ -515,10 +577,12 @@ class _HistoricalBackfillRemoteDataSource
515 _HistoricalBackfillRemoteDataSource({ 577 _HistoricalBackfillRemoteDataSource({
516 required this.hrvItems, 578 required this.hrvItems,
517 required this.heartRateItems, 579 required this.heartRateItems,
  580 + this.sleepItems = const [],
518 }); 581 });
519 582
520 final List<OhosHealthRawDataItem> hrvItems; 583 final List<OhosHealthRawDataItem> hrvItems;
521 final List<OhosHealthRawDataItem> heartRateItems; 584 final List<OhosHealthRawDataItem> heartRateItems;
  585 + final List<OhosHealthRawDataItem> sleepItems;
522 final calls = <_RemoteRawCall>[]; 586 final calls = <_RemoteRawCall>[];
523 587
524 @override 588 @override
@@ -534,6 +598,9 @@ class _HistoricalBackfillRemoteDataSource @@ -534,6 +598,9 @@ class _HistoricalBackfillRemoteDataSource
534 endTime: endTime, 598 endTime: endTime,
535 ), 599 ),
536 ); 600 );
  601 + if (dataType == OhosHealthRawDataType.sleepAnalysis) {
  602 + return OhosHealthRawDataPage(items: sleepItems);
  603 + }
537 if (dataType == 1) return OhosHealthRawDataPage(items: hrvItems); 604 if (dataType == 1) return OhosHealthRawDataPage(items: hrvItems);
538 if (dataType == 2) return OhosHealthRawDataPage(items: heartRateItems); 605 if (dataType == 2) return OhosHealthRawDataPage(items: heartRateItems);
539 return const OhosHealthRawDataPage(items: <OhosHealthRawDataItem>[]); 606 return const OhosHealthRawDataPage(items: <OhosHealthRawDataItem>[]);
@@ -559,6 +626,7 @@ class _RemoteRawCall { @@ -559,6 +626,7 @@ class _RemoteRawCall {
559 626
560 class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { 627 class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
561 _FakeOhosHealthRawDataLocalStore({ 628 _FakeOhosHealthRawDataLocalStore({
  629 + this.storeGate,
562 int? latestDataTime, 630 int? latestDataTime,
563 Map<int, int>? latestDataTimeByType, 631 Map<int, int>? latestDataTimeByType,
564 Map<int, List<OhosHealthRawDataItem>>? itemsByDataType, 632 Map<int, List<OhosHealthRawDataItem>>? itemsByDataType,
@@ -567,6 +635,8 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { @@ -567,6 +635,8 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
567 _itemsByDataType = 635 _itemsByDataType =
568 itemsByDataType ?? const <int, List<OhosHealthRawDataItem>>{}; 636 itemsByDataType ?? const <int, List<OhosHealthRawDataItem>>{};
569 637
  638 + final Future<void>? storeGate;
  639 + int sleepQueryCount = 0;
570 final int? _latestDataTime; 640 final int? _latestDataTime;
571 final Map<int, int> _latestDataTimeByType; 641 final Map<int, int> _latestDataTimeByType;
572 final Map<int, List<OhosHealthRawDataItem>> _itemsByDataType; 642 final Map<int, List<OhosHealthRawDataItem>> _itemsByDataType;
@@ -583,13 +653,15 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { @@ -583,13 +653,15 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
583 required int startTime, 653 required int startTime,
584 required int endTime, 654 required int endTime,
585 }) async { 655 }) async {
  656 + if (dataType == OhosHealthRawDataType.sleepAnalysis) sleepQueryCount++;
586 return [ 657 return [
587 ...(_itemsByDataType[dataType] ?? const <OhosHealthRawDataItem>[]), 658 ...(_itemsByDataType[dataType] ?? const <OhosHealthRawDataItem>[]),
588 ...storedBatches.expand((batch) => batch), 659 ...storedBatches.expand((batch) => batch),
589 ] 660 ]
590 .where( 661 .where(
591 (item) => 662 (item) =>
592 - item.dataType == dataType && 663 + (dataType == OhosHealthRawDataType.sleepAnalysis ||
  664 + item.dataType == dataType) &&
593 item.dataTime >= startTime && 665 item.dataTime >= startTime &&
594 item.dataTime <= endTime, 666 item.dataTime <= endTime,
595 ) 667 )
@@ -609,6 +681,7 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { @@ -609,6 +681,7 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
609 required int dataType, 681 required int dataType,
610 required List<OhosHealthRawDataItem> items, 682 required List<OhosHealthRawDataItem> items,
611 }) async { 683 }) async {
  684 + if (storeGate != null) await storeGate;
612 storedBatches.add(items); 685 storedBatches.add(items);
613 return items.length; 686 return items.length;
614 } 687 }
@@ -683,6 +756,7 @@ class _FakeHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -683,6 +756,7 @@ class _FakeHealthRawStressLocalStore extends HealthRawStressLocalStore {
683 final realtimeStressPoints = <HealthRawRealtimeStressPoint>[]; 756 final realtimeStressPoints = <HealthRawRealtimeStressPoint>[];
684 final dailyStressPoints = <HealthRawDailyStressPoint>[]; 757 final dailyStressPoints = <HealthRawDailyStressPoint>[];
685 final sleepResults = <HealthRawSleepResult>[]; 758 final sleepResults = <HealthRawSleepResult>[];
  759 + int? latestSleepTime;
686 760
687 @override 761 @override
688 Future<void> ensureReadable(int userId) async {} 762 Future<void> ensureReadable(int userId) async {}
@@ -711,7 +785,7 @@ class _FakeHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -711,7 +785,7 @@ class _FakeHealthRawStressLocalStore extends HealthRawStressLocalStore {
711 785
712 @override 786 @override
713 Future<int?> latestSleepResultTime(int userId) async { 787 Future<int?> latestSleepResultTime(int userId) async {
714 - return null; 788 + return latestSleepTime;
715 } 789 }
716 790
717 @override 791 @override
@@ -11,6 +11,128 @@ import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; @@ -11,6 +11,128 @@ import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
11 import 'package:flutter_test/flutter_test.dart'; 11 import 'package:flutter_test/flutter_test.dart';
12 12
13 void main() { 13 void main() {
  14 + test('calculation snapshot waits for every segment and stores sorted batches',
  15 + () async {
  16 + final remote = _ControlledSnapshotRemote();
  17 + final local = _FakeOhosHealthRawDataLocalStore();
  18 + final service = OhosHealthRawDataSyncService(
  19 + remoteDataSource: remote,
  20 + localStore: local,
  21 + );
  22 + var snapshotReady = false;
  23 + final future = service.syncCalculationRawDataSnapshot(
  24 + startTime: _unixSeconds(DateTime(2026, 1, 30)),
  25 + endTime: _unixSeconds(DateTime(2026, 2, 22)),
  26 + dataTypes: [2, OhosHealthRawDataType.sleepAnalysis],
  27 + ).then((snapshot) {
  28 + snapshotReady = true;
  29 + return snapshot;
  30 + });
  31 + await Future<void>.delayed(Duration.zero);
  32 + expect(remote.requests, hasLength(4));
  33 +
  34 + // Newer ranges/types finish first; the oldest heart-rate range stays pending.
  35 + for (var i = remote.requests.length - 1; i > 0; i--) {
  36 + remote.complete(i);
  37 + await Future<void>.delayed(Duration.zero);
  38 + expect(snapshotReady, isFalse);
  39 + expect(local.storedBatches, isEmpty);
  40 + }
  41 + remote.complete(0);
  42 + final snapshot = await future;
  43 + await snapshot.storeFuture;
  44 +
  45 + expect(snapshot.rawData.length, 8);
  46 + expect(
  47 + snapshot.newCalculationDataTypes,
  48 + containsAll(<int>[2, OhosHealthRawDataType.sleepAnalysis]),
  49 + );
  50 + expect(snapshot.results.fold<int>(0, (n, r) => n + r.pageCount), 4);
  51 + expect(local.storedBatches, hasLength(2));
  52 + expect(local.storedBatches.map((batch) => batch.length), [6, 2]);
  53 + for (final batch in local.storedBatches) {
  54 + final times = batch.map((item) => item.dataTime).toList();
  55 + expect(times, orderedEquals([...times]..sort()));
  56 + }
  57 + });
  58 +
  59 + test('failed snapshot segment prevents partial batch storage', () async {
  60 + final remote = _ControlledSnapshotRemote();
  61 + final local = _FakeOhosHealthRawDataLocalStore();
  62 + final service = OhosHealthRawDataSyncService(
  63 + remoteDataSource: remote,
  64 + localStore: local,
  65 + );
  66 + final future = service.syncCalculationRawDataSnapshot(
  67 + startTime: _unixSeconds(DateTime(2026, 1, 30)),
  68 + endTime: _unixSeconds(DateTime(2026, 2, 22)),
  69 + dataTypes: [2],
  70 + );
  71 + final failure = expectLater(future, throwsStateError);
  72 + await Future<void>.delayed(Duration.zero);
  73 + expect(remote.requests, hasLength(3));
  74 + remote.complete(2);
  75 + remote.complete(1);
  76 + await Future<void>.delayed(Duration.zero);
  77 + expect(local.storedBatches, isEmpty);
  78 + remote.requests.first.response.completeError(StateError('oldest failed'));
  79 + await failure;
  80 + expect(local.storedBatches, isEmpty);
  81 + });
  82 +
  83 + test('calculation snapshot ignores existing HR and sleep time keys',
  84 + () async {
  85 + const heartRateTime = 1769904001;
  86 + const sleepEndTime = 1769907600;
  87 + final remote = _FakeOhosHealthRawDataRemoteDataSource([
  88 + const OhosHealthRawDataPage(
  89 + items: [
  90 + OhosHealthRawDataItem(
  91 + dataType: 2,
  92 + dataTime: heartRateTime,
  93 + payload: {'time': heartRateTime, 'value': 70},
  94 + ),
  95 + ],
  96 + ),
  97 + const OhosHealthRawDataPage(
  98 + items: [
  99 + OhosHealthRawDataItem(
  100 + dataType: OhosHealthRawDataType.sleepAnalysis,
  101 + dataTime: sleepEndTime,
  102 + payload: {'from_time': 1769886000, 'to_time': sleepEndTime},
  103 + ),
  104 + ],
  105 + ),
  106 + ]);
  107 + final local = _FakeOhosHealthRawDataLocalStore(
  108 + existingItems: const [
  109 + OhosHealthRawDataItem(
  110 + dataType: 2,
  111 + dataTime: heartRateTime,
  112 + payload: {'time': heartRateTime, 'value': 70},
  113 + ),
  114 + OhosHealthRawDataItem(
  115 + dataType: OhosHealthRawDataType.sleepAnalysis,
  116 + dataTime: sleepEndTime,
  117 + payload: {'from_time': 1769886000, 'to_time': sleepEndTime},
  118 + ),
  119 + ],
  120 + );
  121 + final service = OhosHealthRawDataSyncService(
  122 + remoteDataSource: remote,
  123 + localStore: local,
  124 + );
  125 +
  126 + final snapshot = await service.syncCalculationRawDataSnapshot(
  127 + startTime: 1769817600,
  128 + endTime: 1769907600,
  129 + dataTypes: const [2, OhosHealthRawDataType.sleepAnalysis],
  130 + );
  131 +
  132 + expect(snapshot.newCalculationDataTypes, isEmpty);
  133 + expect(snapshot.hasNewCalculationData, isFalse);
  134 + });
  135 +
14 test('syncRawData starts from latest local data time and stores fetched data', 136 test('syncRawData starts from latest local data time and stores fetched data',
15 () async { 137 () async {
16 final remote = _FakeOhosHealthRawDataRemoteDataSource([ 138 final remote = _FakeOhosHealthRawDataRemoteDataSource([
@@ -375,7 +497,8 @@ void main() { @@ -375,7 +497,8 @@ void main() {
375 expect(result.pageCount, 3); 497 expect(result.pageCount, 3);
376 }); 498 });
377 499
378 - test('syncRawData limits segment fetch concurrency to ten', () async { 500 + test('syncRawData starts every segment within the two-month fetch range',
  501 + () async {
379 final start = _unixSeconds(DateTime(2026, 1)); 502 final start = _unixSeconds(DateTime(2026, 1));
380 final end = _unixSeconds(DateTime(2026, 4, 30)); 503 final end = _unixSeconds(DateTime(2026, 4, 30));
381 final gate = Completer<void>(); 504 final gate = Completer<void>();
@@ -393,18 +516,18 @@ void main() { @@ -393,18 +516,18 @@ void main() {
393 startTime: start, 516 startTime: start,
394 endTime: end, 517 endTime: end,
395 ); 518 );
396 - for (var i = 0; i < 20 && remote.calls.length < 10; i += 1) { 519 + for (var i = 0; i < 20 && remote.calls.length < 6; i += 1) {
397 await Future<void>.delayed(const Duration(milliseconds: 1)); 520 await Future<void>.delayed(const Duration(milliseconds: 1));
398 } 521 }
399 522
400 - expect(remote.calls, hasLength(10)); 523 + expect(remote.calls, hasLength(6));
401 524
402 gate.complete(); 525 gate.complete();
403 final result = await sync; 526 final result = await sync;
404 527
405 - expect(result.segmentCount, 12);  
406 - expect(result.pageCount, 12);  
407 - expect(remote.calls, hasLength(12)); 528 + expect(result.segmentCount, 6);
  529 + expect(result.pageCount, 6);
  530 + expect(remote.calls, hasLength(6));
408 }); 531 });
409 532
410 test('syncRawData joins duplicate in-flight syncs', () async { 533 test('syncRawData joins duplicate in-flight syncs', () async {
@@ -807,11 +930,14 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { @@ -807,11 +930,14 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
807 _FakeOhosHealthRawDataLocalStore({ 930 _FakeOhosHealthRawDataLocalStore({
808 int? latestDataTime, 931 int? latestDataTime,
809 Map<int, int>? latestDataTimeByType, 932 Map<int, int>? latestDataTimeByType,
  933 + List<OhosHealthRawDataItem> existingItems = const <OhosHealthRawDataItem>[],
810 }) : _latestDataTime = latestDataTime, 934 }) : _latestDataTime = latestDataTime,
811 - _latestDataTimeByType = latestDataTimeByType ?? const <int, int>{}; 935 + _latestDataTimeByType = latestDataTimeByType ?? const <int, int>{},
  936 + _existingItems = existingItems;
812 937
813 final int? _latestDataTime; 938 final int? _latestDataTime;
814 final Map<int, int> _latestDataTimeByType; 939 final Map<int, int> _latestDataTimeByType;
  940 + final List<OhosHealthRawDataItem> _existingItems;
815 final List<List<OhosHealthRawDataItem>> storedBatches = 941 final List<List<OhosHealthRawDataItem>> storedBatches =
816 <List<OhosHealthRawDataItem>>[]; 942 <List<OhosHealthRawDataItem>>[];
817 V2ActivityTarget? activityGoal; 943 V2ActivityTarget? activityGoal;
@@ -836,8 +962,10 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { @@ -836,8 +962,10 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
836 required int startTime, 962 required int startTime,
837 required int endTime, 963 required int endTime,
838 }) async { 964 }) async {
839 - return storedBatches  
840 - .expand((batch) => batch) 965 + return <OhosHealthRawDataItem>[
  966 + ..._existingItems,
  967 + ...storedBatches.expand((batch) => batch),
  968 + ]
841 .where( 969 .where(
842 (item) => 970 (item) =>
843 item.dataType == dataType && 971 item.dataType == dataType &&
@@ -967,3 +1095,41 @@ int _dateKey(int seconds) { @@ -967,3 +1095,41 @@ int _dateKey(int seconds) {
967 final dateTime = DateTime.fromMillisecondsSinceEpoch(seconds * 1000); 1095 final dateTime = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
968 return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day; 1096 return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day;
969 } 1097 }
  1098 +
  1099 +class _ControlledSnapshotRemote implements OhosHealthRawDataRemoteDataSource {
  1100 + final requests = <({
  1101 + int dataType,
  1102 + int start,
  1103 + Completer<OhosHealthRawDataPage> response
  1104 + })>[];
  1105 +
  1106 + @override
  1107 + Future<OhosHealthRawDataPage> fetchRawDataPage({
  1108 + required int dataType,
  1109 + required int startTime,
  1110 + required int endTime,
  1111 + }) {
  1112 + final response = Completer<OhosHealthRawDataPage>();
  1113 + requests.add((dataType: dataType, start: startTime, response: response));
  1114 + return response.future;
  1115 + }
  1116 +
  1117 + void complete(int index) {
  1118 + final request = requests[index];
  1119 + request.response.complete(OhosHealthRawDataPage(items: [
  1120 + for (final offset in [2, 1])
  1121 + OhosHealthRawDataItem(
  1122 + dataType: request.dataType,
  1123 + dataTime: request.start + offset,
  1124 + payload: {
  1125 + 'time': request.start + offset,
  1126 + 'from_time': request.start + offset,
  1127 + 'to_time': request.start + offset,
  1128 + },
  1129 + ),
  1130 + ]));
  1131 + }
  1132 +
  1133 + @override
  1134 + Future<V2ActivityTarget?> fetchActivityGoal() async => null;
  1135 +}