Commit 8d7571c7b83eadcdc9a87ce9664c8c113777a24a

Authored by 权海
1 parent 5d3e13ed

feat(ui):本地推送条件后置单独记录

@@ -449,9 +449,30 @@ class AppleHealthRawDataCoreService { @@ -449,9 +449,30 @@ class AppleHealthRawDataCoreService {
449 'latest_sleep': _latestSleepResultPayload(result.sleepResults), 449 'latest_sleep': _latestSleepResultPayload(result.sleepResults),
450 }, 450 },
451 ); 451 );
452 - if (result.hrvStressPoints.isEmpty &&  
453 - result.realtimeStressPoints.isEmpty &&  
454 - result.sleepResults.isEmpty) { 452 + final record = await _readLocalNotificationRecordSafely(result.userId);
  453 + await _saveLocalNotificationDebugEvent(
  454 + <String, Object?>{
  455 + 'event': 'flutterNotificationRecordRead',
  456 + 'user_id': result.userId,
  457 + 'record': _localNotificationRecordPayload(record),
  458 + },
  459 + );
  460 + final candidateData = await _resolveLocalNotificationCandidates(
  461 + result: result,
  462 + record: record,
  463 + );
  464 + await _saveLocalNotificationDebugEvent(
  465 + <String, Object?>{
  466 + 'event': 'flutterNotificationLatestDataTimeChecked',
  467 + 'user_id': result.userId,
  468 + 'record': _localNotificationRecordPayload(record),
  469 + 'latest_data_time': candidateData.latestDataTimePayload,
  470 + },
  471 + );
  472 + final candidateResult = candidateData.result;
  473 + if (candidateResult.hrvStressPoints.isEmpty &&
  474 + candidateResult.realtimeStressPoints.isEmpty &&
  475 + candidateResult.sleepResults.isEmpty) {
455 await _saveLocalNotificationDebugEvent( 476 await _saveLocalNotificationDebugEvent(
456 <String, Object?>{ 477 <String, Object?>{
457 'event': 'flutterNotificationNotBuilt', 478 'event': 'flutterNotificationNotBuilt',
@@ -462,12 +483,13 @@ class AppleHealthRawDataCoreService { @@ -462,12 +483,13 @@ class AppleHealthRawDataCoreService {
462 return; 483 return;
463 } 484 }
464 485
465 - final latestRealtimeStressPoint = result.realtimeStressPoints.isEmpty  
466 - ? null  
467 - : result.realtimeStressPoints.reduce(  
468 - (a, b) => a.rawEndTime >= b.rawEndTime ? a : b,  
469 - );  
470 - var notificationResult = result; 486 + final latestRealtimeStressPoint =
  487 + candidateResult.realtimeStressPoints.isEmpty
  488 + ? null
  489 + : candidateResult.realtimeStressPoints.reduce(
  490 + (a, b) => a.rawEndTime >= b.rawEndTime ? a : b,
  491 + );
  492 + var notificationResult = candidateResult;
471 var realtimeWindow = const <HealthRawRealtimeStressPoint>[]; 493 var realtimeWindow = const <HealthRawRealtimeStressPoint>[];
472 if (latestRealtimeStressPoint != null) { 494 if (latestRealtimeStressPoint != null) {
473 final shouldResetRealtimeStressPushTime = 495 final shouldResetRealtimeStressPushTime =
@@ -484,36 +506,28 @@ class AppleHealthRawDataCoreService { @@ -484,36 +506,28 @@ class AppleHealthRawDataCoreService {
484 }, 506 },
485 ); 507 );
486 await _recordRealtimeStressTimeSafely( 508 await _recordRealtimeStressTimeSafely(
487 - userId: result.userId, 509 + userId: candidateResult.userId,
488 recordTime: latestRealtimeStressPoint.rawEndTime, 510 recordTime: latestRealtimeStressPoint.rawEndTime,
489 ); 511 );
490 - notificationResult = result.copyWith( 512 + notificationResult = candidateResult.copyWith(
491 realtimeStressPoints: const <HealthRawRealtimeStressPoint>[], 513 realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
492 ); 514 );
493 } else { 515 } else {
494 realtimeWindow = await _queryRealtimeStressNotificationWindowSafely( 516 realtimeWindow = await _queryRealtimeStressNotificationWindowSafely(
495 - userId: result.userId, 517 + userId: candidateResult.userId,
496 latestRawEndTime: latestRealtimeStressPoint.rawEndTime, 518 latestRawEndTime: latestRealtimeStressPoint.rawEndTime,
497 ); 519 );
498 } 520 }
499 } 521 }
500 522
501 - final record = await _readLocalNotificationRecordSafely(result.userId);  
502 - await _saveLocalNotificationDebugEvent(  
503 - <String, Object?>{  
504 - 'event': 'flutterNotificationRecordRead',  
505 - 'user_id': result.userId,  
506 - 'record': _localNotificationRecordPayload(record),  
507 - },  
508 - );  
509 await _saveLocalNotificationDebugEvent( 523 await _saveLocalNotificationDebugEvent(
510 <String, Object?>{ 524 <String, Object?>{
511 'event': 'flutterNotificationBuildDecisions', 525 'event': 'flutterNotificationBuildDecisions',
512 - 'user_id': result.userId, 526 + 'user_id': candidateResult.userId,
513 'decisions': _localNotificationDecisionPayloads( 527 'decisions': _localNotificationDecisionPayloads(
514 result: notificationResult, 528 result: notificationResult,
515 - hasExistingHrv: hasExistingHrv,  
516 - hasExistingSleep: hasExistingSleep, 529 + hasExistingHrv: candidateData.hasExistingHrvDataTime,
  530 + hasExistingSleep: candidateData.hasExistingSleepDataTime,
517 realtimeWindow: realtimeWindow, 531 realtimeWindow: realtimeWindow,
518 record: record, 532 record: record,
519 ), 533 ),
@@ -521,15 +535,15 @@ class AppleHealthRawDataCoreService { @@ -521,15 +535,15 @@ class AppleHealthRawDataCoreService {
521 ); 535 );
522 final notifications = HealthRawLocalNotificationBuilder(l10n).build( 536 final notifications = HealthRawLocalNotificationBuilder(l10n).build(
523 result: notificationResult, 537 result: notificationResult,
524 - hasExistingHrv: hasExistingHrv,  
525 - hasExistingSleep: hasExistingSleep, 538 + hasExistingHrv: candidateData.hasExistingHrvDataTime,
  539 + hasExistingSleep: candidateData.hasExistingSleepDataTime,
526 realtimeWindow: realtimeWindow, 540 realtimeWindow: realtimeWindow,
527 record: record, 541 record: record,
528 ); 542 );
529 await _saveLocalNotificationDebugEvent( 543 await _saveLocalNotificationDebugEvent(
530 <String, Object?>{ 544 <String, Object?>{
531 'event': 'flutterNotificationBuildFinished', 545 'event': 'flutterNotificationBuildFinished',
532 - 'user_id': result.userId, 546 + 'user_id': candidateResult.userId,
533 'notification_count': notifications.length, 547 'notification_count': notifications.length,
534 'notifications': notifications.map(_localNotificationPayload).toList(), 548 'notifications': notifications.map(_localNotificationPayload).toList(),
535 }, 549 },
@@ -539,7 +553,7 @@ class AppleHealthRawDataCoreService { @@ -539,7 +553,7 @@ class AppleHealthRawDataCoreService {
539 <String, Object?>{ 553 <String, Object?>{
540 'event': 'flutterNotificationNotSent', 554 'event': 'flutterNotificationNotSent',
541 'reason': 'no_built_notifications', 555 'reason': 'no_built_notifications',
542 - 'user_id': result.userId, 556 + 'user_id': candidateResult.userId,
543 }, 557 },
544 ); 558 );
545 return; 559 return;
@@ -550,18 +564,18 @@ class AppleHealthRawDataCoreService { @@ -550,18 +564,18 @@ class AppleHealthRawDataCoreService {
550 await _saveLocalNotificationDebugEvent( 564 await _saveLocalNotificationDebugEvent(
551 <String, Object?>{ 565 <String, Object?>{
552 'event': 'flutterNotificationSendStart', 566 'event': 'flutterNotificationSendStart',
553 - 'user_id': result.userId, 567 + 'user_id': candidateResult.userId,
554 'notification': _localNotificationPayload(notification), 568 'notification': _localNotificationPayload(notification),
555 }, 569 },
556 ); 570 );
557 final sent = await _sendLocalNotificationSafely( 571 final sent = await _sendLocalNotificationSafely(
558 - userId: result.userId, 572 + userId: candidateResult.userId,
559 notification: notification, 573 notification: notification,
560 ); 574 );
561 await _saveLocalNotificationDebugEvent( 575 await _saveLocalNotificationDebugEvent(
562 <String, Object?>{ 576 <String, Object?>{
563 'event': 'flutterNotificationSendFinished', 577 'event': 'flutterNotificationSendFinished',
564 - 'user_id': result.userId, 578 + 'user_id': candidateResult.userId,
565 'sent': sent, 579 'sent': sent,
566 'reason': sent ? null : 'native_or_dispatcher_returned_false', 580 'reason': sent ? null : 'native_or_dispatcher_returned_false',
567 'notification': _localNotificationPayload(notification), 581 'notification': _localNotificationPayload(notification),
@@ -574,6 +588,93 @@ class AppleHealthRawDataCoreService { @@ -574,6 +588,93 @@ class AppleHealthRawDataCoreService {
574 _scheduleRealtimeStressServerPush(sentNotifications); 588 _scheduleRealtimeStressServerPush(sentNotifications);
575 } 589 }
576 590
  591 + Future<_HealthRawLocalNotificationCandidateData>
  592 + _resolveLocalNotificationCandidates({
  593 + required HealthRawStressCalculationResult result,
  594 + required HealthRawLocalNotificationRecord record,
  595 + }) async {
  596 + final userId = result.userId;
  597 + final latestHrvRawEndTime = await _localStore.latestHrvRawEndTime(userId);
  598 + final latestRealtimeRawEndTime =
  599 + await _localStore.latestRealtimeRawEndTime(userId);
  600 + final latestSleepDataTime = await _localStore.latestSleepResultTime(userId);
  601 +
  602 + var hrvPoints = const <HealthRawHrvStressPoint>[];
  603 + var realtimePoints = const <HealthRawRealtimeStressPoint>[];
  604 + var sleepResults = const <HealthRawSleepResult>[];
  605 + int? nextHrvDataTime;
  606 + int? nextRealtimeStressDataTime;
  607 + int? nextSleepDataTime;
  608 +
  609 + final hrvDataTime = record.latestHrvDataTime;
  610 + if (latestHrvRawEndTime != null) {
  611 + if (hrvDataTime == null) {
  612 + nextHrvDataTime = latestHrvRawEndTime;
  613 + } else if (hrvDataTime < latestHrvRawEndTime) {
  614 + hrvPoints = await _localStore.queryHrvStressPoints(
  615 + userId: userId,
  616 + startTime: hrvDataTime + 1,
  617 + endTime: latestHrvRawEndTime,
  618 + );
  619 + nextHrvDataTime = latestHrvRawEndTime;
  620 + }
  621 + }
  622 +
  623 + final realtimeDataTime = record.latestRealtimeStressDataTime;
  624 + if (latestRealtimeRawEndTime != null) {
  625 + if (realtimeDataTime == null) {
  626 + nextRealtimeStressDataTime = latestRealtimeRawEndTime;
  627 + } else if (realtimeDataTime < latestRealtimeRawEndTime) {
  628 + realtimePoints = await _localStore.queryRealtimeStressPoints(
  629 + userId: userId,
  630 + startTime: realtimeDataTime + 1,
  631 + endTime: latestRealtimeRawEndTime,
  632 + );
  633 + nextRealtimeStressDataTime = latestRealtimeRawEndTime;
  634 + }
  635 + }
  636 +
  637 + final sleepDataTime = record.latestSleepDataTime;
  638 + if (latestSleepDataTime != null) {
  639 + if (sleepDataTime == null) {
  640 + nextSleepDataTime = latestSleepDataTime;
  641 + } else if (sleepDataTime < latestSleepDataTime) {
  642 + sleepResults = await _localStore.querySleepResults(
  643 + userId: userId,
  644 + startTime: sleepDataTime + 1,
  645 + endTime: latestSleepDataTime,
  646 + );
  647 + nextSleepDataTime = latestSleepDataTime;
  648 + }
  649 + }
  650 +
  651 + await _updateProcessedDataTimesSafely(
  652 + userId: userId,
  653 + latestHrvDataTime: nextHrvDataTime,
  654 + latestRealtimeStressDataTime: nextRealtimeStressDataTime,
  655 + latestSleepDataTime: nextSleepDataTime,
  656 + );
  657 +
  658 + return _HealthRawLocalNotificationCandidateData(
  659 + result: HealthRawStressCalculationResult(
  660 + userId: userId,
  661 + hrvStressPoints: hrvPoints,
  662 + realtimeStressPoints: realtimePoints,
  663 + dailyStressPoints: result.dailyStressPoints,
  664 + sleepResults: sleepResults,
  665 + ),
  666 + hasExistingHrvDataTime: hrvDataTime != null,
  667 + hasExistingRealtimeStressDataTime: realtimeDataTime != null,
  668 + hasExistingSleepDataTime: sleepDataTime != null,
  669 + currentHrvDataTime: latestHrvRawEndTime,
  670 + currentRealtimeStressDataTime: latestRealtimeRawEndTime,
  671 + currentSleepDataTime: latestSleepDataTime,
  672 + nextHrvDataTime: nextHrvDataTime,
  673 + nextRealtimeStressDataTime: nextRealtimeStressDataTime,
  674 + nextSleepDataTime: nextSleepDataTime,
  675 + );
  676 + }
  677 +
577 Future<HealthRawLocalNotificationRecord> _readLocalNotificationRecordSafely( 678 Future<HealthRawLocalNotificationRecord> _readLocalNotificationRecordSafely(
578 int userId, 679 int userId,
579 ) async { 680 ) async {
@@ -585,6 +686,28 @@ class AppleHealthRawDataCoreService { @@ -585,6 +686,28 @@ class AppleHealthRawDataCoreService {
585 } 686 }
586 } 687 }
587 688
  689 + Future<void> _updateProcessedDataTimesSafely({
  690 + required int userId,
  691 + int? latestHrvDataTime,
  692 + int? latestRealtimeStressDataTime,
  693 + int? latestSleepDataTime,
  694 + }) async {
  695 + try {
  696 + await _localNotificationDispatcher.updateProcessedDataTimes(
  697 + userId: userId,
  698 + latestHrvDataTime: latestHrvDataTime,
  699 + latestRealtimeStressDataTime: latestRealtimeStressDataTime,
  700 + latestSleepDataTime: latestSleepDataTime,
  701 + );
  702 + } catch (error, stackTrace) {
  703 + _logError(
  704 + 'update local notification processed data time failed',
  705 + error,
  706 + stackTrace,
  707 + );
  708 + }
  709 + }
  710 +
588 Future<void> _recordRealtimeStressTimeSafely({ 711 Future<void> _recordRealtimeStressTimeSafely({
589 required int userId, 712 required int userId,
590 required int recordTime, 713 required int recordTime,
@@ -869,6 +992,9 @@ class AppleHealthRawDataCoreService { @@ -869,6 +992,9 @@ class AppleHealthRawDataCoreService {
869 'last_sleep_time': record.lastSleepTime, 992 'last_sleep_time': record.lastSleepTime,
870 'last_hrv_time': record.lastHrvTime, 993 'last_hrv_time': record.lastHrvTime,
871 'last_realtime_stress_time': record.lastRealtimeStressTime, 994 'last_realtime_stress_time': record.lastRealtimeStressTime,
  995 + 'latest_hrv_data_time': record.latestHrvDataTime,
  996 + 'latest_realtime_stress_data_time': record.latestRealtimeStressDataTime,
  997 + 'latest_sleep_data_time': record.latestSleepDataTime,
872 }; 998 };
873 } 999 }
874 1000
@@ -1680,6 +1806,50 @@ class HealthRawStressDbRows { @@ -1680,6 +1806,50 @@ class HealthRawStressDbRows {
1680 final List<Map<String, Object?>> sleepRows; 1806 final List<Map<String, Object?>> sleepRows;
1681 } 1807 }
1682 1808
  1809 +class _HealthRawLocalNotificationCandidateData {
  1810 + const _HealthRawLocalNotificationCandidateData({
  1811 + required this.result,
  1812 + required this.hasExistingHrvDataTime,
  1813 + required this.hasExistingRealtimeStressDataTime,
  1814 + required this.hasExistingSleepDataTime,
  1815 + required this.currentHrvDataTime,
  1816 + required this.currentRealtimeStressDataTime,
  1817 + required this.currentSleepDataTime,
  1818 + required this.nextHrvDataTime,
  1819 + required this.nextRealtimeStressDataTime,
  1820 + required this.nextSleepDataTime,
  1821 + });
  1822 +
  1823 + final HealthRawStressCalculationResult result;
  1824 + final bool hasExistingHrvDataTime;
  1825 + final bool hasExistingRealtimeStressDataTime;
  1826 + final bool hasExistingSleepDataTime;
  1827 + final int? currentHrvDataTime;
  1828 + final int? currentRealtimeStressDataTime;
  1829 + final int? currentSleepDataTime;
  1830 + final int? nextHrvDataTime;
  1831 + final int? nextRealtimeStressDataTime;
  1832 + final int? nextSleepDataTime;
  1833 +
  1834 + Map<String, Object?> get latestDataTimePayload {
  1835 + return <String, Object?>{
  1836 + 'has_existing_hrv_data_time': hasExistingHrvDataTime,
  1837 + 'has_existing_realtime_stress_data_time':
  1838 + hasExistingRealtimeStressDataTime,
  1839 + 'has_existing_sleep_data_time': hasExistingSleepDataTime,
  1840 + 'current_hrv_data_time': currentHrvDataTime,
  1841 + 'current_realtime_stress_data_time': currentRealtimeStressDataTime,
  1842 + 'current_sleep_data_time': currentSleepDataTime,
  1843 + 'next_hrv_data_time': nextHrvDataTime,
  1844 + 'next_realtime_stress_data_time': nextRealtimeStressDataTime,
  1845 + 'next_sleep_data_time': nextSleepDataTime,
  1846 + 'candidate_hrv_count': result.hrvStressPoints.length,
  1847 + 'candidate_realtime_count': result.realtimeStressPoints.length,
  1848 + 'candidate_sleep_count': result.sleepResults.length,
  1849 + };
  1850 + }
  1851 +}
  1852 +
1683 double _integerDouble(num value) => value.toInt().toDouble(); 1853 double _integerDouble(num value) => value.toInt().toDouble();
1684 1854
1685 class HealthRawStressLocalStore { 1855 class HealthRawStressLocalStore {
@@ -289,6 +289,28 @@ class HealthRawLocalNotificationDispatcher { @@ -289,6 +289,28 @@ class HealthRawLocalNotificationDispatcher {
289 record.copyWith(lastRealtimeStressTime: recordTime), 289 record.copyWith(lastRealtimeStressTime: recordTime),
290 ); 290 );
291 } 291 }
  292 +
  293 + Future<void> updateProcessedDataTimes({
  294 + required int userId,
  295 + int? latestHrvDataTime,
  296 + int? latestRealtimeStressDataTime,
  297 + int? latestSleepDataTime,
  298 + }) async {
  299 + if (latestHrvDataTime == null &&
  300 + latestRealtimeStressDataTime == null &&
  301 + latestSleepDataTime == null) {
  302 + return;
  303 + }
  304 + final record = await _recordStore.read(userId);
  305 + await _recordStore.write(
  306 + userId,
  307 + record.copyWith(
  308 + latestHrvDataTime: latestHrvDataTime,
  309 + latestRealtimeStressDataTime: latestRealtimeStressDataTime,
  310 + latestSleepDataTime: latestSleepDataTime,
  311 + ),
  312 + );
  313 + }
292 } 314 }
293 315
294 class HealthRawLocalNotificationRecord { 316 class HealthRawLocalNotificationRecord {
@@ -296,11 +318,17 @@ class HealthRawLocalNotificationRecord { @@ -296,11 +318,17 @@ class HealthRawLocalNotificationRecord {
296 this.lastSleepTime, 318 this.lastSleepTime,
297 this.lastHrvTime, 319 this.lastHrvTime,
298 this.lastRealtimeStressTime, 320 this.lastRealtimeStressTime,
  321 + this.latestHrvDataTime,
  322 + this.latestRealtimeStressDataTime,
  323 + this.latestSleepDataTime,
299 }); 324 });
300 325
301 final int? lastSleepTime; 326 final int? lastSleepTime;
302 final int? lastHrvTime; 327 final int? lastHrvTime;
303 final int? lastRealtimeStressTime; 328 final int? lastRealtimeStressTime;
  329 + final int? latestHrvDataTime;
  330 + final int? latestRealtimeStressDataTime;
  331 + final int? latestSleepDataTime;
304 332
305 factory HealthRawLocalNotificationRecord.fromJson(Map<String, Object?> json) { 333 factory HealthRawLocalNotificationRecord.fromJson(Map<String, Object?> json) {
306 return HealthRawLocalNotificationRecord( 334 return HealthRawLocalNotificationRecord(
@@ -308,6 +336,10 @@ class HealthRawLocalNotificationRecord { @@ -308,6 +336,10 @@ class HealthRawLocalNotificationRecord {
308 lastHrvTime: (json['last_hrv_time'] as num?)?.toInt(), 336 lastHrvTime: (json['last_hrv_time'] as num?)?.toInt(),
309 lastRealtimeStressTime: 337 lastRealtimeStressTime:
310 (json['last_realtime_stress_time'] as num?)?.toInt(), 338 (json['last_realtime_stress_time'] as num?)?.toInt(),
  339 + latestHrvDataTime: (json['latest_hrv_data_time'] as num?)?.toInt(),
  340 + latestRealtimeStressDataTime:
  341 + (json['latest_realtime_stress_data_time'] as num?)?.toInt(),
  342 + latestSleepDataTime: (json['latest_sleep_data_time'] as num?)?.toInt(),
311 ); 343 );
312 } 344 }
313 345
@@ -317,6 +349,11 @@ class HealthRawLocalNotificationRecord { @@ -317,6 +349,11 @@ class HealthRawLocalNotificationRecord {
317 if (lastHrvTime != null) 'last_hrv_time': lastHrvTime, 349 if (lastHrvTime != null) 'last_hrv_time': lastHrvTime,
318 if (lastRealtimeStressTime != null) 350 if (lastRealtimeStressTime != null)
319 'last_realtime_stress_time': lastRealtimeStressTime, 351 'last_realtime_stress_time': lastRealtimeStressTime,
  352 + if (latestHrvDataTime != null) 'latest_hrv_data_time': latestHrvDataTime,
  353 + if (latestRealtimeStressDataTime != null)
  354 + 'latest_realtime_stress_data_time': latestRealtimeStressDataTime,
  355 + if (latestSleepDataTime != null)
  356 + 'latest_sleep_data_time': latestSleepDataTime,
320 }; 357 };
321 } 358 }
322 359
@@ -340,12 +377,19 @@ class HealthRawLocalNotificationRecord { @@ -340,12 +377,19 @@ class HealthRawLocalNotificationRecord {
340 int? lastSleepTime, 377 int? lastSleepTime,
341 int? lastHrvTime, 378 int? lastHrvTime,
342 int? lastRealtimeStressTime, 379 int? lastRealtimeStressTime,
  380 + int? latestHrvDataTime,
  381 + int? latestRealtimeStressDataTime,
  382 + int? latestSleepDataTime,
343 }) { 383 }) {
344 return HealthRawLocalNotificationRecord( 384 return HealthRawLocalNotificationRecord(
345 lastSleepTime: lastSleepTime ?? this.lastSleepTime, 385 lastSleepTime: lastSleepTime ?? this.lastSleepTime,
346 lastHrvTime: lastHrvTime ?? this.lastHrvTime, 386 lastHrvTime: lastHrvTime ?? this.lastHrvTime,
347 lastRealtimeStressTime: 387 lastRealtimeStressTime:
348 lastRealtimeStressTime ?? this.lastRealtimeStressTime, 388 lastRealtimeStressTime ?? this.lastRealtimeStressTime,
  389 + latestHrvDataTime: latestHrvDataTime ?? this.latestHrvDataTime,
  390 + latestRealtimeStressDataTime:
  391 + latestRealtimeStressDataTime ?? this.latestRealtimeStressDataTime,
  392 + latestSleepDataTime: latestSleepDataTime ?? this.latestSleepDataTime,
349 ); 393 );
350 } 394 }
351 } 395 }
@@ -662,7 +662,11 @@ void main() { @@ -662,7 +662,11 @@ void main() {
662 ), 662 ),
663 ], 663 ],
664 ); 664 );
665 - final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher(); 665 + final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher()
  666 + ..record = HealthRawLocalNotificationRecord(
  667 + latestHrvDataTime: base + 10,
  668 + latestSleepDataTime: base - Duration.secondsPerDay,
  669 + );
666 final service = AppleHealthRawDataCoreService( 670 final service = AppleHealthRawDataCoreService(
667 healthApi: _FakeHealthKitHostApi(), 671 healthApi: _FakeHealthKitHostApi(),
668 rawDataApi: api, 672 rawDataApi: api,
@@ -688,6 +692,9 @@ void main() { @@ -688,6 +692,9 @@ void main() {
688 ), 692 ),
689 hasLength(1), 693 hasLength(1),
690 ); 694 );
  695 + expect(notificationDispatcher.record.latestHrvDataTime, base + 720);
  696 + expect(notificationDispatcher.record.latestSleepDataTime,
  697 + LocalHealthDataConvert.unixSeconds(sleepEnd));
691 }); 698 });
692 699
693 test( 700 test(
@@ -719,7 +726,11 @@ void main() { @@ -719,7 +726,11 @@ void main() {
719 ), 726 ),
720 ], 727 ],
721 ); 728 );
722 - final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher(); 729 + final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher()
  730 + ..record = const HealthRawLocalNotificationRecord(
  731 + latestHrvDataTime: base + 100,
  732 + latestSleepDataTime: base + 100,
  733 + );
