Commit fe916dc3111475ec0fb81d13d50b028c9e830fda

Authored by 权海
1 parent b6275e80

feat(ui):优化计算数据和推送发送

@@ -160,6 +160,8 @@ class HealthRawHrvStressPoint { @@ -160,6 +160,8 @@ class HealthRawHrvStressPoint {
160 required this.baselineRestingHr, 160 required this.baselineRestingHr,
161 this.flags = const HealthRawPointFlags.none(), 161 this.flags = const HealthRawPointFlags.none(),
162 this.uploaded = false, 162 this.uploaded = false,
  163 + this.pushSendTime,
  164 + this.uploadTime,
163 }); 165 });
164 166
165 final int userId; 167 final int userId;
@@ -175,6 +177,8 @@ class HealthRawHrvStressPoint { @@ -175,6 +177,8 @@ class HealthRawHrvStressPoint {
175 final double baselineRestingHr; 177 final double baselineRestingHr;
176 final HealthRawPointFlags flags; 178 final HealthRawPointFlags flags;
177 final bool uploaded; 179 final bool uploaded;
  180 + final int? pushSendTime;
  181 + final int? uploadTime;
178 182
179 factory HealthRawHrvStressPoint.fromDb(Map<String, Object?> row) { 183 factory HealthRawHrvStressPoint.fromDb(Map<String, Object?> row) {
180 final result = (row['result'] as num).toDouble(); 184 final result = (row['result'] as num).toDouble();
@@ -196,6 +200,8 @@ class HealthRawHrvStressPoint { @@ -196,6 +200,8 @@ class HealthRawHrvStressPoint {
196 baselineRestingHr: _positiveDouble(row['baseline_resting_hr']) ?? 65, 200 baselineRestingHr: _positiveDouble(row['baseline_resting_hr']) ?? 65,
197 flags: _flagsFromDb(row), 201 flags: _flagsFromDb(row),
198 uploaded: (row['uploaded'] as int) == 1, 202 uploaded: (row['uploaded'] as int) == 1,
  203 + pushSendTime: (row['push_send_time'] as num?)?.toInt(),
  204 + uploadTime: (row['upload_time'] as num?)?.toInt(),
199 ); 205 );
200 } 206 }
201 } 207 }
@@ -210,6 +216,8 @@ class HealthRawRealtimeStressPoint { @@ -210,6 +216,8 @@ class HealthRawRealtimeStressPoint {
210 required this.sourceEndTime, 216 required this.sourceEndTime,
211 this.flags = const HealthRawPointFlags.none(), 217 this.flags = const HealthRawPointFlags.none(),
212 this.uploaded = false, 218 this.uploaded = false,
  219 + this.pushSendTime,
  220 + this.uploadTime,
213 }); 221 });
214 222
215 final int userId; 223 final int userId;
@@ -220,6 +228,8 @@ class HealthRawRealtimeStressPoint { @@ -220,6 +228,8 @@ class HealthRawRealtimeStressPoint {
220 final int sourceEndTime; 228 final int sourceEndTime;
221 final HealthRawPointFlags flags; 229 final HealthRawPointFlags flags;
222 final bool uploaded; 230 final bool uploaded;
  231 + final int? pushSendTime;
  232 + final int? uploadTime;
223 233
224 HealthRawStressState get state => healthRawRealtimeStressState(result); 234 HealthRawStressState get state => healthRawRealtimeStressState(result);
225 bool get isWorkout => flags.isWorkout; 235 bool get isWorkout => flags.isWorkout;
@@ -237,6 +247,8 @@ class HealthRawRealtimeStressPoint { @@ -237,6 +247,8 @@ class HealthRawRealtimeStressPoint {
237 sourceEndTime: row['source_end_time'] as int, 247 sourceEndTime: row['source_end_time'] as int,
238 flags: _flagsFromDb(row), 248 flags: _flagsFromDb(row),
239 uploaded: (row['uploaded'] as int) == 1, 249 uploaded: (row['uploaded'] as int) == 1,
  250 + pushSendTime: (row['push_send_time'] as num?)?.toInt(),
  251 + uploadTime: (row['upload_time'] as num?)?.toInt(),
240 ); 252 );
241 } 253 }
242 } 254 }
@@ -250,6 +262,7 @@ class HealthRawDailyStressPoint { @@ -250,6 +262,7 @@ class HealthRawDailyStressPoint {
250 required this.state, 262 required this.state,
251 required this.dataTime, 263 required this.dataTime,
252 this.uploaded = false, 264 this.uploaded = false,
  265 + this.uploadTime,
253 }); 266 });
254 267
255 final int userId; 268 final int userId;
@@ -259,6 +272,7 @@ class HealthRawDailyStressPoint { @@ -259,6 +272,7 @@ class HealthRawDailyStressPoint {
259 final HealthRawStressState state; 272 final HealthRawStressState state;
260 final int dataTime; 273 final int dataTime;
261 final bool uploaded; 274 final bool uploaded;
  275 + final int? uploadTime;
262 276
263 factory HealthRawDailyStressPoint.fromDb(Map<String, Object?> row) { 277 factory HealthRawDailyStressPoint.fromDb(Map<String, Object?> row) {
264 final stressScore = row['stress_score'] as int; 278 final stressScore = row['stress_score'] as int;
@@ -271,6 +285,7 @@ class HealthRawDailyStressPoint { @@ -271,6 +285,7 @@ class HealthRawDailyStressPoint {
271 healthRawRealtimeStressState(stressScore), 285 healthRawRealtimeStressState(stressScore),
272 dataTime: row['data_time'] as int, 286 dataTime: row['data_time'] as int,
273 uploaded: (row['uploaded'] as int) == 1, 287 uploaded: (row['uploaded'] as int) == 1,
  288 + uploadTime: (row['upload_time'] as num?)?.toInt(),
274 ); 289 );
275 } 290 }
276 } 291 }
@@ -286,6 +301,8 @@ class HealthRawSleepResult { @@ -286,6 +301,8 @@ class HealthRawSleepResult {
286 required this.awakMinutes, 301 required this.awakMinutes,
287 required this.sleepMinutes, 302 required this.sleepMinutes,
288 this.uploaded = false, 303 this.uploaded = false,
  304 + this.pushSendTime,
  305 + this.uploadTime,
289 }); 306 });
290 307
291 final int userId; 308 final int userId;
@@ -297,6 +314,8 @@ class HealthRawSleepResult { @@ -297,6 +314,8 @@ class HealthRawSleepResult {
297 final int awakMinutes; 314 final int awakMinutes;
298 final int sleepMinutes; 315 final int sleepMinutes;
299 final bool uploaded; 316 final bool uploaded;
  317 + final int? pushSendTime;
  318 + final int? uploadTime;
300 319
301 factory HealthRawSleepResult.fromDb(Map<String, Object?> row) { 320 factory HealthRawSleepResult.fromDb(Map<String, Object?> row) {
302 return HealthRawSleepResult( 321 return HealthRawSleepResult(
@@ -309,6 +328,8 @@ class HealthRawSleepResult { @@ -309,6 +328,8 @@ class HealthRawSleepResult {
309 awakMinutes: row['awak_minutes'] as int, 328 awakMinutes: row['awak_minutes'] as int,
310 sleepMinutes: row['sleep_minutes'] as int, 329 sleepMinutes: row['sleep_minutes'] as int,
311 uploaded: (row['uploaded'] as int? ?? 0) == 1, 330 uploaded: (row['uploaded'] as int? ?? 0) == 1,
  331 + pushSendTime: (row['push_send_time'] as num?)?.toInt(),
  332 + uploadTime: (row['upload_time'] as num?)?.toInt(),
312 ); 333 );
313 } 334 }
314 335
@@ -16,7 +16,6 @@ import '../../../logging/app_logger.dart'; @@ -16,7 +16,6 @@ import '../../../logging/app_logger.dart';
16 import '../../../network/api/health_api.dart'; 16 import '../../../network/api/health_api.dart';
17 import '../../../result/app_result.dart'; 17 import '../../../result/app_result.dart';
18 import '../health_raw_models.dart'; 18 import '../health_raw_models.dart';
19 -import 'apple_health_raw_local_notification_debug_store.dart';  
20 import 'apple_health_raw_local_notification.dart'; 19 import 'apple_health_raw_local_notification.dart';
21 import '../health_raw_stress_calculator.dart'; 20 import '../health_raw_stress_calculator.dart';
22 import '../health_sleep_calculator.dart'; 21 import '../health_sleep_calculator.dart';
@@ -38,7 +37,6 @@ class AppleHealthRawDataCoreService { @@ -38,7 +37,6 @@ class AppleHealthRawDataCoreService {
38 int Function()? userIdProvider, 37 int Function()? userIdProvider,
39 bool uploadResultsAfterCalculation = true, 38 bool uploadResultsAfterCalculation = true,
40 HealthRawLocalNotificationDispatcher? localNotificationDispatcher, 39 HealthRawLocalNotificationDispatcher? localNotificationDispatcher,
41 - HealthRawLocalNotificationDebugStore? localNotificationDebugStore,  
42 HealthApi? serverHealthApi, 40 HealthApi? serverHealthApi,
43 }) : _healthApi = AppHealthKitHostApi(api: healthApi), 41 }) : _healthApi = AppHealthKitHostApi(api: healthApi),
44 _rawDataApi = AppHealthKitRawDataHostApi(api: rawDataApi), 42 _rawDataApi = AppHealthKitRawDataHostApi(api: rawDataApi),
@@ -48,8 +46,6 @@ class AppleHealthRawDataCoreService { @@ -48,8 +46,6 @@ class AppleHealthRawDataCoreService {
48 _uploadResultsAfterCalculation = uploadResultsAfterCalculation, 46 _uploadResultsAfterCalculation = uploadResultsAfterCalculation,
49 _localNotificationDispatcher = localNotificationDispatcher ?? 47 _localNotificationDispatcher = localNotificationDispatcher ??
50 HealthRawLocalNotificationDispatcher(), 48 HealthRawLocalNotificationDispatcher(),
51 - _localNotificationDebugStore = localNotificationDebugStore ??  
52 - HealthRawLocalNotificationDebugStore(),  
53 _serverHealthApi = serverHealthApi; 49 _serverHealthApi = serverHealthApi;
54 50
55 final AppHealthKitHostApi _healthApi; 51 final AppHealthKitHostApi _healthApi;
@@ -59,7 +55,6 @@ class AppleHealthRawDataCoreService { @@ -59,7 +55,6 @@ class AppleHealthRawDataCoreService {
59 final int Function()? _userIdProvider; 55 final int Function()? _userIdProvider;
60 final bool _uploadResultsAfterCalculation; 56 final bool _uploadResultsAfterCalculation;
61 final HealthRawLocalNotificationDispatcher _localNotificationDispatcher; 57 final HealthRawLocalNotificationDispatcher _localNotificationDispatcher;
62 - final HealthRawLocalNotificationDebugStore _localNotificationDebugStore;  
63 final HealthApi? _serverHealthApi; 58 final HealthApi? _serverHealthApi;
64 final StreamController<HealthRawDataUpdatedEvent> 59 final StreamController<HealthRawDataUpdatedEvent>
65 _healthDataUpdatedController = 60 _healthDataUpdatedController =
@@ -218,7 +213,7 @@ class AppleHealthRawDataCoreService { @@ -218,7 +213,7 @@ class AppleHealthRawDataCoreService {
218 } 213 }
219 214
220 Future<String> readLocalNotificationDebugLogText() { 215 Future<String> readLocalNotificationDebugLogText() {
221 - return _localNotificationDebugStore.readText(); 216 + return Future.value('');
222 } 217 }
223 218
224 Future<bool> shareLocalNotificationDebugRecord() async { 219 Future<bool> shareLocalNotificationDebugRecord() async {
@@ -536,6 +531,12 @@ class AppleHealthRawDataCoreService { @@ -536,6 +531,12 @@ class AppleHealthRawDataCoreService {
536 ); 531 );
537 var notificationResult = candidateResult; 532 var notificationResult = candidateResult;
538 var realtimeWindow = const <HealthRawRealtimeStressPoint>[]; 533 var realtimeWindow = const <HealthRawRealtimeStressPoint>[];
  534 + final lastRealtimeStressPushSendTime =
  535 + await _localStore.latestRealtimeStressPushSendTime(
  536 + candidateResult.userId,
  537 + );
  538 + final notificationBuildTime = _unixSecondsNow();
  539 + var realtimeSleepIntervals = const <HealthRawSleepInterval>[];
539 if (latestRealtimeStressPoint != null) { 540 if (latestRealtimeStressPoint != null) {
540 final shouldResetRealtimeStressPushTime = 541 final shouldResetRealtimeStressPushTime =
541 latestRealtimeStressPoint.isWorkout || 542 latestRealtimeStressPoint.isWorkout ||
@@ -550,10 +551,6 @@ class AppleHealthRawDataCoreService { @@ -550,10 +551,6 @@ class AppleHealthRawDataCoreService {
550 _realtimeStressPointPayload(latestRealtimeStressPoint), 551 _realtimeStressPointPayload(latestRealtimeStressPoint),
551 }, 552 },
552 ); 553 );
553 - await _recordRealtimeStressTimeSafely(  
554 - userId: candidateResult.userId,  
555 - recordTime: latestRealtimeStressPoint.rawEndTime,  
556 - );  
557 notificationResult = candidateResult.copyWith( 554 notificationResult = candidateResult.copyWith(
558 realtimeStressPoints: const <HealthRawRealtimeStressPoint>[], 555 realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
559 ); 556 );
@@ -562,6 +559,11 @@ class AppleHealthRawDataCoreService { @@ -562,6 +559,11 @@ class AppleHealthRawDataCoreService {
562 userId: candidateResult.userId, 559 userId: candidateResult.userId,
563 latestRawEndTime: latestRealtimeStressPoint.rawEndTime, 560 latestRawEndTime: latestRealtimeStressPoint.rawEndTime,
564 ); 561 );
  562 + realtimeSleepIntervals =
  563 + await _queryRealtimeStressSleepIntervalsForNotificationSafely(
  564 + userId: candidateResult.userId,
  565 + latestRawEndTime: latestRealtimeStressPoint.rawEndTime,
  566 + );
565 } 567 }
566 } 568 }
567 569
@@ -575,7 +577,10 @@ class AppleHealthRawDataCoreService { @@ -575,7 +577,10 @@ class AppleHealthRawDataCoreService {
575 hasExistingHrv: candidateData.hasExistingHrvDataTime, 577 hasExistingHrv: candidateData.hasExistingHrvDataTime,
576 hasExistingSleep: candidateData.hasExistingSleepDataTime, 578 hasExistingSleep: candidateData.hasExistingSleepDataTime,
577 realtimeWindow: realtimeWindow, 579 realtimeWindow: realtimeWindow,
  580 + realtimeSleepIntervals: realtimeSleepIntervals,
578 record: record, 581 record: record,
  582 + lastRealtimeStressPushSendTime: lastRealtimeStressPushSendTime,
  583 + notificationBuildTime: notificationBuildTime,
579 ), 584 ),
580 }, 585 },
581 ); 586 );
@@ -584,7 +589,10 @@ class AppleHealthRawDataCoreService { @@ -584,7 +589,10 @@ class AppleHealthRawDataCoreService {
584 hasExistingHrv: candidateData.hasExistingHrvDataTime, 589 hasExistingHrv: candidateData.hasExistingHrvDataTime,
585 hasExistingSleep: candidateData.hasExistingSleepDataTime, 590 hasExistingSleep: candidateData.hasExistingSleepDataTime,
586 realtimeWindow: realtimeWindow, 591 realtimeWindow: realtimeWindow,
  592 + realtimeSleepIntervals: realtimeSleepIntervals,
587 record: record, 593 record: record,
  594 + lastRealtimeStressPushSendTime: lastRealtimeStressPushSendTime,
  595 + notificationBuildTime: notificationBuildTime,
588 ); 596 );
589 await _saveLocalNotificationDebugEvent( 597 await _saveLocalNotificationDebugEvent(
590 <String, Object?>{ 598 <String, Object?>{
@@ -633,6 +641,10 @@ class AppleHealthRawDataCoreService { @@ -633,6 +641,10 @@ class AppleHealthRawDataCoreService {
633 ); 641 );
634 if (sent) { 642 if (sent) {
635 sentNotifications.add(notification); 643 sentNotifications.add(notification);
  644 + await _recordLocalNotificationPushSendTimeSafely(
  645 + userId: candidateResult.userId,
  646 + notification: notification,
  647 + );
636 } 648 }
637 } 649 }
638 _scheduleRealtimeStressServerPush(sentNotifications); 650 _scheduleRealtimeStressServerPush(sentNotifications);
@@ -758,17 +770,20 @@ class AppleHealthRawDataCoreService { @@ -758,17 +770,20 @@ class AppleHealthRawDataCoreService {
758 } 770 }
759 } 771 }
760 772
761 - Future<void> _recordRealtimeStressTimeSafely({ 773 + Future<void> _recordLocalNotificationPushSendTimeSafely({
762 required int userId, 774 required int userId,
763 - required int recordTime, 775 + required HealthRawLocalNotification notification,
764 }) async { 776 }) async {
765 try { 777 try {
766 - await _localNotificationDispatcher.recordRealtimeStressTime( 778 + await _localStore.markLocalNotificationPushed(
767 userId: userId, 779 userId: userId,
768 - recordTime: recordTime, 780 + recordType: notification.recordType,
  781 + recordTime: notification.recordTime,
  782 + pushSendTime: _unixSecondsNow(),
769 ); 783 );
770 } catch (error, stackTrace) { 784 } catch (error, stackTrace) {
771 - _logError('record realtime stress push time failed', error, stackTrace); 785 + _logError(
  786 + 'record local notification push send time failed', error, stackTrace);
772 } 787 }
773 } 788 }
774 789
@@ -814,6 +829,73 @@ class AppleHealthRawDataCoreService { @@ -814,6 +829,73 @@ class AppleHealthRawDataCoreService {
814 } 829 }
815 } 830 }
816 831
  832 + Future<List<HealthRawSleepInterval>>
  833 + _queryRealtimeStressSleepIntervalsForNotificationSafely({
  834 + required int userId,
  835 + required int latestRawEndTime,
  836 + }) async {
  837 + try {
  838 + final intervals = await _queryRealtimeStressSleepIntervalsForNotification(
  839 + userId: userId,
  840 + latestRawEndTime: latestRawEndTime,
  841 + );
  842 + await _saveLocalNotificationDebugEvent(
  843 + <String, Object?>{
  844 + 'event': 'flutterRealtimeStressSleepIntervalsQueried',
  845 + 'user_id': userId,
  846 + 'latest_raw_end_time': latestRawEndTime,
  847 + 'sleep_intervals': intervals.map(_sleepIntervalPayload).toList(),
  848 + },
  849 + );
  850 + return intervals;
  851 + } catch (error, stackTrace) {
  852 + _logError(
  853 + 'query realtime stress sleep intervals failed',
  854 + error,
  855 + stackTrace,
  856 + );
  857 + final fallback = [_defaultSleepIntervalForUnixSeconds(latestRawEndTime)];
  858 + await _saveLocalNotificationDebugEvent(
  859 + <String, Object?>{
  860 + 'event': 'flutterRealtimeStressSleepIntervalsQueryFailed',
  861 + 'user_id': userId,
  862 + 'latest_raw_end_time': latestRawEndTime,
  863 + 'sleep_intervals': fallback.map(_sleepIntervalPayload).toList(),
  864 + 'error': error.toString(),
  865 + 'stack_trace': stackTrace.toString(),
  866 + },
  867 + );
  868 + return fallback;
  869 + }
  870 + }
  871 +
  872 + Future<List<HealthRawSleepInterval>>
  873 + _queryRealtimeStressSleepIntervalsForNotification({
  874 + required int userId,
  875 + required int latestRawEndTime,
  876 + }) async {
  877 + final dayRange = _dayRangeFromUnixSeconds(latestRawEndTime);
  878 + final sleepResults = await _localStore.querySleepResults(
  879 + userId: userId,
  880 + startTime: dayRange.$1,
  881 + endTime: dayRange.$2,
  882 + );
  883 + final intervals = sleepResults
  884 + .where((result) => result.startDate <= result.date)
  885 + .map(
  886 + (result) => (
  887 + startTime: result.startDate,
  888 + endTime: result.date,
  889 + ),
  890 + )
  891 + .toList()
  892 + ..sort((a, b) => a.startTime.compareTo(b.startTime));
  893 + if (intervals.isNotEmpty) {
  894 + return intervals;
  895 + }
  896 + return [_defaultSleepIntervalForUnixSeconds(latestRawEndTime)];
  897 + }
  898 +
