Commit 2a66a694034c6fc1ca97a4bfdba49cf466aa29a1

Authored by 权海
1 parent 2ad10b3d

feat(ui):增加本地通知日志系统

@@ -396,10 +396,14 @@ protocol HealthKitRawDataHostApi { @@ -396,10 +396,14 @@ protocol HealthKitRawDataHostApi {
396 /// caculate 一旦完成,让native 存储这次的计算记录 396 /// caculate 一旦完成,让native 存储这次的计算记录
397 /// 需要保存引起此次计算的数据变化类型列表、当前时间 397 /// 需要保存引起此次计算的数据变化类型列表、当前时间
398 func saveFlutterCaculateFinished(relativeDataTypes: [Int64], completion: @escaping (Result<Bool, Error>) -> Void) 398 func saveFlutterCaculateFinished(relativeDataTypes: [Int64], completion: @escaping (Result<Bool, Error>) -> Void)
  399 + /// 让原生保存本地通知记录
  400 + func saveLocalNotificationRecord(notificationType: String, timestamp: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
399 /// 让原生分享出来本地的记录文件 401 /// 让原生分享出来本地的记录文件
400 func shareNativeAppleHealthObserverRecord(completion: @escaping (Result<Bool, Error>) -> Void) 402 func shareNativeAppleHealthObserverRecord(completion: @escaping (Result<Bool, Error>) -> Void)
  403 + func saveLocalNotificationDebugEvent(event: [String: Any?], completion: @escaping (Result<Bool, Error>) -> Void)
401 /// 让原生分享出来flutter的记录文件 404 /// 让原生分享出来flutter的记录文件
402 func shareFlutterObserverRecord(completion: @escaping (Result<Bool, Error>) -> Void) 405 func shareFlutterObserverRecord(completion: @escaping (Result<Bool, Error>) -> Void)
  406 + func shareLocalNotificationDebugRecord(completion: @escaping (Result<Bool, Error>) -> Void)
403 /// 让原生分享出来上传记录 407 /// 让原生分享出来上传记录
404 func shareUploadTaskRecord(completion: @escaping (Result<Bool, Error>) -> Void) 408 func shareUploadTaskRecord(completion: @escaping (Result<Bool, Error>) -> Void)
405 } 409 }
@@ -650,6 +654,25 @@ class HealthKitRawDataHostApiSetup { @@ -650,6 +654,25 @@ class HealthKitRawDataHostApiSetup {
650 } else { 654 } else {
651 saveFlutterCaculateFinishedChannel.setMessageHandler(nil) 655 saveFlutterCaculateFinishedChannel.setMessageHandler(nil)
652 } 656 }
  657 + /// 让原生保存本地通知记录
  658 + let saveLocalNotificationRecordChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.saveLocalNotificationRecord\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  659 + if let api = api {
  660 + saveLocalNotificationRecordChannel.setMessageHandler { message, reply in
  661 + let args = message as! [Any?]
  662 + let notificationTypeArg = args[0] as! String
  663 + let timestampArg = args[1] as! Int64
  664 + api.saveLocalNotificationRecord(notificationType: notificationTypeArg, timestamp: timestampArg) { result in
  665 + switch result {
  666 + case .success(let res):
  667 + reply(wrapResult(res))
  668 + case .failure(let error):
  669 + reply(wrapError(error))
  670 + }
  671 + }
  672 + }
  673 + } else {
  674 + saveLocalNotificationRecordChannel.setMessageHandler(nil)
  675 + }
653 /// 让原生分享出来本地的记录文件 676 /// 让原生分享出来本地的记录文件
654 let shareNativeAppleHealthObserverRecordChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareNativeAppleHealthObserverRecord\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) 677 let shareNativeAppleHealthObserverRecordChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareNativeAppleHealthObserverRecord\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
655 if let api = api { 678 if let api = api {
@@ -666,6 +689,23 @@ class HealthKitRawDataHostApiSetup { @@ -666,6 +689,23 @@ class HealthKitRawDataHostApiSetup {
666 } else { 689 } else {
667 shareNativeAppleHealthObserverRecordChannel.setMessageHandler(nil) 690 shareNativeAppleHealthObserverRecordChannel.setMessageHandler(nil)
668 } 691 }
  692 + let saveLocalNotificationDebugEventChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.saveLocalNotificationDebugEvent\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  693 + if let api = api {
  694 + saveLocalNotificationDebugEventChannel.setMessageHandler { message, reply in
  695 + let args = message as! [Any?]
  696 + let eventArg = args[0] as! [String: Any?]
  697 + api.saveLocalNotificationDebugEvent(event: eventArg) { result in
  698 + switch result {
  699 + case .success(let res):
  700 + reply(wrapResult(res))
  701 + case .failure(let error):
  702 + reply(wrapError(error))
  703 + }
  704 + }
  705 + }
  706 + } else {
  707 + saveLocalNotificationDebugEventChannel.setMessageHandler(nil)
  708 + }
669 /// 让原生分享出来flutter的记录文件 709 /// 让原生分享出来flutter的记录文件
670 let shareFlutterObserverRecordChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareFlutterObserverRecord\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) 710 let shareFlutterObserverRecordChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareFlutterObserverRecord\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
671 if let api = api { 711 if let api = api {
@@ -682,6 +722,21 @@ class HealthKitRawDataHostApiSetup { @@ -682,6 +722,21 @@ class HealthKitRawDataHostApiSetup {
682 } else { 722 } else {
683 shareFlutterObserverRecordChannel.setMessageHandler(nil) 723 shareFlutterObserverRecordChannel.setMessageHandler(nil)
684 } 724 }
  725 + let shareLocalNotificationDebugRecordChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareLocalNotificationDebugRecord\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  726 + if let api = api {
  727 + shareLocalNotificationDebugRecordChannel.setMessageHandler { _, reply in
  728 + api.shareLocalNotificationDebugRecord { result in
  729 + switch result {
  730 + case .success(let res):
  731 + reply(wrapResult(res))
  732 + case .failure(let error):
  733 + reply(wrapError(error))
  734 + }
  735 + }
  736 + }
  737 + } else {
  738 + shareLocalNotificationDebugRecordChannel.setMessageHandler(nil)
  739 + }
685 /// 让原生分享出来上传记录 740 /// 让原生分享出来上传记录
686 let shareUploadTaskRecordChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareUploadTaskRecord\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) 741 let shareUploadTaskRecordChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareUploadTaskRecord\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
687 if let api = api { 742 if let api = api {
@@ -25,6 +25,7 @@ enum DeveloperOptionsAction { @@ -25,6 +25,7 @@ enum DeveloperOptionsAction {
25 shareNativeAppleHealthObserverRecord, 25 shareNativeAppleHealthObserverRecord,
26 shareFlutterObserverRecord, 26 shareFlutterObserverRecord,
27 shareUploadTaskRecord, 27 shareUploadTaskRecord,
  28 + shareLocalNotificationDebugRecord,
28 clearHealthRawDataDatabaseAndUploadLog, 29 clearHealthRawDataDatabaseAndUploadLog,
29 } 30 }
30 31
@@ -73,6 +74,10 @@ class DeveloperOptionsController extends GetxController { @@ -73,6 +74,10 @@ class DeveloperOptionsController extends GetxController {
73 action: DeveloperOptionsAction.shareUploadTaskRecord, 74 action: DeveloperOptionsAction.shareUploadTaskRecord,
74 ), 75 ),
75 DeveloperOptionsItem( 76 DeveloperOptionsItem(
  77 + title: '分享本地推送诊断日志',
  78 + action: DeveloperOptionsAction.shareLocalNotificationDebugRecord,
  79 + ),
  80 + DeveloperOptionsItem(
76 title: '清除本地数据库、上传日志', 81 title: '清除本地数据库、上传日志',
77 action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog, 82 action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
78 ), 83 ),
@@ -107,6 +112,9 @@ class DeveloperOptionsController extends GetxController { @@ -107,6 +112,9 @@ class DeveloperOptionsController extends GetxController {
107 case DeveloperOptionsAction.shareUploadTaskRecord: 112 case DeveloperOptionsAction.shareUploadTaskRecord:
108 await shareUploadTaskRecord(); 113 await shareUploadTaskRecord();
109 break; 114 break;
  115 + case DeveloperOptionsAction.shareLocalNotificationDebugRecord:
  116 + await shareLocalNotificationDebugRecord();
  117 + break;
110 case DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog: 118 case DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog:
111 await clearHealthRawDataDatabaseAndUploadLog(); 119 await clearHealthRawDataDatabaseAndUploadLog();
112 break; 120 break;
@@ -174,6 +182,24 @@ class DeveloperOptionsController extends GetxController { @@ -174,6 +182,24 @@ class DeveloperOptionsController extends GetxController {
174 } 182 }
175 } 183 }
176 184
  185 + Future<void> shareLocalNotificationDebugRecord() async {
  186 + try {
  187 + final sharedByNative =
  188 + await _healthRawDataCoreService.shareLocalNotificationDebugRecord();
  189 + if (sharedByNative) return;
  190 + final text =
  191 + await _healthRawDataCoreService.readLocalNotificationDebugLogText();
  192 + await _platformHostApi.shareText(text);
  193 + } catch (error, stackTrace) {
  194 + AppLogger.e(
  195 + 'Share local notification debug record failed',
  196 + error,
  197 + stackTrace,
  198 + );
  199 + Get.snackbar('Share failed', error.toString());
  200 + }
  201 + }
  202 +
177 Future<void> clearHealthRawDataDatabaseAndUploadLog() async { 203 Future<void> clearHealthRawDataDatabaseAndUploadLog() async {
178 try { 204 try {
179 await _healthRawDataCoreService.clearLocalDatabaseAndUploadLog(); 205 await _healthRawDataCoreService.clearLocalDatabaseAndUploadLog();
@@ -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');
  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({required bool isDebug}) async {
  39 + if (!isDebug) {
  40 + return '当前非 Debug 模式,未记录本地推送诊断日志';
  41 + }
  42 + final file = await _file();
  43 + if (!await file.exists()) {
  44 + return '暂无本地推送诊断日志';
  45 + }
  46 + final text = await file.readAsString();
  47 + if (text.trim().isEmpty) {
  48 + return '暂无本地推送诊断日志';
  49 + }
  50 + return text;
  51 + }
  52 +
  53 + Future<File> _file() async {
  54 + final rootDirectory =
  55 + _rootDirectory ?? await getApplicationDocumentsDirectory();
  56 + return File('${rootDirectory.path}/$_fileName');
  57 + }
  58 +
  59 + Future<Map<String, List<Map<String, Object?>>>> _readEventsByMinute(
  60 + File file,
  61 + ) async {
  62 + if (!await file.exists()) {
  63 + return <String, List<Map<String, Object?>>>{};
  64 + }
  65 + final text = await file.readAsString();
  66 + if (text.trim().isEmpty) {
  67 + return <String, List<Map<String, Object?>>>{};
  68 + }
  69 + final json = jsonDecode(text);
  70 + if (json is Map) {
  71 + return json.map((key, value) {
  72 + final events = value is List
  73 + ? value
  74 + .whereType<Map>()
  75 + .map((e) => _sanitizeMap(e.cast<String, Object?>()))
  76 + .toList()
  77 + : <Map<String, Object?>>[];
  78 + return MapEntry(key.toString(), events);
  79 + });
  80 + }
  81 + if (json is List) {
  82 + final events = json
  83 + .whereType<Map>()
  84 + .map((e) => _sanitizeMap(e.cast<String, Object?>()))
  85 + .toList();
  86 + final grouped = <String, List<Map<String, Object?>>>{};
  87 + for (final event in events) {
  88 + final key =
  89 + (event['saved_at_minute'] ?? event['saved_at_readable'] ?? 'legacy')
  90 + .toString();
  91 + grouped.putIfAbsent(key, () => <Map<String, Object?>>[]);
  92 + grouped[key]!.add(event);
  93 + }
  94 + return grouped;
  95 + }
  96 + return <String, List<Map<String, Object?>>>{};
  97 + }
  98 +
  99 + Map<String, List<Map<String, Object?>>> _limitedEventsByMinute(
  100 + Map<String, List<Map<String, Object?>>> eventsByMinute,
  101 + ) {
  102 + final events = eventsByMinute.values.expand((e) => e).toList()
  103 + ..sort((a, b) {
  104 + final aTime = (a['saved_at_unix'] as num?)?.toDouble() ?? 0;
  105 + final bTime = (b['saved_at_unix'] as num?)?.toDouble() ?? 0;
  106 + return aTime.compareTo(bTime);
  107 + });
  108 + final limitedEvents = events.length > _maxEventCount
  109 + ? events.sublist(events.length - _maxEventCount)
  110 + : events;
  111 + final grouped = <String, List<Map<String, Object?>>>{};
  112 + for (final event in limitedEvents) {
  113 + final key =
  114 + (event['saved_at_minute'] ?? event['saved_at_readable'] ?? 'unknown')
  115 + .toString();
  116 + grouped.putIfAbsent(key, () => <Map<String, Object?>>[]);
  117 + grouped[key]!.add(event);
  118 + }
  119 + return grouped;
  120 + }
  121 +
  122 + static Map<String, Object?> _sanitizeMap(Map<String, Object?> map) {
  123 + return map.map((key, value) => MapEntry(key, _sanitizeValue(value)));
  124 + }
  125 +
  126 + static Object? _sanitizeValue(Object? value) {
  127 + return switch (value) {
  128 + null => null,
  129 + String() => value,
  130 + num() => value,
  131 + bool() => value,
  132 + DateTime() => value.toIso8601String(),
  133 + List() => value.map(_sanitizeValue).toList(),
  134 + Map() => value.map(
  135 + (key, value) => MapEntry(key.toString(), _sanitizeValue(value)),
  136 + ),
  137 + _ => value.toString(),
  138 + };
  139 + }
  140 +
  141 + static String _minuteKey(DateTime time) {
  142 + return '${time.year.toString().padLeft(4, '0')}-'
  143 + '${time.month.toString().padLeft(2, '0')}-'
  144 + '${time.day.toString().padLeft(2, '0')} '
  145 + '${time.hour.toString().padLeft(2, '0')}:'
  146 + '${time.minute.toString().padLeft(2, '0')}';
  147 + }
  148 +
  149 + static String _readableTime(DateTime time) {
  150 + return '${time.year.toString().padLeft(4, '0')}-'
  151 + '${time.month.toString().padLeft(2, '0')}-'
  152 + '${time.day.toString().padLeft(2, '0')} '
  153 + '${time.hour.toString().padLeft(2, '0')}:'
  154 + '${time.minute.toString().padLeft(2, '0')}:'
  155 + '${time.second.toString().padLeft(2, '0')}';
  156 + }
  157 +}
@@ -764,6 +764,35 @@ class HealthKitRawDataHostApi { @@ -764,6 +764,35 @@ class HealthKitRawDataHostApi {
764 } 764 }
765 } 765 }
766 766
  767 + /// 让原生保存本地通知记录
  768 + Future<bool> saveLocalNotificationRecord({required String notificationType, required int timestamp}) async {
  769 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.saveLocalNotificationRecord$pigeonVar_messageChannelSuffix';
  770 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  771 + pigeonVar_channelName,
  772 + pigeonChannelCodec,
  773 + binaryMessenger: pigeonVar_binaryMessenger,
  774 + );
  775 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[notificationType, timestamp]);
  776 + final List<Object?>? pigeonVar_replyList =
  777 + await pigeonVar_sendFuture as List<Object?>?;
  778 + if (pigeonVar_replyList == null) {
  779 + throw _createConnectionError(pigeonVar_channelName);
  780 + } else if (pigeonVar_replyList.length > 1) {
  781 + throw PlatformException(
  782 + code: pigeonVar_replyList[0]! as String,
  783 + message: pigeonVar_replyList[1] as String?,
  784 + details: pigeonVar_replyList[2],
  785 + );
  786 + } else if (pigeonVar_replyList[0] == null) {
  787 + throw PlatformException(
  788 + code: 'null-error',
  789 + message: 'Host platform returned null value for non-null return value.',
  790 + );
  791 + } else {
  792 + return (pigeonVar_replyList[0] as bool?)!;
  793 + }
  794 + }
  795 +
767 /// 让原生分享出来本地的记录文件 796 /// 让原生分享出来本地的记录文件
768 Future<bool> shareNativeAppleHealthObserverRecord() async { 797 Future<bool> shareNativeAppleHealthObserverRecord() async {
769 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareNativeAppleHealthObserverRecord$pigeonVar_messageChannelSuffix'; 798 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareNativeAppleHealthObserverRecord$pigeonVar_messageChannelSuffix';
@@ -793,6 +822,34 @@ class HealthKitRawDataHostApi { @@ -793,6 +822,34 @@ class HealthKitRawDataHostApi {
793 } 822 }
794 } 823 }
795 824
  825 + Future<bool> saveLocalNotificationDebugEvent({required Map<String, Object?> event}) async {
  826 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.saveLocalNotificationDebugEvent$pigeonVar_messageChannelSuffix';
  827 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  828 + pigeonVar_channelName,
  829 + pigeonChannelCodec,
  830 + binaryMessenger: pigeonVar_binaryMessenger,
  831 + );
  832 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[event]);
  833 + final List<Object?>? pigeonVar_replyList =
  834 + await pigeonVar_sendFuture as List<Object?>?;
  835 + if (pigeonVar_replyList == null) {
  836 + throw _createConnectionError(pigeonVar_channelName);
  837 + } else if (pigeonVar_replyList.length > 1) {
  838 + throw PlatformException(
  839 + code: pigeonVar_replyList[0]! as String,
  840 + message: pigeonVar_replyList[1] as String?,
  841 + details: pigeonVar_replyList[2],
  842 + );
  843 + } else if (pigeonVar_replyList[0] == null) {
  844 + throw PlatformException(
  845 + code: 'null-error',
  846 + message: 'Host platform returned null value for non-null return value.',
  847 + );
  848 + } else {
  849 + return (pigeonVar_replyList[0] as bool?)!;
  850 + }
  851 + }
  852 +
796 /// 让原生分享出来flutter的记录文件 853 /// 让原生分享出来flutter的记录文件
797 Future<bool> shareFlutterObserverRecord() async { 854 Future<bool> shareFlutterObserverRecord() async {
798 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareFlutterObserverRecord$pigeonVar_messageChannelSuffix'; 855 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareFlutterObserverRecord$pigeonVar_messageChannelSuffix';
@@ -822,6 +879,34 @@ class HealthKitRawDataHostApi { @@ -822,6 +879,34 @@ class HealthKitRawDataHostApi {
822 } 879 }
823 } 880 }
824 881
  882 + Future<bool> shareLocalNotificationDebugRecord() async {
  883 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareLocalNotificationDebugRecord$pigeonVar_messageChannelSuffix';
  884 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  885 + pigeonVar_channelName,
  886 + pigeonChannelCodec,
  887 + binaryMessenger: pigeonVar_binaryMessenger,
  888 + );
  889 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
  890 + final List<Object?>? pigeonVar_replyList =
  891 + await pigeonVar_sendFuture as List<Object?>?;
  892 + if (pigeonVar_replyList == null) {
  893 + throw _createConnectionError(pigeonVar_channelName);
  894 + } else if (pigeonVar_replyList.length > 1) {
  895 + throw PlatformException(
  896 + code: pigeonVar_replyList[0]! as String,
  897 + message: pigeonVar_replyList[1] as String?,
  898 + details: pigeonVar_replyList[2],
  899 + );
  900 + } else if (pigeonVar_replyList[0] == null) {
  901 + throw PlatformException(
  902 + code: 'null-error',
  903 + message: 'Host platform returned null value for non-null return value.',
  904 + );
  905 + } else {
  906 + return (pigeonVar_replyList[0] as bool?)!;
  907 + }
  908 + }
  909 +