723 final service = AppleHealthRawDataCoreService( 734 final service = AppleHealthRawDataCoreService(
724 healthApi: _FakeHealthKitHostApi(), 735 healthApi: _FakeHealthKitHostApi(),
725 rawDataApi: api, 736 rawDataApi: api,
@@ -742,6 +753,56 @@ void main() { @@ -742,6 +753,56 @@ void main() {
742 ), 753 ),
743 isEmpty, 754 isEmpty,
744 ); 755 );
  756 + expect(notificationDispatcher.record.latestHrvDataTime, base + 100);
  757 + expect(notificationDispatcher.record.latestSleepDataTime, base + 100);
  758 + });
  759 +
  760 + test(
  761 + 'startCoreCaculate sends realtime notification when database latest is newer than latest data time',
  762 + () async {
  763 + const base = 1800000000;
  764 + final api = _FakeHealthKitRawDataHostApi();
  765 + final store = _MemoryHealthRawStressLocalStore();
  766 + await store.upsertResult(
  767 + HealthRawStressCalculationResult(
  768 + userId: 42,
  769 + hrvStressPoints: const <HealthRawHrvStressPoint>[],
  770 + realtimeStressPoints: [
  771 + for (var i = 0; i < 10; i += 1)
  772 + _realtimeStressPoint(base + i * 300, result: 70),
  773 + ],
  774 + dailyStressPoints: const <HealthRawDailyStressPoint>[],
  775 + ),
  776 + );
  777 + final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher()
  778 + ..record = const HealthRawLocalNotificationRecord(
  779 + latestRealtimeStressDataTime: base,
  780 + );
  781 + final service = AppleHealthRawDataCoreService(
  782 + healthApi: _FakeHealthKitHostApi(),
  783 + rawDataApi: api,
  784 + localStore: store,
  785 + userIdProvider: () => 42,
  786 + uploadResultsAfterCalculation: false,
  787 + localNotificationDispatcher: notificationDispatcher,
  788 + );
  789 +
  790 + await service.startCoreCaculate(
  791 + endTime: base + 3000,
  792 + readChunkDays: 1,
  793 + );
  794 +
  795 + expect(
  796 + notificationDispatcher.sentNotifications.where(
  797 + (e) =>
  798 + e.recordType == HealthRawLocalNotificationRecordType.realtimeStress,
  799 + ),
  800 + hasLength(1),
  801 + );
  802 + expect(
  803 + notificationDispatcher.record.latestRealtimeStressDataTime,
  804 + base + 2700,
  805 + );
745 }); 806 });
746 807
747 test( 808 test(
@@ -797,6 +858,9 @@ void main() { @@ -797,6 +858,9 @@ void main() {
797 ), 858 ),
798 isEmpty, 859 isEmpty,
799 ); 860 );
  861 + expect(notificationDispatcher.record.latestHrvDataTime, base + 420);
  862 + expect(notificationDispatcher.record.latestSleepDataTime,
  863 + LocalHealthDataConvert.unixSeconds(sleepEnd));