817 Future<bool> _sendLocalNotificationSafely({ 899 Future<bool> _sendLocalNotificationSafely({
818 required int userId, 900 required int userId,
819 required HealthRawLocalNotification notification, 901 required HealthRawLocalNotification notification,
@@ -843,7 +925,10 @@ class AppleHealthRawDataCoreService { @@ -843,7 +925,10 @@ class AppleHealthRawDataCoreService {
843 required bool hasExistingHrv, 925 required bool hasExistingHrv,
844 required bool hasExistingSleep, 926 required bool hasExistingSleep,
845 required List<HealthRawRealtimeStressPoint> realtimeWindow, 927 required List<HealthRawRealtimeStressPoint> realtimeWindow,
  928 + required List<HealthRawSleepInterval> realtimeSleepIntervals,
846 required HealthRawLocalNotificationRecord record, 929 required HealthRawLocalNotificationRecord record,
  930 + required int? lastRealtimeStressPushSendTime,
  931 + required int notificationBuildTime,
847 }) { 932 }) {
848 return <Map<String, Object?>>[ 933 return <Map<String, Object?>>[
849 _sleepNotificationDecisionPayload( 934 _sleepNotificationDecisionPayload(
@@ -858,7 +943,10 @@ class AppleHealthRawDataCoreService { @@ -858,7 +943,10 @@ class AppleHealthRawDataCoreService {
858 _realtimeStressNotificationDecisionPayload( 943 _realtimeStressNotificationDecisionPayload(
859 result.realtimeStressPoints, 944 result.realtimeStressPoints,
860 realtimeWindow: realtimeWindow, 945 realtimeWindow: realtimeWindow,
  946 + realtimeSleepIntervals: realtimeSleepIntervals,
861 record: record, 947 record: record,
  948 + lastPushSendTime: lastRealtimeStressPushSendTime,
  949 + notificationBuildTime: notificationBuildTime,
862 ), 950 ),
863 ]; 951 ];
864 } 952 }
@@ -936,20 +1024,23 @@ class AppleHealthRawDataCoreService { @@ -936,20 +1024,23 @@ class AppleHealthRawDataCoreService {
936 } 1024 }
937 final sorted = [...hrvPoints] 1025 final sorted = [...hrvPoints]
938 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime)); 1026 ..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
939 - final latest = sorted.last;  
940 return <String, Object?>{ 1027 return <String, Object?>{
941 'type': HealthRawLocalNotificationRecordType.hrv.name, 1028 'type': HealthRawLocalNotificationRecordType.hrv.name,
942 'will_build': true, 1029 'will_build': true,
943 'reason': 'candidate', 1030 'reason': 'candidate',
944 'last_hrv_time': record.lastHrvTime, 1031 'last_hrv_time': record.lastHrvTime,
945 - 'latest_hrv': _hrvStressPointPayload(latest), 1032 + 'candidate_count': sorted.length,
  1033 + 'latest_hrv': _hrvStressPointPayload(sorted.last),
946 }; 1034 };
947 } 1035 }
948 1036
949 Map<String, Object?> _realtimeStressNotificationDecisionPayload( 1037 Map<String, Object?> _realtimeStressNotificationDecisionPayload(
950 List<HealthRawRealtimeStressPoint> realtimePoints, { 1038 List<HealthRawRealtimeStressPoint> realtimePoints, {
951 required List<HealthRawRealtimeStressPoint> realtimeWindow, 1039 required List<HealthRawRealtimeStressPoint> realtimeWindow,
  1040 + required List<HealthRawSleepInterval> realtimeSleepIntervals,
952 required HealthRawLocalNotificationRecord record, 1041 required HealthRawLocalNotificationRecord record,
  1042 + required int? lastPushSendTime,
  1043 + required int notificationBuildTime,
953 }) { 1044 }) {
954 if (realtimePoints.isEmpty) { 1045 if (realtimePoints.isEmpty) {
955 return <String, Object?>{ 1046 return <String, Object?>{
@@ -969,6 +1060,19 @@ class AppleHealthRawDataCoreService { @@ -969,6 +1060,19 @@ class AppleHealthRawDataCoreService {
969 'latest_realtime': _realtimeStressPointPayload(latest), 1060 'latest_realtime': _realtimeStressPointPayload(latest),
970 }; 1061 };
971 } 1062 }
  1063 + final matchedSleepInterval =
  1064 + _sleepIntervalContaining(latest.rawEndTime, realtimeSleepIntervals);
  1065 + if (matchedSleepInterval != null) {
  1066 + return <String, Object?>{
  1067 + 'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
  1068 + 'will_build': false,
  1069 + 'reason': 'sleep_interval',
  1070 + 'matched_sleep_interval': _sleepIntervalPayload(matchedSleepInterval),
  1071 + 'sleep_intervals':
  1072 + realtimeSleepIntervals.map(_sleepIntervalPayload).toList(),
  1073 + 'latest_realtime': _realtimeStressPointPayload(latest),
  1074 + };
  1075 + }
972 final valid = realtimeWindow 1076 final valid = realtimeWindow
973 .where((e) => e.result >= 1 && e.result <= 100) 1077 .where((e) => e.result >= 1 && e.result <= 100)
974 .toList() 1078 .toList()
@@ -990,14 +1094,15 @@ class AppleHealthRawDataCoreService { @@ -990,14 +1094,15 @@ class AppleHealthRawDataCoreService {
990 'latest_realtime': _realtimeStressPointPayload(latest), 1094 'latest_realtime': _realtimeStressPointPayload(latest),
991 }; 1095 };
992 } 1096 }
993 - final lastPushTime = record.lastRealtimeStressTime; 1097 + final lastPushTime = lastPushSendTime;
994 if (lastPushTime != null && 1098 if (lastPushTime != null &&
995 - latest.rawEndTime - lastPushTime < Duration.secondsPerHour) { 1099 + notificationBuildTime - lastPushTime < Duration.secondsPerHour) {
996 return <String, Object?>{ 1100 return <String, Object?>{
997 'type': HealthRawLocalNotificationRecordType.realtimeStress.name, 1101 'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
998 'will_build': false, 1102 'will_build': false,
999 'reason': 'within_realtime_interval', 1103 'reason': 'within_realtime_interval',
1000 - 'last_realtime_stress_time': lastPushTime, 1104 + 'last_realtime_stress_push_send_time': lastPushTime,
  1105 + 'notification_build_time': notificationBuildTime,
1001 'latest_realtime': _realtimeStressPointPayload(latest), 1106 'latest_realtime': _realtimeStressPointPayload(latest),
1002 }; 1107 };
1003 } 1108 }
@@ -1005,12 +1110,36 @@ class AppleHealthRawDataCoreService { @@ -1005,12 +1110,36 @@ class AppleHealthRawDataCoreService {
1005 'type': HealthRawLocalNotificationRecordType.realtimeStress.name, 1110 'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
1006 'will_build': true, 1111 'will_build': true,
1007 'reason': 'candidate', 1112 'reason': 'candidate',
1008 - 'last_realtime_stress_time': lastPushTime, 1113 + 'last_realtime_stress_push_send_time': lastPushTime,
  1114 + 'notification_build_time': notificationBuildTime,
1009 'valid_window_count': valid.length, 1115 'valid_window_count': valid.length,
  1116 + 'sleep_intervals':
  1117 + realtimeSleepIntervals.map(_sleepIntervalPayload).toList(),
1010 'latest_realtime': _realtimeStressPointPayload(latest), 1118 'latest_realtime': _realtimeStressPointPayload(latest),
1011 }; 1119 };
1012 } 1120 }
1013 1121
  1122 + HealthRawSleepInterval? _sleepIntervalContaining(
  1123 + int time,
  1124 + List<HealthRawSleepInterval> intervals,
  1125 + ) {
  1126 + for (final interval in intervals) {
  1127 + if (time >= interval.startTime && time <= interval.endTime) {
  1128 + return interval;
  1129 + }
  1130 + }
  1131 + return null;
  1132 + }
  1133 +
  1134 + Map<String, Object?> _sleepIntervalPayload(
  1135 + HealthRawSleepInterval interval,
  1136 + ) {
  1137 + return <String, Object?>{
  1138 + 'start_time': interval.startTime,
  1139 + 'end_time': interval.endTime,
  1140 + };
  1141 + }
  1142 +
1014 Map<String, Object?> _localNotificationPayload( 1143 Map<String, Object?> _localNotificationPayload(
1015 HealthRawLocalNotification notification, 1144 HealthRawLocalNotification notification,
1016 ) { 1145 ) {
@@ -1805,6 +1934,10 @@ class AppleHealthRawDataCoreService { @@ -1805,6 +1934,10 @@ class AppleHealthRawDataCoreService {
1805 return DateTime(date.year, date.month, date.day); 1934 return DateTime(date.year, date.month, date.day);
1806 } 1935 }
1807 1936
  1937 + static (int startTime, int endTime) _dayRangeFromUnixSeconds(int seconds) {
  1938 + return _dayRangeFromDateKey(_dateKeyFromUnixSeconds(seconds));
  1939 + }
  1940 +
1808 static (int startTime, int endTime) _dayRangeFromDateKey(int dateKey) { 1941 static (int startTime, int endTime) _dayRangeFromDateKey(int dateKey) {
1809 final year = dateKey ~/ 10000; 1942 final year = dateKey ~/ 10000;
1810 final month = (dateKey ~/ 100) % 100; 1943 final month = (dateKey ~/ 100) % 100;
@@ -1816,6 +1949,16 @@ class AppleHealthRawDataCoreService { @@ -1816,6 +1949,16 @@ class AppleHealthRawDataCoreService {
1816 ); 1949 );
1817 } 1950 }
1818 1951
  1952 + static HealthRawSleepInterval _defaultSleepIntervalForUnixSeconds(
  1953 + int seconds,
  1954 + ) {
  1955 + final dayRange = _dayRangeFromUnixSeconds(seconds);
  1956 + return (
  1957 + startTime: dayRange.$1,
  1958 + endTime: dayRange.$1 + 6 * 60 * 60,
  1959 + );
  1960 + }
  1961 +
1819 static String _formatDateTimeMilliseconds(DateTime dateTime) { 1962 static String _formatDateTimeMilliseconds(DateTime dateTime) {
1820 String two(int value) => value.toString().padLeft(2, '0'); 1963 String two(int value) => value.toString().padLeft(2, '0');
1821 String three(int value) => value.toString().padLeft(3, '0'); 1964 String three(int value) => value.toString().padLeft(3, '0');
@@ -1914,6 +2057,8 @@ class _HealthRawLocalNotificationCandidateData { @@ -1914,6 +2057,8 @@ class _HealthRawLocalNotificationCandidateData {
1914 2057
1915 double _integerDouble(num value) => value.toInt().toDouble(); 2058 double _integerDouble(num value) => value.toInt().toDouble();
1916 2059
  2060 +int _unixSecondsNow() => DateTime.now().millisecondsSinceEpoch ~/ 1000;
  2061 +
1917 class HealthRawStressLocalStore { 2062 class HealthRawStressLocalStore {
1918 HealthRawStressLocalStore({ 2063 HealthRawStressLocalStore({
1919 Directory? rootDirectory, 2064 Directory? rootDirectory,
@@ -1935,19 +2080,20 @@ class HealthRawStressLocalStore { @@ -1935,19 +2080,20 @@ class HealthRawStressLocalStore {
1935 2080
1936 Future<void> upsertResult(HealthRawStressCalculationResult result) async { 2081 Future<void> upsertResult(HealthRawStressCalculationResult result) async {
1937 final db = await _database(result.userId); 2082 final db = await _database(result.userId);
  2083 + final updateTime = _currentUnixSeconds();
1938 await db.transaction((txn) async { 2084 await db.transaction((txn) async {
1939 for (final point in result.hrvStressPoints) { 2085 for (final point in result.hrvStressPoints) {
1940 await _upsertResettingUploaded( 2086 await _upsertResettingUploaded(
1941 txn, 2087 txn,
1942 hrvResultsTable, 2088 hrvResultsTable,
1943 - _hrvRow(point), 2089 + _hrvRow(point, updateTime),
1944 ); 2090 );
1945 } 2091 }
1946 for (final point in result.realtimeStressPoints) { 2092 for (final point in result.realtimeStressPoints) {
1947 await _upsertResettingUploaded( 2093 await _upsertResettingUploaded(
1948 txn, 2094 txn,
1949 realtimeStressResultsTable, 2095 realtimeStressResultsTable,
1950 - _realtimeRow(point), 2096 + _realtimeRow(point, updateTime),
1951 ); 2097 );
1952 } 2098 }
1953 }); 2099 });
@@ -1958,9 +2104,13 @@ class HealthRawStressLocalStore { @@ -1958,9 +2104,13 @@ class HealthRawStressLocalStore {
1958 required Iterable<HealthRawDailyStressPoint> points, 2104 required Iterable<HealthRawDailyStressPoint> points,
1959 }) async { 2105 }) async {
1960 final db = await _database(userId); 2106 final db = await _database(userId);
  2107 + final updateTime = _currentUnixSeconds();
1961 await db.transaction((txn) async { 2108 await db.transaction((txn) async {
1962 for (final point in points) { 2109 for (final point in points) {
1963 - await _upsertDailyStressResettingUploaded(txn, _dailyStressRow(point)); 2110 + await _upsertDailyStressResettingUploaded(
  2111 + txn,
  2112 + _dailyStressRow(point, updateTime),
  2113 + );
1964 } 2114 }
1965 }); 2115 });
1966 } 2116 }
@@ -1970,9 +2120,13 @@ class HealthRawStressLocalStore { @@ -1970,9 +2120,13 @@ class HealthRawStressLocalStore {
1970 required Iterable<HealthRawSleepResult> results, 2120 required Iterable<HealthRawSleepResult> results,
1971 }) async { 2121 }) async {
1972 final db = await _database(userId); 2122 final db = await _database(userId);
  2123 + final updateTime = _currentUnixSeconds();
1973 await db.transaction((txn) async { 2124 await db.transaction((txn) async {
1974 for (final result in results) { 2125 for (final result in results) {
1975 - await _upsertSleepResultResettingUploaded(txn, _sleepRow(result)); 2126 + await _upsertSleepResultResettingUploaded(
  2127 + txn,
  2128 + _sleepRow(result, updateTime),
  2129 + );
1976 } 2130 }
1977 }); 2131 });
1978 } 2132 }
@@ -2204,10 +2358,43 @@ class HealthRawStressLocalStore { @@ -2204,10 +2358,43 @@ class HealthRawStressLocalStore {
2204 return _latestRawEndTime(userId, realtimeStressResultsTable); 2358 return _latestRawEndTime(userId, realtimeStressResultsTable);
2205 } 2359 }
2206 2360
  2361 + Future<int?> latestRealtimeStressPushSendTime(int userId) {
  2362 + return _latestPushSendTime(userId, realtimeStressResultsTable);
  2363 + }
  2364 +
2207 Future<int?> earliestRealtimeRawEndTime(int userId) { 2365 Future<int?> earliestRealtimeRawEndTime(int userId) {
2208 return _earliestRawEndTime(userId, realtimeStressResultsTable); 2366 return _earliestRawEndTime(userId, realtimeStressResultsTable);
2209 } 2367 }
2210 2368
  2369 + Future<void> markLocalNotificationPushed({
  2370 + required int userId,
  2371 + required HealthRawLocalNotificationRecordType recordType,
  2372 + required int recordTime,
  2373 + required int pushSendTime,
  2374 + }) async {
  2375 + final (table, timeColumn) = switch (recordType) {
  2376 + HealthRawLocalNotificationRecordType.hrv => (
  2377 + hrvResultsTable,
  2378 + 'raw_end_time',
  2379 + ),
  2380 + HealthRawLocalNotificationRecordType.realtimeStress => (
  2381 + realtimeStressResultsTable,
  2382 + 'raw_end_time',
  2383 + ),
  2384 + HealthRawLocalNotificationRecordType.sleep => (
  2385 + sleepResultsTable,
  2386 + 'date',
  2387 + ),
  2388 + };
  2389 + final db = await _database(userId);
  2390 + await db.update(
  2391 + table,
  2392 + <String, Object?>{'push_send_time': pushSendTime},
  2393 + where: '$timeColumn = ?',
  2394 + whereArgs: [recordTime],
  2395 + );
  2396 + }
  2397 +
2211 Future<int?> latestSleepResultTime(int userId) async { 2398 Future<int?> latestSleepResultTime(int userId) async {
2212 final db = await _database(userId); 2399 final db = await _database(userId);
2213 final rows = await db.query( 2400 final rows = await db.query(
@@ -2347,7 +2534,7 @@ class HealthRawStressLocalStore { @@ -2347,7 +2534,7 @@ class HealthRawStressLocalStore {
2347 final db = await factory.openDatabase( 2534 final db = await factory.openDatabase(
2348 path, 2535 path,
2349 options: OpenDatabaseOptions( 2536 options: OpenDatabaseOptions(
2350 - version: 9, 2537 + version: 10,
2351 onCreate: (db, version) async { 2538 onCreate: (db, version) async {
2352 await _createTables(db); 2539 await _createTables(db);
2353 }, 2540 },
@@ -2377,6 +2564,9 @@ class HealthRawStressLocalStore { @@ -2377,6 +2564,9 @@ class HealthRawStressLocalStore {
2377 if (oldVersion < 9) { 2564 if (oldVersion < 9) {
2378 await _addDateKeyColumns(db); 2565 await _addDateKeyColumns(db);
2379 } 2566 }
  2567 + if (oldVersion < 10) {
  2568 + await _addUpdateTimeColumns(db);
  2569 + }
2380 }, 2570 },
2381 ), 2571 ),
2382 ); 2572 );
@@ -2404,7 +2594,9 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable ( @@ -2404,7 +2594,9 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable (
2404 is_sleep_likely INTEGER NOT NULL DEFAULT 0, 2594 is_sleep_likely INTEGER NOT NULL DEFAULT 0,
2405 is_suspected_activity INTEGER NOT NULL DEFAULT 0, 2595 is_suspected_activity INTEGER NOT NULL DEFAULT 0,
2406 uploaded INTEGER NOT NULL DEFAULT 0, 2596 uploaded INTEGER NOT NULL DEFAULT 0,
2407 - update_time INTEGER NOT NULL DEFAULT 0 2597 + update_time INTEGER NOT NULL DEFAULT 0,
  2598 + push_send_time INTEGER,
  2599 + upload_time INTEGER
2408 ) 2600 )
2409 '''); 2601 ''');
2410 await db.execute(''' 2602 await db.execute('''
@@ -2421,7 +2613,9 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( @@ -2421,7 +2613,9 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable (
2421 is_sleep_likely INTEGER NOT NULL DEFAULT 0, 2613 is_sleep_likely INTEGER NOT NULL DEFAULT 0,
2422 is_suspected_activity INTEGER NOT NULL DEFAULT 0, 2614 is_suspected_activity INTEGER NOT NULL DEFAULT 0,
2423 uploaded INTEGER NOT NULL DEFAULT 0, 2615 uploaded INTEGER NOT NULL DEFAULT 0,
2424 - update_time INTEGER NOT NULL DEFAULT 0 2616 + update_time INTEGER NOT NULL DEFAULT 0,
  2617 + push_send_time INTEGER,
  2618 + upload_time INTEGER
2425 ) 2619 )
2426 '''); 2620 ''');
2427 await _createDailyStressTable(db); 2621 await _createDailyStressTable(db);
@@ -2439,7 +2633,8 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( @@ -2439,7 +2633,8 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable (
2439 state INTEGER NOT NULL, 2633 state INTEGER NOT NULL,
2440 data_time INTEGER NOT NULL, 2634 data_time INTEGER NOT NULL,
2441 uploaded INTEGER NOT NULL DEFAULT 0, 2635 uploaded INTEGER NOT NULL DEFAULT 0,
2442 - update_time INTEGER NOT NULL DEFAULT 0 2636 + update_time INTEGER NOT NULL DEFAULT 0,
  2637 + upload_time INTEGER
2443 ) 2638 )
2444 '''); 2639 ''');
2445 } 2640 }
@@ -2457,7 +2652,9 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2457,7 +2652,9 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2457 awak_minutes INTEGER NOT NULL, 2652 awak_minutes INTEGER NOT NULL,
2458 sleep_minutes INTEGER NOT NULL, 2653 sleep_minutes INTEGER NOT NULL,
2459 uploaded INTEGER NOT NULL DEFAULT 0, 2654 uploaded INTEGER NOT NULL DEFAULT 0,
2460 - update_time INTEGER NOT NULL DEFAULT 0 2655 + update_time INTEGER NOT NULL DEFAULT 0,
  2656 + push_send_time INTEGER,
  2657 + upload_time INTEGER
2461 ) 2658 )
2462 '''); 2659 ''');
2463 } 2660 }
@@ -2517,6 +2714,21 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2517,6 +2714,21 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2517 } 2714 }
2518 } 2715 }
2519 2716
  2717 + Future<void> _addPushSendTimeColumns(DatabaseExecutor db) async {
  2718 + for (final table in const [
  2719 + hrvResultsTable,
  2720 + realtimeStressResultsTable,
  2721 + dailyStressResultsTable,
  2722 + sleepResultsTable,
  2723 + ]) {
  2724 + try {
  2725 + await db.execute('ALTER TABLE $table ADD COLUMN upload_time INTEGER');
  2726 + } on DatabaseException catch (error) {
  2727 + if (!error.isDuplicateColumnError()) rethrow;
  2728 + }
  2729 + }
  2730 + }
  2731 +
2520 Future<void> _addFlagColumns(DatabaseExecutor db, String table) async { 2732 Future<void> _addFlagColumns(DatabaseExecutor db, String table) async {
2521 for (final column in const [ 2733 for (final column in const [
2522 'is_workout', 2734 'is_workout',
@@ -2600,6 +2812,19 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2600,6 +2812,19 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2600 return rows.first['raw_end_time'] as int; 2812 return rows.first['raw_end_time'] as int;
2601 } 2813 }
2602 2814
  2815 + Future<int?> _latestPushSendTime(int userId, String table) async {
  2816 + final db = await _database(userId);
  2817 + final rows = await db.query(
  2818 + table,
  2819 + columns: ['push_send_time'],
  2820 + where: 'push_send_time IS NOT NULL',
  2821 + orderBy: 'push_send_time DESC',
  2822 + limit: 1,
  2823 + );
  2824 + if (rows.isEmpty) return null;
  2825 + return rows.first['push_send_time'] as int?;
  2826 + }
  2827 +
2603 Future<int?> _earliestRawEndTime(int userId, String table) async { 2828 Future<int?> _earliestRawEndTime(int userId, String table) async {
2604 final db = await _database(userId); 2829 final db = await _database(userId);
2605 final rows = await db.query( 2830 final rows = await db.query(
@@ -2620,9 +2845,10 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2620,9 +2845,10 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2620 final times = rawEndTimes.toList(); 2845 final times = rawEndTimes.toList();
2621 if (times.isEmpty) return; 2846 if (times.isEmpty) return;
2622 final db = await _database(userId); 2847 final db = await _database(userId);
  2848 + final uploadTime = _currentUnixSeconds();
2623 await db.update( 2849 await db.update(
2624 table, 2850 table,
2625 - {'uploaded': 1}, 2851 + {'uploaded': 1, 'upload_time': uploadTime},
2626 where: 'raw_end_time IN (${List.filled(times.length, '?').join(',')})', 2852 where: 'raw_end_time IN (${List.filled(times.length, '?').join(',')})',
2627 whereArgs: times, 2853 whereArgs: times,
2628 ); 2854 );
@@ -2636,9 +2862,10 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2636,9 +2862,10 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2636 }) async { 2862 }) async {
2637 if (time <= 0) return; 2863 if (time <= 0) return;
2638 final db = await _database(userId); 2864 final db = await _database(userId);
  2865 + final uploadTime = _currentUnixSeconds();
2639 await db.update( 2866 await db.update(
2640 table, 2867 table,
2641 - {'uploaded': 1}, 2868 + {'uploaded': 1, 'upload_time': uploadTime},
2642 where: '$timeColumn <= ? AND uploaded != 1', 2869 where: '$timeColumn <= ? AND uploaded != 1',
2643 whereArgs: [time], 2870 whereArgs: [time],
2644 ); 2871 );
@@ -2668,7 +2895,10 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2668,7 +2895,10 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2668 return date.year * 10000 + date.month * 100 + date.day; 2895 return date.year * 10000 + date.month * 100 + date.day;
2669 } 2896 }
2670 2897
2671 - Map<String, Object?> _hrvRow(HealthRawHrvStressPoint point) { 2898 + Map<String, Object?> _hrvRow(
  2899 + HealthRawHrvStressPoint point,
  2900 + int updateTime,
  2901 + ) {
2672 return <String, Object?>{ 2902 return <String, Object?>{
2673 'raw_end_time': point.rawEndTime, 2903 'raw_end_time': point.rawEndTime,
2674 'date_key': _dateKeyFromUnixSeconds(point.rawEndTime), 2904 'date_key': _dateKeyFromUnixSeconds(point.rawEndTime),
@@ -2684,11 +2914,15 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2684,11 +2914,15 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2684 'baseline_resting_hr': point.baselineRestingHr, 2914 'baseline_resting_hr': point.baselineRestingHr,
2685 ..._flagsRow(point.flags), 2915 ..._flagsRow(point.flags),
2686 'uploaded': point.uploaded ? 1 : 0, 2916 'uploaded': point.uploaded ? 1 : 0,
2687 - 'update_time': _currentUnixSeconds(), 2917 + 'update_time': updateTime,
  2918 + 'upload_time': point.uploadTime,
2688 }; 2919 };
2689 } 2920 }
2690 2921
2691 - Map<String, Object?> _realtimeRow(HealthRawRealtimeStressPoint point) { 2922 + Map<String, Object?> _realtimeRow(
  2923 + HealthRawRealtimeStressPoint point,
  2924 + int updateTime,
  2925 + ) {
2692 return <String, Object?>{ 2926 return <String, Object?>{
2693 'raw_end_time': point.rawEndTime, 2927 'raw_end_time': point.rawEndTime,
2694 'date_key': _dateKeyFromUnixSeconds(point.rawEndTime), 2928 'date_key': _dateKeyFromUnixSeconds(point.rawEndTime),
@@ -2699,11 +2933,15 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2699,11 +2933,15 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2699 'source_end_time': point.sourceEndTime, 2933 'source_end_time': point.sourceEndTime,
2700 ..._flagsRow(point.flags), 2934 ..._flagsRow(point.flags),
2701 'uploaded': point.uploaded ? 1 : 0, 2935 'uploaded': point.uploaded ? 1 : 0,
2702 - 'update_time': _currentUnixSeconds(), 2936 + 'update_time': updateTime,
  2937 + 'upload_time': point.uploadTime,
2703 }; 2938 };
2704 } 2939 }
2705 2940
2706 - Map<String, Object?> _dailyStressRow(HealthRawDailyStressPoint point) { 2941 + Map<String, Object?> _dailyStressRow(
  2942 + HealthRawDailyStressPoint point,
  2943 + int updateTime,
  2944 + ) {
2707 return <String, Object?>{ 2945 return <String, Object?>{
2708 'date': point.date, 2946 'date': point.date,
2709 'date_key': point.date, 2947 'date_key': point.date,
@@ -2713,11 +2951,12 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2713,11 +2951,12 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2713 'state': point.state.value, 2951 'state': point.state.value,
2714 'data_time': point.dataTime, 2952 'data_time': point.dataTime,
2715 'uploaded': point.uploaded ? 1 : 0, 2953 'uploaded': point.uploaded ? 1 : 0,
2716 - 'update_time': _currentUnixSeconds(), 2954 + 'update_time': updateTime,
  2955 + 'upload_time': point.uploadTime,
2717 }; 2956 };
2718 } 2957 }
2719 2958
2720 - Map<String, Object?> _sleepRow(HealthRawSleepResult result) { 2959 + Map<String, Object?> _sleepRow(HealthRawSleepResult result, int updateTime) {
2721 return <String, Object?>{ 2960 return <String, Object?>{
2722 'date': result.date, 2961 'date': result.date,
2723 'date_key': _dateKeyFromUnixSeconds(result.date), 2962 'date_key': _dateKeyFromUnixSeconds(result.date),
@@ -2729,7 +2968,8 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2729,7 +2968,8 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2729 'awak_minutes': result.awakMinutes, 2968 'awak_minutes': result.awakMinutes,
2730 'sleep_minutes': result.sleepMinutes, 2969 'sleep_minutes': result.sleepMinutes,
2731 'uploaded': result.uploaded ? 1 : 0, 2970 'uploaded': result.uploaded ? 1 : 0,
2732 - 'update_time': _currentUnixSeconds(), 2971 + 'update_time': updateTime,
  2972 + 'upload_time': result.uploadTime,
2733 }; 2973 };
2734 } 2974 }
2735 2975
@@ -2798,6 +3038,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2798,6 +3038,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2798 'is_sleep_likely': row['is_sleep_likely'], 3038 'is_sleep_likely': row['is_sleep_likely'],
2799 'is_suspected_activity': row['is_suspected_activity'], 3039 'is_suspected_activity': row['is_suspected_activity'],
2800 'uploaded': uploadPayloadChanged ? 0 : existingRow['uploaded'], 3040 'uploaded': uploadPayloadChanged ? 0 : existingRow['uploaded'],
  3041 + 'upload_time': uploadPayloadChanged ? null : existingRow['upload_time'],
2801 'update_time': row['update_time'], 3042 'update_time': row['update_time'],
2802 }, 3043 },
2803 where: 'raw_end_time = ?', 3044 where: 'raw_end_time = ?',
@@ -2832,6 +3073,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2832,6 +3073,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2832 final isToday = row['date'] == _todayDateKey(); 3073 final isToday = row['date'] == _todayDateKey();
2833 if (isToday) { 3074 if (isToday) {
2834 row['uploaded'] = 0; 3075 row['uploaded'] = 0;
  3076 + row['upload_time'] = null;
2835 } 3077 }
2836 final existing = await db.query( 3078 final existing = await db.query(
2837 dailyStressResultsTable, 3079 dailyStressResultsTable,
@@ -2884,6 +3126,8 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2884,6 +3126,8 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2884 'state': row['state'], 3126 'state': row['state'],
2885 'data_time': row['data_time'], 3127 'data_time': row['data_time'],
2886 'uploaded': isToday ? 0 : (valueChanged ? 0 : existingRow['uploaded']), 3128 'uploaded': isToday ? 0 : (valueChanged ? 0 : existingRow['uploaded']),
  3129 + 'upload_time':
  3130 + isToday || valueChanged ? null : existingRow['upload_time'],
2887 'update_time': row['update_time'], 3131 'update_time': row['update_time'],
2888 }, 3132 },
2889 where: 'date = ?', 3133 where: 'date = ?',
@@ -2945,6 +3189,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2945,6 +3189,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2945 'awak_minutes': row['awak_minutes'], 3189 'awak_minutes': row['awak_minutes'],
2946 'sleep_minutes': row['sleep_minutes'], 3190 'sleep_minutes': row['sleep_minutes'],
2947 'uploaded': valueChanged ? 0 : existingRow['uploaded'], 3191 'uploaded': valueChanged ? 0 : existingRow['uploaded'],
  3192 + 'upload_time': valueChanged ? null : existingRow['upload_time'],
2948 'update_time': row['update_time'], 3193 'update_time': row['update_time'],
2949 }, 3194 },
2950 where: 'date = ?', 3195 where: 'date = ?',
1 -import 'dart:convert';  
2 -import 'dart:io';  
3 -  
4 -import 'package:path_provider/path_provider.dart';  
5 -  
6 -class HealthRawLocalNotificationDebugStore {  
7 - HealthRawLocalNotificationDebugStore({Directory? rootDirectory})  
8 - : _rootDirectory = rootDirectory;  
9 -  
10 - static const _fileName = 'health_raw_local_notification_debug_events.json';  
11 - static const _maxEventCount = 500;  
12 -  
13 - final Directory? _rootDirectory;  
14 -  
15 - Future<void> append(Map<String, Object?> event) async {  
16 - final file = await _file();  
17 - await file.parent.create(recursive: true);  
18 - final eventsByMinute = await _readEventsByMinute(file);  
19 - final now = DateTime.now();  
20 - final minuteKey = _minuteKey(now);  
21 - final enrichedEvent = <String, Object?>{  
22 - ..._sanitizeMap(event),  
23 - 'saved_at': now.toIso8601String(),  
24 - 'saved_at_readable': _readableTime(now),  
25 - 'saved_at_minute': minuteKey,  
26 - 'saved_at_unix': now.millisecondsSinceEpoch / 1000,  
27 - };  
28 - eventsByMinute.putIfAbsent(minuteKey, () => <Map<String, Object?>>[]);  
29 - eventsByMinute[minuteKey]!.add(enrichedEvent);  
30 - final limitedEventsByMinute = _limitedEventsByMinute(eventsByMinute);  
31 - const encoder = JsonEncoder.withIndent(' ');  
32 - await file.writeAsString(  
33 - encoder.convert(limitedEventsByMinute),  
34 - flush: true,  
35 - );  
36 - }  
37 -  
38 - Future<String> readText() async {  
39 - final file = await _file();  
40 - if (!await file.exists()) {  
41 - return '暂无本地推送诊断日志';  
42 - }  
43 - final text = await file.readAsString();  
44 - if (text.trim().isEmpty) {  
45 - return '暂无本地推送诊断日志';  
46 - }  
47 - return text;  
48 - }  
49 -  
50 - Future<File> _file() async {  
51 - final rootDirectory =  
52 - _rootDirectory ?? await getApplicationDocumentsDirectory();  
53 - return File('${rootDirectory.path}/$_fileName');  
54 - }  
55 -  
56 - Future<Map<String, List<Map<String, Object?>>>> _readEventsByMinute(  
57 - File file,  
58 - ) async {  
59 - if (!await file.exists()) {  
60 - return <String, List<Map<String, Object?>>>{};  
61 - }  
62 - final text = await file.readAsString();  
63 - if (text.trim().isEmpty) {  
64 - return <String, List<Map<String, Object?>>>{};  
65 - }  
66 - final json = jsonDecode(text);  
67 - if (json is Map) {  
68 - return json.map((key, value) {  
69 - final events = value is List  
70 - ? value  
71 - .whereType<Map>()  
72 - .map((e) => _sanitizeMap(e.cast<String, Object?>()))  
73 - .toList()  
74 - : <Map<String, Object?>>[];  
75 - return MapEntry(key.toString(), events);  
76 - });  
77 - }  
78 - if (json is List) {  
79 - final events = json  
80 - .whereType<Map>()  
81 - .map((e) => _sanitizeMap(e.cast<String, Object?>()))  
82 - .toList();  
83 - final grouped = <String, List<Map<String, Object?>>>{};  
84 - for (final event in events) {  
85 - final key =  
86 - (event['saved_at_minute'] ?? event['saved_at_readable'] ?? 'legacy')  
87 - .toString();  
88 - grouped.putIfAbsent(key, () => <Map<String, Object?>>[]);  
89 - grouped[key]!.add(event);  
90 - }  
91 - return grouped;  
92 - }  
93 - return <String, List<Map<String, Object?>>>{};  
94 - }  
95 -  
96 - Map<String, List<Map<String, Object?>>> _limitedEventsByMinute(  
97 - Map<String, List<Map<String, Object?>>> eventsByMinute,  
98 - ) {  
99 - final events = eventsByMinute.values.expand((e) => e).toList()  
100 - ..sort((a, b) {  
101 - final aTime = (a['saved_at_unix'] as num?)?.toDouble() ?? 0;  
102 - final bTime = (b['saved_at_unix'] as num?)?.toDouble() ?? 0;  
103 - return aTime.compareTo(bTime);  
104 - });  
105 - final limitedEvents = events.length > _maxEventCount  
106 - ? events.sublist(events.length - _maxEventCount)  
107 - : events;  
108 - final grouped = <String, List<Map<String, Object?>>>{};  
109 - for (final event in limitedEvents) {  
110 - final key =  
111 - (event['saved_at_minute'] ?? event['saved_at_readable'] ?? 'unknown')  
112 - .toString();  
113 - grouped.putIfAbsent(key, () => <Map<String, Object?>>[]);  
114 - grouped[key]!.add(event);  
115 - }  
116 - return grouped;  
117 - }  
118 -  
119 - static Map<String, Object?> _sanitizeMap(Map<String, Object?> map) {  
120 - return map.map((key, value) => MapEntry(key, _sanitizeValue(value)));  
121 - }  
122 -  
123 - static Object? _sanitizeValue(Object? value) {  
124 - return switch (value) {  
125 - null => null,  
126 - String() => value,  
127 - num() => value,  
128 - bool() => value,  
129 - DateTime() => value.toIso8601String(),  
130 - List() => value.map(_sanitizeValue).toList(),  
131 - Map() => value.map(  
132 - (key, value) => MapEntry(key.toString(), _sanitizeValue(value)),  
133 - ),  
134 - _ => value.toString(),  
135 - };  
136 - }  
137 -  
138 - static String _minuteKey(DateTime time) {  
139 - return '${time.year.toString().padLeft(4, '0')}-'  
140 - '${time.month.toString().padLeft(2, '0')}-'  
141 - '${time.day.toString().padLeft(2, '0')} '  
142 - '${time.hour.toString().padLeft(2, '0')}:'  
143 - '${time.minute.toString().padLeft(2, '0')}';  
144 - }  
145 -  
146 - static String _readableTime(DateTime time) {  
147 - return '${time.year.toString().padLeft(4, '0')}-'  
148 - '${time.month.toString().padLeft(2, '0')}-'  
149 - '${time.day.toString().padLeft(2, '0')} '  
150 - '${time.hour.toString().padLeft(2, '0')}:'  
151 - '${time.minute.toString().padLeft(2, '0')}:'  
152 - '${time.second.toString().padLeft(2, '0')}';  
153 - }  
154 -}