825 /// 让原生分享出来上传记录 910 /// 让原生分享出来上传记录
826 Future<bool> shareUploadTaskRecord() async { 911 Future<bool> shareUploadTaskRecord() async {
827 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareUploadTaskRecord$pigeonVar_messageChannelSuffix'; 912 final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.shareUploadTaskRecord$pigeonVar_messageChannelSuffix';
@@ -155,14 +155,25 @@ abstract class HealthKitRawDataHostApi { @@ -155,14 +155,25 @@ abstract class HealthKitRawDataHostApi {
155 @async 155 @async
156 bool saveFlutterCaculateFinished({required List<int> relativeDataTypes}); 156 bool saveFlutterCaculateFinished({required List<int> relativeDataTypes});
157 157
  158 + /// 让原生保存本地通知记录
  159 + @async
  160 + bool saveLocalNotificationRecord(
  161 + {required String notificationType, required int timestamp});
  162 +
158 /// 让原生分享出来本地的记录文件 163 /// 让原生分享出来本地的记录文件
159 @async 164 @async
160 bool shareNativeAppleHealthObserverRecord(); 165 bool shareNativeAppleHealthObserverRecord();
  166 +
  167 + @async
  168 + bool saveLocalNotificationDebugEvent({required Map<String, Object?> event});
161 169
162 /// 让原生分享出来flutter的记录文件 170 /// 让原生分享出来flutter的记录文件
163 @async 171 @async
164 bool shareFlutterObserverRecord(); 172 bool shareFlutterObserverRecord();
165 173
  174 + @async
  175 + bool shareLocalNotificationDebugRecord();
  176 +
166 /// 让原生分享出来上传记录 177 /// 让原生分享出来上传记录
167 @async 178 @async
168 bool shareUploadTaskRecord(); 179 bool shareUploadTaskRecord();