800 }); 864 });
801 865
802 test( 866 test(
@@ -968,12 +1032,15 @@ HealthKitRawDataPoint _point( @@ -968,12 +1032,15 @@ HealthKitRawDataPoint _point(
968 ); 1032 );
969 } 1033 }
970 1034
971 -HealthRawRealtimeStressPoint _realtimeStressPoint(int time) { 1035 +HealthRawRealtimeStressPoint _realtimeStressPoint(
  1036 + int time, {
  1037 + double result = 30,
  1038 +}) {
972 return HealthRawRealtimeStressPoint( 1039 return HealthRawRealtimeStressPoint(
973 userId: 42, 1040 userId: 42,
974 rawEndTime: time, 1041 rawEndTime: time,
975 rawHr: 70, 1042 rawHr: 70,
976 - result: 30, 1043 + result: result,
977 sourceStartTime: time, 1044 sourceStartTime: time,
978 sourceEndTime: time, 1045 sourceEndTime: time,
979 ); 1046 );
@@ -1167,6 +1234,20 @@ class _FakeHealthRawLocalNotificationDispatcher @@ -1167,6 +1234,20 @@ class _FakeHealthRawLocalNotificationDispatcher
1167 }) async { 1234 }) async {
1168 record = record.copyWith(lastRealtimeStressTime: recordTime); 1235 record = record.copyWith(lastRealtimeStressTime: recordTime);
1169 } 1236 }
  1237 +
  1238 + @override
  1239 + Future<void> updateProcessedDataTimes({
  1240 + required int userId,
  1241 + int? latestHrvDataTime,
  1242 + int? latestRealtimeStressDataTime,
  1243 + int? latestSleepDataTime,
  1244 + }) async {
  1245 + record = record.copyWith(
  1246 + latestHrvDataTime: latestHrvDataTime,
  1247 + latestRealtimeStressDataTime: latestRealtimeStressDataTime,
  1248 + latestSleepDataTime: latestSleepDataTime,
  1249 + );
  1250 + }
