|
...
|
...
|
@@ -16,6 +16,7 @@ import '../config/app_environment_config.dart'; |
|
|
|
import '../logging/app_logger.dart';
|
|
|
|
import '../network/api/health_api.dart';
|
|
|
|
import '../result/app_result.dart';
|
|
|
|
import 'health_raw_local_notification_debug_store.dart';
|
|
|
|
import 'health_raw_local_notification.dart';
|
|
|
|
import 'health_raw_stress_calculator.dart';
|
|
|
|
import 'health_sleep_calculator.dart';
|
|
...
|
...
|
@@ -32,6 +33,7 @@ class HealthRawDataCoreService { |
|
|
|
int Function()? userIdProvider,
|
|
|
|
bool uploadResultsAfterCalculation = true,
|
|
|
|
HealthRawLocalNotificationDispatcher? localNotificationDispatcher,
|
|
|
|
HealthRawLocalNotificationDebugStore? localNotificationDebugStore,
|
|
|
|
HealthApi? serverHealthApi,
|
|
|
|
}) : _healthApi = healthApi ?? HealthKitHostApi(),
|
|
|
|
_rawDataApi = rawDataApi ?? HealthKitRawDataHostApi(),
|
|
...
|
...
|
@@ -41,6 +43,8 @@ class HealthRawDataCoreService { |
|
|
|
_uploadResultsAfterCalculation = uploadResultsAfterCalculation,
|
|
|
|
_localNotificationDispatcher = localNotificationDispatcher ??
|
|
|
|
HealthRawLocalNotificationDispatcher(),
|
|
|
|
_localNotificationDebugStore = localNotificationDebugStore ??
|
|
|
|
HealthRawLocalNotificationDebugStore(),
|
|
|
|
_serverHealthApi = serverHealthApi;
|
|
|
|
|
|
|
|
final HealthKitHostApi _healthApi;
|
|
...
|
...
|
@@ -50,6 +54,7 @@ class HealthRawDataCoreService { |
|
|
|
final int Function()? _userIdProvider;
|
|
|
|
final bool _uploadResultsAfterCalculation;
|
|
|
|
final HealthRawLocalNotificationDispatcher _localNotificationDispatcher;
|
|
|
|
final HealthRawLocalNotificationDebugStore _localNotificationDebugStore;
|
|
|
|
final HealthApi? _serverHealthApi;
|
|
|
|
final StreamController<HealthRawDataUpdatedEvent>
|
|
|
|
_healthDataUpdatedController =
|
|
...
|
...
|
@@ -187,6 +192,19 @@ class HealthRawDataCoreService { |
|
|
|
return _rawDataApi.shareUploadTaskRecord();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<String> readLocalNotificationDebugLogText() {
|
|
|
|
return _localNotificationDebugStore.readText(isDebug: _isDebug);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> shareLocalNotificationDebugRecord() async {
|
|
|
|
if (!_isDebug) return false;
|
|
|
|
return _shareNativeLocalNotificationDebugRecord();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> _shareNativeLocalNotificationDebugRecord() async {
|
|
|
|
return _rawDataApi.shareLocalNotificationDebugRecord();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<HealthRawStressCalculationResult> startCoreCaculate({
|
|
|
|
int? endTime,
|
|
|
|
int readChunkDays = defaultReadChunkDays,
|
|
...
|
...
|
@@ -399,9 +417,30 @@ class HealthRawDataCoreService { |
|
|
|
required HealthRawStressCalculationResult result,
|
|
|
|
required bool hasExistingHrv,
|
|
|
|
}) async {
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterNotificationCalculationFinished',
|
|
|
|
'user_id': result.userId,
|
|
|
|
'has_existing_hrv': hasExistingHrv,
|
|
|
|
'hrv_count': result.hrvStressPoints.length,
|
|
|
|
'realtime_count': result.realtimeStressPoints.length,
|
|
|
|
'sleep_count': result.sleepResults.length,
|
|
|
|
'latest_hrv': _latestHrvStressPointPayload(result.hrvStressPoints),
|
|
|
|
'latest_realtime':
|
|
|
|
_latestRealtimeStressPointPayload(result.realtimeStressPoints),
|
|
|
|
'latest_sleep': _latestSleepResultPayload(result.sleepResults),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if (result.hrvStressPoints.isEmpty &&
|
|
|
|
result.realtimeStressPoints.isEmpty &&
|
|
|
|
result.sleepResults.isEmpty) {
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterNotificationNotBuilt',
|
|
|
|
'reason': 'empty_calculation_result',
|
|
|
|
'user_id': result.userId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -417,6 +456,15 @@ class HealthRawDataCoreService { |
|
|
|
latestRealtimeStressPoint.isWorkout ||
|
|
|
|
latestRealtimeStressPoint.isWorkoutRecovery;
|
|
|
|
if (shouldResetRealtimeStressPushTime) {
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterRealtimeStressSuppressedBeforeBuild',
|
|
|
|
'reason': 'workout_or_recovery',
|
|
|
|
'user_id': result.userId,
|
|
|
|
'latest_realtime':
|
|
|
|
_realtimeStressPointPayload(latestRealtimeStressPoint),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
await _recordRealtimeStressTimeSafely(
|
|
|
|
userId: result.userId,
|
|
|
|
recordTime: latestRealtimeStressPoint.rawEndTime,
|
|
...
|
...
|
@@ -433,20 +481,72 @@ class HealthRawDataCoreService { |
|
|
|
}
|
|
|
|
|
|
|
|
final record = await _readLocalNotificationRecordSafely(result.userId);
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterNotificationRecordRead',
|
|
|
|
'user_id': result.userId,
|
|
|
|
'record': _localNotificationRecordPayload(record),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterNotificationBuildDecisions',
|
|
|
|
'user_id': result.userId,
|
|
|
|
'decisions': _localNotificationDecisionPayloads(
|
|
|
|
result: notificationResult,
|
|
|
|
hasExistingHrv: hasExistingHrv,
|
|
|
|
realtimeWindow: realtimeWindow,
|
|
|
|
record: record,
|
|
|
|
),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
final notifications = HealthRawLocalNotificationBuilder(l10n).build(
|
|
|
|
result: notificationResult,
|
|
|
|
hasExistingHrv: hasExistingHrv,
|
|
|
|
realtimeWindow: realtimeWindow,
|
|
|
|
record: record,
|
|
|
|
);
|
|
|
|
if (notifications.isEmpty) return;
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterNotificationBuildFinished',
|
|
|
|
'user_id': result.userId,
|
|
|
|
'notification_count': notifications.length,
|
|
|
|
'notifications': notifications.map(_localNotificationPayload).toList(),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if (notifications.isEmpty) {
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterNotificationNotSent',
|
|
|
|
'reason': 'no_built_notifications',
|
|
|
|
'user_id': result.userId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
final sentNotifications = <HealthRawLocalNotification>[];
|
|
|
|
for (final notification in notifications) {
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterNotificationSendStart',
|
|
|
|
'user_id': result.userId,
|
|
|
|
'notification': _localNotificationPayload(notification),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
final sent = await _sendLocalNotificationSafely(
|
|
|
|
userId: result.userId,
|
|
|
|
notification: notification,
|
|
|
|
);
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterNotificationSendFinished',
|
|
|
|
'user_id': result.userId,
|
|
|
|
'sent': sent,
|
|
|
|
'reason': sent ? null : 'native_or_dispatcher_returned_false',
|
|
|
|
'notification': _localNotificationPayload(notification),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if (sent) {
|
|
|
|
sentNotifications.add(notification);
|
|
|
|
}
|
|
...
|
...
|
@@ -485,17 +585,38 @@ class HealthRawDataCoreService { |
|
|
|
required int latestRawEndTime,
|
|
|
|
}) async {
|
|
|
|
try {
|
|
|
|
return await _localStore.queryRealtimeStressPoints(
|
|
|
|
final points = await _localStore.queryRealtimeStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
startTime: latestRawEndTime - Duration.secondsPerHour + 1,
|
|
|
|
endTime: latestRawEndTime,
|
|
|
|
);
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterRealtimeStressWindowQueried',
|
|
|
|
'user_id': userId,
|
|
|
|
'latest_raw_end_time': latestRawEndTime,
|
|
|
|
'window_start_time': latestRawEndTime - Duration.secondsPerHour + 1,
|
|
|
|
'window_count': points.length,
|
|
|
|
'valid_window_count':
|
|
|
|
points.where((e) => e.result >= 1 && e.result <= 100).length,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
return points;
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError(
|
|
|
|
'query realtime stress notification window failed',
|
|
|
|
error,
|
|
|
|
stackTrace,
|
|
|
|
);
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterRealtimeStressWindowQueryFailed',
|
|
|
|
'user_id': userId,
|
|
|
|
'latest_raw_end_time': latestRawEndTime,
|
|
|
|
'error': error.toString(),
|
|
|
|
'stack_trace': stackTrace.toString(),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
return const <HealthRawRealtimeStressPoint>[];
|
|
|
|
}
|
|
|
|
}
|
|
...
|
...
|
@@ -511,10 +632,285 @@ class HealthRawDataCoreService { |
|
|
|
);
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError('send local health notification failed', error, stackTrace);
|
|
|
|
await _saveLocalNotificationDebugEvent(
|
|
|
|
<String, Object?>{
|
|
|
|
'event': 'flutterNotificationSendError',
|
|
|
|
'user_id': userId,
|
|
|
|
'notification': _localNotificationPayload(notification),
|
|
|
|
'error': error.toString(),
|
|
|
|
'stack_trace': stackTrace.toString(),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Map<String, Object?>> _localNotificationDecisionPayloads({
|
|
|
|
required HealthRawStressCalculationResult result,
|
|
|
|
required bool hasExistingHrv,
|
|
|
|
required List<HealthRawRealtimeStressPoint> realtimeWindow,
|
|
|
|
required HealthRawLocalNotificationRecord record,
|
|
|
|
}) {
|
|
|
|
return <Map<String, Object?>>[
|
|
|
|
_sleepNotificationDecisionPayload(result.sleepResults),
|
|
|
|
_hrvNotificationDecisionPayload(
|
|
|
|
result.hrvStressPoints,
|
|
|
|
hasExistingHrv: hasExistingHrv,
|
|
|
|
record: record,
|
|
|
|
),
|
|
|
|
_realtimeStressNotificationDecisionPayload(
|
|
|
|
result.realtimeStressPoints,
|
|
|
|
realtimeWindow: realtimeWindow,
|
|
|
|
record: record,
|
|
|
|
),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _sleepNotificationDecisionPayload(
|
|
|
|
List<HealthRawSleepResult> sleepResults,
|
|
|
|
) {
|
|
|
|
if (sleepResults.isEmpty) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.sleep.name,
|
|
|
|
'will_build': false,
|
|
|
|
'reason': 'no_sleep_results',
|
|
|
|
};
|
|
|
|
}
|
|
|
|
final sorted = [...sleepResults]..sort((a, b) => a.date.compareTo(b.date));
|
|
|
|
final latest = sorted.last;
|
|
|
|
final invalidState = !<int>{1, 2, 3}.contains(latest.sleepState);
|
|
|
|
if (invalidState) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.sleep.name,
|
|
|
|
'will_build': false,
|
|
|
|
'reason': 'invalid_sleep_state',
|
|
|
|
'latest_sleep': _sleepResultPayload(latest),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (latest.sleepMinutes <= 0) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.sleep.name,
|
|
|
|
'will_build': false,
|
|
|
|
'reason': 'invalid_sleep_minutes',
|
|
|
|
'latest_sleep': _sleepResultPayload(latest),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.sleep.name,
|
|
|
|
'will_build': true,
|
|
|
|
'reason': 'candidate',
|
|
|
|
'latest_sleep': _sleepResultPayload(latest),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _hrvNotificationDecisionPayload(
|
|
|
|
List<HealthRawHrvStressPoint> hrvPoints, {
|
|
|
|
required bool hasExistingHrv,
|
|
|
|
required HealthRawLocalNotificationRecord record,
|
|
|
|
}) {
|
|
|
|
if (!hasExistingHrv) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.hrv.name,
|
|
|
|
'will_build': false,
|
|
|
|
'reason': 'first_calculation_no_existing_hrv',
|
|
|
|
'has_existing_hrv': hasExistingHrv,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (hrvPoints.isEmpty) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.hrv.name,
|
|
|
|
'will_build': false,
|
|
|
|
'reason': 'no_new_hrv_points',
|
|
|
|
'has_existing_hrv': hasExistingHrv,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
final sorted = [...hrvPoints]
|
|
|
|
..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
|
|
|
|
final latest = sorted.last;
|
|
|
|
if (record.lastHrvTime == latest.rawEndTime) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.hrv.name,
|
|
|
|
'will_build': false,
|
|
|
|
'reason': 'duplicate_hrv_record_time',
|
|
|
|
'last_hrv_time': record.lastHrvTime,
|
|
|
|
'latest_hrv': _hrvStressPointPayload(latest),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.hrv.name,
|
|
|
|
'will_build': true,
|
|
|
|
'reason': 'candidate',
|
|
|
|
'last_hrv_time': record.lastHrvTime,
|
|
|
|
'latest_hrv': _hrvStressPointPayload(latest),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _realtimeStressNotificationDecisionPayload(
|
|
|
|
List<HealthRawRealtimeStressPoint> realtimePoints, {
|
|
|
|
required List<HealthRawRealtimeStressPoint> realtimeWindow,
|
|
|
|
required HealthRawLocalNotificationRecord record,
|
|
|
|
}) {
|
|
|
|
if (realtimePoints.isEmpty) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
'will_build': false,
|
|
|
|
'reason': 'no_realtime_points',
|
|
|
|
};
|
|
|
|
}
|
|
|
|
final latest = realtimePoints.reduce(
|
|
|
|
(a, b) => a.rawEndTime >= b.rawEndTime ? a : b,
|
|
|
|
);
|
|
|
|
if (latest.isWorkout || latest.isWorkoutRecovery) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
'will_build': false,
|
|
|
|
'reason': 'workout_or_recovery',
|
|
|
|
'latest_realtime': _realtimeStressPointPayload(latest),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
final valid = realtimeWindow
|
|
|
|
.where((e) => e.result >= 1 && e.result <= 100)
|
|
|
|
.toList()
|
|
|
|
..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
|
|
|
|
if (valid.length < 10) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
'will_build': false,
|
|
|
|
'reason': 'insufficient_valid_realtime_points',
|
|
|
|
'valid_window_count': valid.length,
|
|
|
|
'latest_realtime': _realtimeStressPointPayload(latest),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (latest.isSleepLikely) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
'will_build': false,
|
|
|
|
'reason': 'sleep_likely',
|
|
|
|
'latest_realtime': _realtimeStressPointPayload(latest),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
final lastPushTime = record.lastRealtimeStressTime;
|
|
|
|
if (lastPushTime != null &&
|
|
|
|
latest.rawEndTime - lastPushTime < Duration.secondsPerHour) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
'will_build': false,
|
|
|
|
'reason': 'within_realtime_interval',
|
|
|
|
'last_realtime_stress_time': lastPushTime,
|
|
|
|
'latest_realtime': _realtimeStressPointPayload(latest),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
|
|
|
|
'will_build': true,
|
|
|
|
'reason': 'candidate',
|
|
|
|
'last_realtime_stress_time': lastPushTime,
|
|
|
|
'valid_window_count': valid.length,
|
|
|
|
'latest_realtime': _realtimeStressPointPayload(latest),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _localNotificationPayload(
|
|
|
|
HealthRawLocalNotification notification,
|
|
|
|
) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'type': notification.recordType.name,
|
|
|
|
'record_time': notification.recordTime,
|
|
|
|
'title': notification.title,
|
|
|
|
'content': notification.content,
|
|
|
|
'link': notification.link,
|
|
|
|
'current_state': notification.currentState,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _localNotificationRecordPayload(
|
|
|
|
HealthRawLocalNotificationRecord record,
|
|
|
|
) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'last_sleep_time': record.lastSleepTime,
|
|
|
|
'last_hrv_time': record.lastHrvTime,
|
|
|
|
'last_realtime_stress_time': record.lastRealtimeStressTime,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?>? _latestHrvStressPointPayload(
|
|
|
|
List<HealthRawHrvStressPoint> points,
|
|
|
|
) {
|
|
|
|
if (points.isEmpty) return null;
|
|
|
|
return _hrvStressPointPayload(
|
|
|
|
points.reduce((a, b) => a.rawEndTime >= b.rawEndTime ? a : b),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _hrvStressPointPayload(HealthRawHrvStressPoint point) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'raw_end_time': point.rawEndTime,
|
|
|
|
'raw_hrv': point.rawHrv,
|
|
|
|
'result': point.result,
|
|
|
|
'state': point.state.name,
|
|
|
|
'state_value': point.state.value,
|
|
|
|
'baseline_hrv': point.baselineHrv,
|
|
|
|
'baseline_awake_hrv': point.baselineAwakeHrv,
|
|
|
|
'baseline_sleep_hrv': point.baselineSleepHrv,
|
|
|
|
'uploaded': point.uploaded,
|
|
|
|
'flags': _pointFlagsPayload(point.flags),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?>? _latestRealtimeStressPointPayload(
|
|
|
|
List<HealthRawRealtimeStressPoint> points,
|
|
|
|
) {
|
|
|
|
if (points.isEmpty) return null;
|
|
|
|
return _realtimeStressPointPayload(
|
|
|
|
points.reduce((a, b) => a.rawEndTime >= b.rawEndTime ? a : b),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _realtimeStressPointPayload(
|
|
|
|
HealthRawRealtimeStressPoint point,
|
|
|
|
) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'raw_end_time': point.rawEndTime,
|
|
|
|
'raw_hr': point.rawHr,
|
|
|
|
'result': point.result,
|
|
|
|
'state': point.state.name,
|
|
|
|
'state_value': point.state.value,
|
|
|
|
'uploaded': point.uploaded,
|
|
|
|
'flags': _pointFlagsPayload(point.flags),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?>? _latestSleepResultPayload(
|
|
|
|
List<HealthRawSleepResult> sleepResults,
|
|
|
|
) {
|
|
|
|
if (sleepResults.isEmpty) return null;
|
|
|
|
final sorted = [...sleepResults]..sort((a, b) => a.date.compareTo(b.date));
|
|
|
|
return _sleepResultPayload(sorted.last);
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _sleepResultPayload(HealthRawSleepResult result) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'date': result.date,
|
|
|
|
'start_date': result.startDate,
|
|
|
|
'sleep_score': result.sleepScore,
|
|
|
|
'sleep_state': result.sleepState,
|
|
|
|
'in_bed_minutes': result.inBedMinutes,
|
|
|
|
'awak_minutes': result.awakMinutes,
|
|
|
|
'sleep_minutes': result.sleepMinutes,
|
|
|
|
'uploaded': result.uploaded,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _pointFlagsPayload(HealthRawPointFlags flags) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'is_workout': flags.isWorkout,
|
|
|
|
'is_workout_recovery': flags.isWorkoutRecovery,
|
|
|
|
'is_sleep_likely': flags.isSleepLikely,
|
|
|
|
'is_suspected_activity': flags.isSuspectedActivity,
|
|
|
|
'context': flags.context.name,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
void _scheduleRealtimeStressServerPush(
|
|
|
|
Iterable<HealthRawLocalNotification> sentNotifications,
|
|
|
|
) {
|
|
...
|
...
|
@@ -938,6 +1334,25 @@ class HealthRawDataCoreService { |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _saveLocalNotificationDebugEvent(
|
|
|
|
Map<String, Object?> event,
|
|
|
|
) async {
|
|
|
|
if (!_isDebug) return;
|
|
|
|
try {
|
|
|
|
await _localNotificationDebugStore.append(event);
|
|
|
|
await _saveNativeLocalNotificationDebugEvent(event);
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError(
|
|
|
|
'save local notification debug event failed', error, stackTrace);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> _saveNativeLocalNotificationDebugEvent(
|
|
|
|
Map<String, Object?> event,
|
|
|
|
) async {
|
|
|
|
return _rawDataApi.saveLocalNotificationDebugEvent(event: event);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<File> _uploadApiLogFile() async {
|
|
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
|
|
return File('${dir.path}/health_raw_upload_debug.log');
|
...
|
...
|
|