|
@@ -16,6 +16,7 @@ import '../config/app_environment_config.dart'; |
|
@@ -16,6 +16,7 @@ import '../config/app_environment_config.dart'; |
|
16
|
import '../logging/app_logger.dart';
|
16
|
import '../logging/app_logger.dart';
|
|
17
|
import '../network/api/health_api.dart';
|
17
|
import '../network/api/health_api.dart';
|
|
18
|
import '../result/app_result.dart';
|
18
|
import '../result/app_result.dart';
|
|
|
|
19
|
+import 'health_raw_local_notification_debug_store.dart';
|
|
19
|
import 'health_raw_local_notification.dart';
|
20
|
import 'health_raw_local_notification.dart';
|
|
20
|
import 'health_raw_stress_calculator.dart';
|
21
|
import 'health_raw_stress_calculator.dart';
|
|
21
|
import 'health_sleep_calculator.dart';
|
22
|
import 'health_sleep_calculator.dart';
|
|
@@ -32,6 +33,7 @@ class HealthRawDataCoreService { |
|
@@ -32,6 +33,7 @@ class HealthRawDataCoreService { |
|
32
|
int Function()? userIdProvider,
|
33
|
int Function()? userIdProvider,
|
|
33
|
bool uploadResultsAfterCalculation = true,
|
34
|
bool uploadResultsAfterCalculation = true,
|
|
34
|
HealthRawLocalNotificationDispatcher? localNotificationDispatcher,
|
35
|
HealthRawLocalNotificationDispatcher? localNotificationDispatcher,
|
|
|
|
36
|
+ HealthRawLocalNotificationDebugStore? localNotificationDebugStore,
|
|
35
|
HealthApi? serverHealthApi,
|
37
|
HealthApi? serverHealthApi,
|
|
36
|
}) : _healthApi = healthApi ?? HealthKitHostApi(),
|
38
|
}) : _healthApi = healthApi ?? HealthKitHostApi(),
|
|
37
|
_rawDataApi = rawDataApi ?? HealthKitRawDataHostApi(),
|
39
|
_rawDataApi = rawDataApi ?? HealthKitRawDataHostApi(),
|
|
@@ -41,6 +43,8 @@ class HealthRawDataCoreService { |
|
@@ -41,6 +43,8 @@ class HealthRawDataCoreService { |
|
41
|
_uploadResultsAfterCalculation = uploadResultsAfterCalculation,
|
43
|
_uploadResultsAfterCalculation = uploadResultsAfterCalculation,
|
|
42
|
_localNotificationDispatcher = localNotificationDispatcher ??
|
44
|
_localNotificationDispatcher = localNotificationDispatcher ??
|
|
43
|
HealthRawLocalNotificationDispatcher(),
|
45
|
HealthRawLocalNotificationDispatcher(),
|
|
|
|
46
|
+ _localNotificationDebugStore = localNotificationDebugStore ??
|
|
|
|
47
|
+ HealthRawLocalNotificationDebugStore(),
|
|
44
|
_serverHealthApi = serverHealthApi;
|
48
|
_serverHealthApi = serverHealthApi;
|
|
45
|
|
49
|
|
|
46
|
final HealthKitHostApi _healthApi;
|
50
|
final HealthKitHostApi _healthApi;
|
|
@@ -50,6 +54,7 @@ class HealthRawDataCoreService { |
|
@@ -50,6 +54,7 @@ class HealthRawDataCoreService { |
|
50
|
final int Function()? _userIdProvider;
|
54
|
final int Function()? _userIdProvider;
|
|
51
|
final bool _uploadResultsAfterCalculation;
|
55
|
final bool _uploadResultsAfterCalculation;
|
|
52
|
final HealthRawLocalNotificationDispatcher _localNotificationDispatcher;
|
56
|
final HealthRawLocalNotificationDispatcher _localNotificationDispatcher;
|
|
|
|
57
|
+ final HealthRawLocalNotificationDebugStore _localNotificationDebugStore;
|
|
53
|
final HealthApi? _serverHealthApi;
|
58
|
final HealthApi? _serverHealthApi;
|
|
54
|
final StreamController<HealthRawDataUpdatedEvent>
|
59
|
final StreamController<HealthRawDataUpdatedEvent>
|
|
55
|
_healthDataUpdatedController =
|
60
|
_healthDataUpdatedController =
|
|
@@ -187,6 +192,19 @@ class HealthRawDataCoreService { |
|
@@ -187,6 +192,19 @@ class HealthRawDataCoreService { |
|
187
|
return _rawDataApi.shareUploadTaskRecord();
|
192
|
return _rawDataApi.shareUploadTaskRecord();
|
|
188
|
}
|
193
|
}
|
|
189
|
|
194
|
|
|
|
|
195
|
+ Future<String> readLocalNotificationDebugLogText() {
|
|
|
|
196
|
+ return _localNotificationDebugStore.readText(isDebug: _isDebug);
|
|
|
|
197
|
+ }
|
|
|
|
198
|
+
|
|
|
|
199
|
+ Future<bool> shareLocalNotificationDebugRecord() async {
|
|
|
|
200
|
+ if (!_isDebug) return false;
|
|
|
|
201
|
+ return _shareNativeLocalNotificationDebugRecord();
|
|
|
|
202
|
+ }
|
|
|
|
203
|
+
|
|
|
|
204
|
+ Future<bool> _shareNativeLocalNotificationDebugRecord() async {
|
|
|
|
205
|
+ return _rawDataApi.shareLocalNotificationDebugRecord();
|
|
|
|
206
|
+ }
|
|
|
|
207
|
+
|
|
190
|
Future<HealthRawStressCalculationResult> startCoreCaculate({
|
208
|
Future<HealthRawStressCalculationResult> startCoreCaculate({
|
|
191
|
int? endTime,
|
209
|
int? endTime,
|
|
192
|
int readChunkDays = defaultReadChunkDays,
|
210
|
int readChunkDays = defaultReadChunkDays,
|
|
@@ -399,9 +417,30 @@ class HealthRawDataCoreService { |
|
@@ -399,9 +417,30 @@ class HealthRawDataCoreService { |
|
399
|
required HealthRawStressCalculationResult result,
|
417
|
required HealthRawStressCalculationResult result,
|
|
400
|
required bool hasExistingHrv,
|
418
|
required bool hasExistingHrv,
|
|
401
|
}) async {
|
419
|
}) async {
|
|
|
|
420
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
421
|
+ <String, Object?>{
|
|
|
|
422
|
+ 'event': 'flutterNotificationCalculationFinished',
|
|
|
|
423
|
+ 'user_id': result.userId,
|
|
|
|
424
|
+ 'has_existing_hrv': hasExistingHrv,
|
|
|
|
425
|
+ 'hrv_count': result.hrvStressPoints.length,
|
|
|
|
426
|
+ 'realtime_count': result.realtimeStressPoints.length,
|
|
|
|
427
|
+ 'sleep_count': result.sleepResults.length,
|
|
|
|
428
|
+ 'latest_hrv': _latestHrvStressPointPayload(result.hrvStressPoints),
|
|
|
|
429
|
+ 'latest_realtime':
|
|
|
|
430
|
+ _latestRealtimeStressPointPayload(result.realtimeStressPoints),
|
|
|
|
431
|
+ 'latest_sleep': _latestSleepResultPayload(result.sleepResults),
|
|
|
|
432
|
+ },
|
|
|
|
433
|
+ );
|
|
402
|
if (result.hrvStressPoints.isEmpty &&
|
434
|
if (result.hrvStressPoints.isEmpty &&
|
|
403
|
result.realtimeStressPoints.isEmpty &&
|
435
|
result.realtimeStressPoints.isEmpty &&
|
|
404
|
result.sleepResults.isEmpty) {
|
436
|
result.sleepResults.isEmpty) {
|
|
|
|
437
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
438
|
+ <String, Object?>{
|
|
|
|
439
|
+ 'event': 'flutterNotificationNotBuilt',
|
|
|
|
440
|
+ 'reason': 'empty_calculation_result',
|
|
|
|
441
|
+ 'user_id': result.userId,
|
|
|
|
442
|
+ },
|
|
|
|
443
|
+ );
|
|
405
|
return;
|
444
|
return;
|
|
406
|
}
|
445
|
}
|
|
407
|
|
446
|
|
|
@@ -417,6 +456,15 @@ class HealthRawDataCoreService { |
|
@@ -417,6 +456,15 @@ class HealthRawDataCoreService { |
|
417
|
latestRealtimeStressPoint.isWorkout ||
|
456
|
latestRealtimeStressPoint.isWorkout ||
|
|
418
|
latestRealtimeStressPoint.isWorkoutRecovery;
|
457
|
latestRealtimeStressPoint.isWorkoutRecovery;
|
|
419
|
if (shouldResetRealtimeStressPushTime) {
|
458
|
if (shouldResetRealtimeStressPushTime) {
|
|
|
|
459
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
460
|
+ <String, Object?>{
|
|
|
|
461
|
+ 'event': 'flutterRealtimeStressSuppressedBeforeBuild',
|
|
|
|
462
|
+ 'reason': 'workout_or_recovery',
|
|
|
|
463
|
+ 'user_id': result.userId,
|
|
|
|
464
|
+ 'latest_realtime':
|
|
|
|
465
|
+ _realtimeStressPointPayload(latestRealtimeStressPoint),
|
|
|
|
466
|
+ },
|
|
|
|
467
|
+ );
|
|
420
|
await _recordRealtimeStressTimeSafely(
|
468
|
await _recordRealtimeStressTimeSafely(
|
|
421
|
userId: result.userId,
|
469
|
userId: result.userId,
|
|
422
|
recordTime: latestRealtimeStressPoint.rawEndTime,
|
470
|
recordTime: latestRealtimeStressPoint.rawEndTime,
|
|
@@ -433,20 +481,72 @@ class HealthRawDataCoreService { |
|
@@ -433,20 +481,72 @@ class HealthRawDataCoreService { |
|
433
|
}
|
481
|
}
|
|
434
|
|
482
|
|
|
435
|
final record = await _readLocalNotificationRecordSafely(result.userId);
|
483
|
final record = await _readLocalNotificationRecordSafely(result.userId);
|
|
|
|
484
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
485
|
+ <String, Object?>{
|
|
|
|
486
|
+ 'event': 'flutterNotificationRecordRead',
|
|
|
|
487
|
+ 'user_id': result.userId,
|
|
|
|
488
|
+ 'record': _localNotificationRecordPayload(record),
|
|
|
|
489
|
+ },
|
|
|
|
490
|
+ );
|
|
|
|
491
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
492
|
+ <String, Object?>{
|
|
|
|
493
|
+ 'event': 'flutterNotificationBuildDecisions',
|
|
|
|
494
|
+ 'user_id': result.userId,
|
|
|
|
495
|
+ 'decisions': _localNotificationDecisionPayloads(
|
|
|
|
496
|
+ result: notificationResult,
|
|
|
|
497
|
+ hasExistingHrv: hasExistingHrv,
|
|
|
|
498
|
+ realtimeWindow: realtimeWindow,
|
|
|
|
499
|
+ record: record,
|
|
|
|
500
|
+ ),
|
|
|
|
501
|
+ },
|
|
|
|
502
|
+ );
|
|
436
|
final notifications = HealthRawLocalNotificationBuilder(l10n).build(
|
503
|
final notifications = HealthRawLocalNotificationBuilder(l10n).build(
|
|
437
|
result: notificationResult,
|
504
|
result: notificationResult,
|
|
438
|
hasExistingHrv: hasExistingHrv,
|
505
|
hasExistingHrv: hasExistingHrv,
|
|
439
|
realtimeWindow: realtimeWindow,
|
506
|
realtimeWindow: realtimeWindow,
|
|
440
|
record: record,
|
507
|
record: record,
|
|
441
|
);
|
508
|
);
|
|
442
|
- if (notifications.isEmpty) return;
|
509
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
510
|
+ <String, Object?>{
|
|
|
|
511
|
+ 'event': 'flutterNotificationBuildFinished',
|
|
|
|
512
|
+ 'user_id': result.userId,
|
|
|
|
513
|
+ 'notification_count': notifications.length,
|
|
|
|
514
|
+ 'notifications': notifications.map(_localNotificationPayload).toList(),
|
|
|
|
515
|
+ },
|
|
|
|
516
|
+ );
|
|
|
|
517
|
+ if (notifications.isEmpty) {
|
|
|
|
518
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
519
|
+ <String, Object?>{
|
|
|
|
520
|
+ 'event': 'flutterNotificationNotSent',
|
|
|
|
521
|
+ 'reason': 'no_built_notifications',
|
|
|
|
522
|
+ 'user_id': result.userId,
|
|
|
|
523
|
+ },
|
|
|
|
524
|
+ );
|
|
|
|
525
|
+ return;
|
|
|
|
526
|
+ }
|
|
443
|
|
527
|
|
|
444
|
final sentNotifications = <HealthRawLocalNotification>[];
|
528
|
final sentNotifications = <HealthRawLocalNotification>[];
|
|
445
|
for (final notification in notifications) {
|
529
|
for (final notification in notifications) {
|
|
|
|
530
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
531
|
+ <String, Object?>{
|
|
|
|
532
|
+ 'event': 'flutterNotificationSendStart',
|
|
|
|
533
|
+ 'user_id': result.userId,
|
|
|
|
534
|
+ 'notification': _localNotificationPayload(notification),
|
|
|
|
535
|
+ },
|
|
|
|
536
|
+ );
|
|
446
|
final sent = await _sendLocalNotificationSafely(
|
537
|
final sent = await _sendLocalNotificationSafely(
|
|
447
|
userId: result.userId,
|
538
|
userId: result.userId,
|
|
448
|
notification: notification,
|
539
|
notification: notification,
|
|
449
|
);
|
540
|
);
|
|
|
|
541
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
542
|
+ <String, Object?>{
|
|
|
|
543
|
+ 'event': 'flutterNotificationSendFinished',
|
|
|
|
544
|
+ 'user_id': result.userId,
|
|
|
|
545
|
+ 'sent': sent,
|
|
|
|
546
|
+ 'reason': sent ? null : 'native_or_dispatcher_returned_false',
|
|
|
|
547
|
+ 'notification': _localNotificationPayload(notification),
|
|
|
|
548
|
+ },
|
|
|
|
549
|
+ );
|
|
450
|
if (sent) {
|
550
|
if (sent) {
|
|
451
|
sentNotifications.add(notification);
|
551
|
sentNotifications.add(notification);
|
|
452
|
}
|
552
|
}
|
|
@@ -485,17 +585,38 @@ class HealthRawDataCoreService { |
|
@@ -485,17 +585,38 @@ class HealthRawDataCoreService { |
|
485
|
required int latestRawEndTime,
|
585
|
required int latestRawEndTime,
|
|
486
|
}) async {
|
586
|
}) async {
|
|
487
|
try {
|
587
|
try {
|
|
488
|
- return await _localStore.queryRealtimeStressPoints(
|
588
|
+ final points = await _localStore.queryRealtimeStressPoints(
|
|
489
|
userId: userId,
|
589
|
userId: userId,
|
|
490
|
startTime: latestRawEndTime - Duration.secondsPerHour + 1,
|
590
|
startTime: latestRawEndTime - Duration.secondsPerHour + 1,
|
|
491
|
endTime: latestRawEndTime,
|
591
|
endTime: latestRawEndTime,
|
|
492
|
);
|
592
|
);
|
|
|
|
593
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
594
|
+ <String, Object?>{
|
|
|
|
595
|
+ 'event': 'flutterRealtimeStressWindowQueried',
|
|
|
|
596
|
+ 'user_id': userId,
|
|
|
|
597
|
+ 'latest_raw_end_time': latestRawEndTime,
|
|
|
|
598
|
+ 'window_start_time': latestRawEndTime - Duration.secondsPerHour + 1,
|
|
|
|
599
|
+ 'window_count': points.length,
|
|
|
|
600
|
+ 'valid_window_count':
|
|
|
|
601
|
+ points.where((e) => e.result >= 1 && e.result <= 100).length,
|
|
|
|
602
|
+ },
|
|
|
|
603
|
+ );
|
|
|
|
604
|
+ return points;
|
|
493
|
} catch (error, stackTrace) {
|
605
|
} catch (error, stackTrace) {
|
|
494
|
_logError(
|
606
|
_logError(
|
|
495
|
'query realtime stress notification window failed',
|
607
|
'query realtime stress notification window failed',
|
|
496
|
error,
|
608
|
error,
|
|
497
|
stackTrace,
|
609
|
stackTrace,
|
|
498
|
);
|
610
|
);
|
|
|
|
611
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
612
|
+ <String, Object?>{
|
|
|
|
613
|
+ 'event': 'flutterRealtimeStressWindowQueryFailed',
|
|
|
|
614
|
+ 'user_id': userId,
|
|
|
|
615
|
+ 'latest_raw_end_time': latestRawEndTime,
|
|
|
|
616
|
+ 'error': error.toString(),
|
|
|
|
617
|
+ 'stack_trace': stackTrace.toString(),
|
|
|
|
618
|
+ },
|
|
|
|
619
|
+ );
|
|
499
|
return const <HealthRawRealtimeStressPoint>[];
|
620
|
return const <HealthRawRealtimeStressPoint>[];
|
|
500
|
}
|
621
|
}
|
|
501
|
}
|
622
|
}
|
|
@@ -511,10 +632,285 @@ class HealthRawDataCoreService { |
|
@@ -511,10 +632,285 @@ class HealthRawDataCoreService { |
|
511
|
);
|
632
|
);
|
|
512
|
} catch (error, stackTrace) {
|
633
|
} catch (error, stackTrace) {
|
|
513
|
_logError('send local health notification failed', error, stackTrace);
|
634
|
_logError('send local health notification failed', error, stackTrace);
|
|
|
|
635
|
+ await _saveLocalNotificationDebugEvent(
|
|
|
|
636
|
+ <String, Object?>{
|
|
|
|
637
|
+ 'event': 'flutterNotificationSendError',
|
|
|
|
638
|
+ 'user_id': userId,
|
|
|
|
639
|
+ 'notification': _localNotificationPayload(notification),
|
|
|
|
640
|
+ 'error': error.toString(),
|
|
|
|
641
|
+ 'stack_trace': stackTrace.toString(),
|
|
|
|
642
|
+ },
|
|
|
|
643
|
+ );
|
|
514
|
return false;
|
644
|
return false;
|
|
515
|
}
|
645
|
}
|
|
516
|
}
|
646
|
}
|
|
517
|
|
647
|
|
|
|
|
648
|
+ List<Map<String, Object?>> _localNotificationDecisionPayloads({
|
|
|
|
649
|
+ required HealthRawStressCalculationResult result,
|
|
|
|
650
|
+ required bool hasExistingHrv,
|
|
|
|
651
|
+ required List<HealthRawRealtimeStressPoint> realtimeWindow,
|
|
|
|
652
|
+ required HealthRawLocalNotificationRecord record,
|
|
|
|
653
|
+ }) {
|
|
|
|
654
|
+ return <Map<String, Object?>>[
|
|
|
|
655
|
+ _sleepNotificationDecisionPayload(result.sleepResults),
|
|
|
|
656
|
+ _hrvNotificationDecisionPayload(
|
|
|
|
657
|
+ result.hrvStressPoints,
|
|
|
|
658
|
+ hasExistingHrv: hasExistingHrv,
|
|
|
|
659
|
+ record: record,
|
|
|
|
660
|
+ ),
|
|
|
|
661
|
+ _realtimeStressNotificationDecisionPayload(
|
|
|
|
662
|
+ result.realtimeStressPoints,
|
|
|
|
663
|
+ realtimeWindow: realtimeWindow,
|
|
|
|
664
|
+ record: record,
|
|
|
|
665
|
+ ),
|
|
|
|
666
|
+ ];
|
|
|
|
667
|
+ }
|
|
|
|
668
|
+
|
|
|
|
669
|
+ Map<String, Object?> _sleepNotificationDecisionPayload(
|
|
|
|
670
|
+ List<HealthRawSleepResult> sleepResults,
|
|
|
|
671
|
+ ) {
|
|
|
|
672
|
+ if (sleepResults.isEmpty) {
|
|
|
|
673
|
+ return <String, Object?>{
|
|
|
|
674
|
+ 'type': HealthRawLocalNotificationRecordType.sleep.name,
|
|
|
|
675
|
+ 'will_build': false,
|
|
|
|
676
|
+ 'reason': 'no_sleep_results',
|
|
|
|
677
|
+ };
|
|
|
|
678
|
+ }
|
|
|
|
679
|
+ final sorted = [...sleepResults]..sort((a, b) => a.date.compareTo(b.date));
|
|
|
|
680
|
+ final latest = sorted.last;
|
|
|
|
681
|
+ final invalidState = !<int>{1, 2, 3}.contains(latest.sleepState);
|
|
|
|
682
|
+ if (invalidState) {
|
|
|
|
683
|
+ return <String, Object?>{
|
|
|
|
684
|
+ 'type': HealthRawLocalNotificationRecordType.sleep.name,
|
|
|
|
685
|
+ 'will_build': false,
|
|
|
|
686
|
+ 'reason': 'invalid_sleep_state',
|
|
|
|
687
|
+ 'latest_sleep': _sleepResultPayload(latest),
|
|
|
|
688
|
+ };
|
|
|
|
689
|
+ }
|
|
|
|
690
|
+ if (latest.sleepMinutes <= 0) {
|
|
|
|
691
|
+ return <String, Object?>{
|
|
|
|
692
|
+ 'type': HealthRawLocalNotificationRecordType.sleep.name,
|
|
|
|
693
|
+ 'will_build': false,
|
|
|
|
694
|
+ 'reason': 'invalid_sleep_minutes',
|
|
|
|
695
|
+ 'latest_sleep': _sleepResultPayload(latest),
|
|
|
|
696
|
+ };
|
|
|
|
697
|
+ }
|
|
|
|
698
|
+ return <String, Object?>{
|
|
|
|
699
|
+ 'type': HealthRawLocalNotificationRecordType.sleep.name,
|
|
|
|
700
|
+ 'will_build': true,
|
|
|
|
701
|
+ 'reason': 'candidate',
|
|
|
|
702
|
+ 'latest_sleep': _sleepResultPayload(latest),
|
|
|
|
703
|
+ };
|
|
|
|
704
|
+ }
|
|
|
|
705
|
+
|
|
|
|
706
|
+ Map<String, Object?> _hrvNotificationDecisionPayload(
|
|
|
|
707
|
+ List<HealthRawHrvStressPoint> hrvPoints, {
|
|
|
|
708
|
+ required bool hasExistingHrv,
|
|
|
|
709
|
+ required HealthRawLocalNotificationRecord record,
|
|
|
|
710
|
+ }) {
|
|
|
|
711
|
+ if (!hasExistingHrv) {
|
|
|
|
712
|
+ return <String, Object?>{
|
|
|
|
713
|
+ 'type': HealthRawLocalNotificationRecordType.hrv.name,
|
|
|
|
714
|
+ 'will_build': false,
|
|
|
|
715
|
+ 'reason': 'first_calculation_no_existing_hrv',
|
|
|
|
716
|
+ 'has_existing_hrv': hasExistingHrv,
|
|
|
|
717
|
+ };
|
|
|
|
718
|
+ }
|
|
|
|
719
|
+ if (hrvPoints.isEmpty) {
|
|
|
|
720
|
+ return <String, Object?>{
|
|
|
|
721
|
+ 'type': HealthRawLocalNotificationRecordType.hrv.name,
|
|
|
|
722
|
+ 'will_build': false,
|
|
|
|
723
|
+ 'reason': 'no_new_hrv_points',
|
|
|
|
724
|
+ 'has_existing_hrv': hasExistingHrv,
|
|
|
|
725
|
+ };
|
|
|
|
726
|
+ }
|
|
|
|
727
|
+ final sorted = [...hrvPoints]
|
|
|
|
728
|
+ ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
|
|
|
|
729
|
+ final latest = sorted.last;
|
|
|
|
730
|
+ if (record.lastHrvTime == latest.rawEndTime) {
|
|
|
|
731
|
+ return <String, Object?>{
|
|
|
|
732
|
+ 'type': HealthRawLocalNotificationRecordType.hrv.name,
|
|
|
|
733
|
+ 'will_build': false,
|
|
|
|
734
|
+ 'reason': 'duplicate_hrv_record_time',
|
|
|
|
735
|
+ 'last_hrv_time': record.lastHrvTime,
|
|
|
|
736
|
+ 'latest_hrv': _hrvStressPointPayload(latest),
|
|
|
|
737
|
+ };
|
|
|
|
738
|
+ }
|
|
|
|
739
|
+ return <String, Object?>{
|
|
|
|
740
|
+ 'type': HealthRawLocalNotificationRecordType.hrv.name,
|
|
|
|
741
|
+ 'will_build': true,
|
|
|
|
742
|
+ 'reason': 'candidate',
|
|
|
|
743
|
+ 'last_hrv_time': record.lastHrvTime,
|
|
|
|
744
|
+ 'latest_hrv': _hrvStressPointPayload(latest),
|
|
|
|
745
|
+ };
|
|
|
|
746
|
+ }
|
|
|
|
747
|
+
|
|
|
|
748
|
+ Map<String, Object?> _realtimeStressNotificationDecisionPayload(
|
|
|
|
749
|
+ List<HealthRawRealtimeStressPoint> realtimePoints, {
|
|
|
|
750
|
+ required List<HealthRawRealtimeStressPoint> realtimeWindow,
|
|
|
|
751
|
+ required HealthRawLocalNotificationRecord record,
|
|
|
|
752
|
+ }) {
|
|
|
|
753
|
+ if (realtimePoints.isEmpty) {
|
|
|
|
754
|
+ return <String, Object?>{
|
|
|
|
755
|
+ 'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
756
|
+ 'will_build': false,
|
|
|
|
757
|
+ 'reason': 'no_realtime_points',
|
|
|
|
758
|
+ };
|
|
|
|
759
|
+ }
|
|
|
|
760
|
+ final latest = realtimePoints.reduce(
|
|
|
|
761
|
+ (a, b) => a.rawEndTime >= b.rawEndTime ? a : b,
|
|
|
|
762
|
+ );
|
|
|
|
763
|
+ if (latest.isWorkout || latest.isWorkoutRecovery) {
|
|
|
|
764
|
+ return <String, Object?>{
|
|
|
|
765
|
+ 'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
766
|
+ 'will_build': false,
|
|
|
|
767
|
+ 'reason': 'workout_or_recovery',
|
|
|
|
768
|
+ 'latest_realtime': _realtimeStressPointPayload(latest),
|
|
|
|
769
|
+ };
|
|
|
|
770
|
+ }
|
|
|
|
771
|
+ final valid = realtimeWindow
|
|
|
|
772
|
+ .where((e) => e.result >= 1 && e.result <= 100)
|
|
|
|
773
|
+ .toList()
|
|
|
|
774
|
+ ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
|
|
|
|
775
|
+ if (valid.length < 10) {
|
|
|
|
776
|
+ return <String, Object?>{
|
|
|
|
777
|
+ 'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
778
|
+ 'will_build': false,
|
|
|
|
779
|
+ 'reason': 'insufficient_valid_realtime_points',
|
|
|
|
780
|
+ 'valid_window_count': valid.length,
|
|
|
|
781
|
+ 'latest_realtime': _realtimeStressPointPayload(latest),
|
|
|
|
782
|
+ };
|
|
|
|
783
|
+ }
|
|
|
|
784
|
+ if (latest.isSleepLikely) {
|
|
|
|
785
|
+ return <String, Object?>{
|
|
|
|
786
|
+ 'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
787
|
+ 'will_build': false,
|
|
|
|
788
|
+ 'reason': 'sleep_likely',
|
|
|
|
789
|
+ 'latest_realtime': _realtimeStressPointPayload(latest),
|
|
|
|
790
|
+ };
|
|
|
|
791
|
+ }
|
|
|
|
792
|
+ final lastPushTime = record.lastRealtimeStressTime;
|
|
|
|
793
|
+ if (lastPushTime != null &&
|
|
|
|
794
|
+ latest.rawEndTime - lastPushTime < Duration.secondsPerHour) {
|
|
|
|
795
|
+ return <String, Object?>{
|
|
|
|
796
|
+ 'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
797
|
+ 'will_build': false,
|
|
|
|
798
|
+ 'reason': 'within_realtime_interval',
|
|
|
|
799
|
+ 'last_realtime_stress_time': lastPushTime,
|
|
|
|
800
|
+ 'latest_realtime': _realtimeStressPointPayload(latest),
|
|
|
|
801
|
+ };
|
|
|
|
802
|
+ }
|
|
|
|
803
|
+ return <String, Object?>{
|
|
|
|
804
|
+ 'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
805
|
+ 'will_build': true,
|
|
|
|
806
|
+ 'reason': 'candidate',
|
|
|
|
807
|
+ 'last_realtime_stress_time': lastPushTime,
|
|
|
|
808
|
+ 'valid_window_count': valid.length,
|
|
|
|
809
|
+ 'latest_realtime': _realtimeStressPointPayload(latest),
|
|
|
|
810
|
+ };
|
|
|
|
811
|
+ }
|
|
|
|
812
|
+
|
|
|
|
813
|
+ Map<String, Object?> _localNotificationPayload(
|
|
|
|
814
|
+ HealthRawLocalNotification notification,
|
|
|
|
815
|
+ ) {
|
|
|
|
816
|
+ return <String, Object?>{
|
|
|
|
817
|
+ 'type': notification.recordType.name,
|
|
|
|
818
|
+ 'record_time': notification.recordTime,
|
|
|
|
819
|
+ 'title': notification.title,
|
|
|
|
820
|
+ 'content': notification.content,
|
|
|
|
821
|
+ 'link': notification.link,
|
|
|
|
822
|
+ 'current_state': notification.currentState,
|
|
|
|
823
|
+ };
|
|
|
|
824
|
+ }
|
|
|
|
825
|
+
|
|
|
|
826
|
+ Map<String, Object?> _localNotificationRecordPayload(
|
|
|
|
827
|
+ HealthRawLocalNotificationRecord record,
|
|
|
|
828
|
+ ) {
|
|
|
|
829
|
+ return <String, Object?>{
|
|
|
|
830
|
+ 'last_sleep_time': record.lastSleepTime,
|
|
|
|
831
|
+ 'last_hrv_time': record.lastHrvTime,
|
|
|
|
832
|
+ 'last_realtime_stress_time': record.lastRealtimeStressTime,
|
|
|
|
833
|
+ };
|
|
|
|
834
|
+ }
|
|
|
|
835
|
+
|
|
|
|
836
|
+ Map<String, Object?>? _latestHrvStressPointPayload(
|
|
|
|
837
|
+ List<HealthRawHrvStressPoint> points,
|
|
|
|
838
|
+ ) {
|
|
|
|
839
|
+ if (points.isEmpty) return null;
|
|
|
|
840
|
+ return _hrvStressPointPayload(
|
|
|
|
841
|
+ points.reduce((a, b) => a.rawEndTime >= b.rawEndTime ? a : b),
|
|
|
|
842
|
+ );
|
|
|
|
843
|
+ }
|
|
|
|
844
|
+
|
|
|
|
845
|
+ Map<String, Object?> _hrvStressPointPayload(HealthRawHrvStressPoint point) {
|
|
|
|
846
|
+ return <String, Object?>{
|
|
|
|
847
|
+ 'raw_end_time': point.rawEndTime,
|
|
|
|
848
|
+ 'raw_hrv': point.rawHrv,
|
|
|
|
849
|
+ 'result': point.result,
|
|
|
|
850
|
+ 'state': point.state.name,
|
|
|
|
851
|
+ 'state_value': point.state.value,
|
|
|
|
852
|
+ 'baseline_hrv': point.baselineHrv,
|
|
|
|
853
|
+ 'baseline_awake_hrv': point.baselineAwakeHrv,
|
|
|
|
854
|
+ 'baseline_sleep_hrv': point.baselineSleepHrv,
|
|
|
|
855
|
+ 'uploaded': point.uploaded,
|
|
|
|
856
|
+ 'flags': _pointFlagsPayload(point.flags),
|
|
|
|
857
|
+ };
|
|
|
|
858
|
+ }
|
|
|
|
859
|
+
|
|
|
|
860
|
+ Map<String, Object?>? _latestRealtimeStressPointPayload(
|
|
|
|
861
|
+ List<HealthRawRealtimeStressPoint> points,
|
|
|
|
862
|
+ ) {
|
|
|
|
863
|
+ if (points.isEmpty) return null;
|
|
|
|
864
|
+ return _realtimeStressPointPayload(
|
|
|
|
865
|
+ points.reduce((a, b) => a.rawEndTime >= b.rawEndTime ? a : b),
|
|
|
|
866
|
+ );
|
|
|
|
867
|
+ }
|
|
|
|
868
|
+
|
|
|
|
869
|
+ Map<String, Object?> _realtimeStressPointPayload(
|
|
|
|
870
|
+ HealthRawRealtimeStressPoint point,
|
|
|
|
871
|
+ ) {
|
|
|
|
872
|
+ return <String, Object?>{
|
|
|
|
873
|
+ 'raw_end_time': point.rawEndTime,
|
|
|
|
874
|
+ 'raw_hr': point.rawHr,
|
|
|
|
875
|
+ 'result': point.result,
|
|
|
|
876
|
+ 'state': point.state.name,
|
|
|
|
877
|
+ 'state_value': point.state.value,
|
|
|
|
878
|
+ 'uploaded': point.uploaded,
|
|
|
|
879
|
+ 'flags': _pointFlagsPayload(point.flags),
|
|
|
|
880
|
+ };
|
|
|
|
881
|
+ }
|
|
|
|
882
|
+
|
|
|
|
883
|
+ Map<String, Object?>? _latestSleepResultPayload(
|
|
|
|
884
|
+ List<HealthRawSleepResult> sleepResults,
|
|
|
|
885
|
+ ) {
|
|
|
|
886
|
+ if (sleepResults.isEmpty) return null;
|
|
|
|
887
|
+ final sorted = [...sleepResults]..sort((a, b) => a.date.compareTo(b.date));
|
|
|
|
888
|
+ return _sleepResultPayload(sorted.last);
|
|
|
|
889
|
+ }
|
|
|
|
890
|
+
|
|
|
|
891
|
+ Map<String, Object?> _sleepResultPayload(HealthRawSleepResult result) {
|
|
|
|
892
|
+ return <String, Object?>{
|
|
|
|
893
|
+ 'date': result.date,
|
|
|
|
894
|
+ 'start_date': result.startDate,
|
|
|
|
895
|
+ 'sleep_score': result.sleepScore,
|
|
|
|
896
|
+ 'sleep_state': result.sleepState,
|
|
|
|
897
|
+ 'in_bed_minutes': result.inBedMinutes,
|
|
|
|
898
|
+ 'awak_minutes': result.awakMinutes,
|
|
|
|
899
|
+ 'sleep_minutes': result.sleepMinutes,
|
|
|
|
900
|
+ 'uploaded': result.uploaded,
|
|
|
|
901
|
+ };
|
|
|
|
902
|
+ }
|
|
|
|
903
|
+
|
|
|
|
904
|
+ Map<String, Object?> _pointFlagsPayload(HealthRawPointFlags flags) {
|
|
|
|
905
|
+ return <String, Object?>{
|
|
|
|
906
|
+ 'is_workout': flags.isWorkout,
|
|
|
|
907
|
+ 'is_workout_recovery': flags.isWorkoutRecovery,
|
|
|
|
908
|
+ 'is_sleep_likely': flags.isSleepLikely,
|
|
|
|
909
|
+ 'is_suspected_activity': flags.isSuspectedActivity,
|
|
|
|
910
|
+ 'context': flags.context.name,
|
|
|
|
911
|
+ };
|
|
|
|
912
|
+ }
|
|
|
|
913
|
+
|
|
518
|
void _scheduleRealtimeStressServerPush(
|
914
|
void _scheduleRealtimeStressServerPush(
|
|
519
|
Iterable<HealthRawLocalNotification> sentNotifications,
|
915
|
Iterable<HealthRawLocalNotification> sentNotifications,
|
|
520
|
) {
|
916
|
) {
|
|
@@ -938,6 +1334,25 @@ class HealthRawDataCoreService { |
|
@@ -938,6 +1334,25 @@ class HealthRawDataCoreService { |
|
938
|
}
|
1334
|
}
|
|
939
|
}
|
1335
|
}
|
|
940
|
|
1336
|
|
|
|
|
1337
|
+ Future<void> _saveLocalNotificationDebugEvent(
|
|
|
|
1338
|
+ Map<String, Object?> event,
|
|
|
|
1339
|
+ ) async {
|
|
|
|
1340
|
+ if (!_isDebug) return;
|
|
|
|
1341
|
+ try {
|
|
|
|
1342
|
+ await _localNotificationDebugStore.append(event);
|
|
|
|
1343
|
+ await _saveNativeLocalNotificationDebugEvent(event);
|
|
|
|
1344
|
+ } catch (error, stackTrace) {
|
|
|
|
1345
|
+ _logError(
|
|
|
|
1346
|
+ 'save local notification debug event failed', error, stackTrace);
|
|
|
|
1347
|
+ }
|
|
|
|
1348
|
+ }
|
|
|
|
1349
|
+
|
|
|
|
1350
|
+ Future<bool> _saveNativeLocalNotificationDebugEvent(
|
|
|
|
1351
|
+ Map<String, Object?> event,
|
|
|
|
1352
|
+ ) async {
|
|
|
|
1353
|
+ return _rawDataApi.saveLocalNotificationDebugEvent(event: event);
|
|
|
|
1354
|
+ }
|
|
|
|
1355
|
+
|
|
941
|
Future<File> _uploadApiLogFile() async {
|
1356
|
Future<File> _uploadApiLogFile() async {
|
|
942
|
final dir = await getApplicationDocumentsDirectory();
|
1357
|
final dir = await getApplicationDocumentsDirectory();
|
|
943
|
return File('${dir.path}/health_raw_upload_debug.log');
|
1358
|
return File('${dir.path}/health_raw_upload_debug.log');
|