1170 } 1251 }
1171 1252
1172 class _ReadCall { 1253 class _ReadCall {
@@ -292,6 +292,49 @@ void main() { @@ -292,6 +292,49 @@ void main() {
292 final record = await dispatcher.readRecord(1); 292 final record = await dispatcher.readRecord(1);
293 expect(record.lastRealtimeStressTime, 1234); 293 expect(record.lastRealtimeStressTime, 1234);
294 }); 294 });
  295 +
  296 + test('reads old notification record json without latest data times', () {
  297 + final record = HealthRawLocalNotificationRecord.fromJson(
  298 + const <String, Object?>{
  299 + 'last_sleep_time': 100,
  300 + 'last_hrv_time': 200,
  301 + 'last_realtime_stress_time': 300,
  302 + },
  303 + );
  304 +
  305 + expect(record.lastSleepTime, 100);
  306 + expect(record.lastHrvTime, 200);
  307 + expect(record.lastRealtimeStressTime, 300);
  308 + expect(record.latestHrvDataTime, isNull);
  309 + expect(record.latestRealtimeStressDataTime, isNull);
  310 + expect(record.latestSleepDataTime, isNull);
  311 + });
  312 +
  313 + test('stores processed latest data times independently', () async {
  314 + final dir = await Directory.systemTemp.createTemp(
  315 + 'health_raw_notification_test_',
  316 + );
  317 + addTearDown(() => dir.delete(recursive: true));
  318 + final dispatcher = HealthRawLocalNotificationDispatcher(
  319 + recordStore: HealthRawLocalNotificationRecordStore(rootDirectory: dir),
  320 + );
  321 +
  322 + await dispatcher.updateProcessedDataTimes(
  323 + userId: 1,
  324 + latestHrvDataTime: 100,
  325 + latestRealtimeStressDataTime: 200,
  326 + latestSleepDataTime: 300,
  327 + );
  328 + await dispatcher.updateProcessedDataTimes(
  329 + userId: 1,
  330 + latestRealtimeStressDataTime: 250,
  331 + );
  332 +
  333 + final record = await dispatcher.readRecord(1);
  334 + expect(record.latestHrvDataTime, 100);
  335 + expect(record.latestRealtimeStressDataTime, 250);
  336 + expect(record.latestSleepDataTime, 300);
  337 + });
295 } 338 }
296 339
297 int _seconds(DateTime time) => time.millisecondsSinceEpoch ~/ 1000; 340 int _seconds(DateTime time) => time.millisecondsSinceEpoch ~/ 1000;