|
...
|
...
|
@@ -9,6 +9,7 @@ import 'package:get/get.dart'; |
|
|
|
|
|
|
|
import '../../../../data/models/enums/app_enums.dart';
|
|
|
|
import '../../../../data/models/health/health_v2_models.dart';
|
|
|
|
import '../../../../l10n/l10n_extensions.dart';
|
|
|
|
import '../../../../pigeon/health_kit_raw_data_api.g.dart';
|
|
|
|
import '../../../config/app_environment_config.dart';
|
|
|
|
import '../../../logging/app_logger.dart';
|
|
...
|
...
|
@@ -19,6 +20,7 @@ import '../health_raw_data_source.dart'; |
|
|
|
import '../health_raw_models.dart';
|
|
|
|
import '../health_sleep_calculator.dart';
|
|
|
|
import '../platform_ios/apple_health_raw_data_core_service.dart';
|
|
|
|
import '../platform_ios/apple_health_raw_local_notification.dart';
|
|
|
|
import 'huawei_health_raw_stress_calculator.dart';
|
|
|
|
import 'ohos_health_raw_data_sync_service.dart';
|
|
|
|
import 'ohos_health_raw_result_upload_service.dart';
|
|
...
|
...
|
@@ -26,6 +28,9 @@ import 'ohos_health_raw_result_upload_service.dart'; |
|
|
|
class OHOSHealthRawDataCoreService {
|
|
|
|
static const int defaultLookbackDays = 183;
|
|
|
|
static const int defaultReadChunkDays = 7;
|
|
|
|
static const String _calculateLogMarker = '[OHOS_HEALTH_CALCULATE]';
|
|
|
|
static const String _dailyStressLogMarker = '[OHOS_DAILY_STRESS_CALC]';
|
|
|
|
static const String _sleepCalcLogMarker = '[OHOS_SLEEP_CALC]';
|
|
|
|
|
|
|
|
OHOSHealthRawDataCoreService({
|
|
|
|
HealthRawDataSource? rawDataSource,
|
|
...
|
...
|
@@ -37,6 +42,7 @@ class OHOSHealthRawDataCoreService { |
|
|
|
HarmonyApi? harmonyApi,
|
|
|
|
Future<bool> Function()? healthReadAuthorizationChecker,
|
|
|
|
OhosHealthRawResultUploadService? resultUploadService,
|
|
|
|
HealthRawLocalNotificationDispatcher? localNotificationDispatcher,
|
|
|
|
}) : _rawDataSource = rawDataSource ??
|
|
|
|
OhosHealthRawDataSource(
|
|
|
|
userIdProvider: userIdProvider,
|
|
...
|
...
|
@@ -51,7 +57,9 @@ class OHOSHealthRawDataCoreService { |
|
|
|
_harmonyApi = harmonyApi ??
|
|
|
|
(Get.isRegistered<HarmonyApi>() ? Get.find<HarmonyApi>() : null),
|
|
|
|
_healthReadAuthorizationChecker = healthReadAuthorizationChecker,
|
|
|
|
_resultUploadService = resultUploadService;
|
|
|
|
_resultUploadService = resultUploadService,
|
|
|
|
_localNotificationDispatcher = localNotificationDispatcher ??
|
|
|
|
HealthRawLocalNotificationDispatcher();
|
|
|
|
|
|
|
|
final HealthRawDataSource _rawDataSource;
|
|
|
|
final HealthRawStressLocalStore _localStore;
|
|
...
|
...
|
@@ -62,6 +70,7 @@ class OHOSHealthRawDataCoreService { |
|
|
|
final HarmonyApi? _harmonyApi;
|
|
|
|
final Future<bool> Function()? _healthReadAuthorizationChecker;
|
|
|
|
OhosHealthRawResultUploadService? _resultUploadService;
|
|
|
|
final HealthRawLocalNotificationDispatcher _localNotificationDispatcher;
|
|
|
|
final StreamController<HealthRawDataUpdatedEvent>
|
|
|
|
_healthDataUpdatedController =
|
|
|
|
StreamController<HealthRawDataUpdatedEvent>.broadcast();
|
|
...
|
...
|
@@ -222,159 +231,214 @@ class OHOSHealthRawDataCoreService { |
|
|
|
if (effectiveEndTime < requestedStartTime) {
|
|
|
|
throw ArgumentError.value(endTime, 'endTime');
|
|
|
|
}
|
|
|
|
final hasAuthorization = await _hasHealthReadAuthorization();
|
|
|
|
final syncResults = hasAuthorization
|
|
|
|
? await _syncCalculationRawDataIfNeeded(
|
|
|
|
startTime: requestedStartTime,
|
|
|
|
endTime: effectiveEndTime,
|
|
|
|
)
|
|
|
|
: const <OhosHealthRawDataSyncResult>[];
|
|
|
|
if (!hasAuthorization) {
|
|
|
|
_logInfo(
|
|
|
|
'skip OHOS raw data sync without health privacy authorization; '
|
|
|
|
'continue calculate and upload from local database',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
final hasAuthorization = await _hasHealthReadAuthorizationSafely();
|
|
|
|
final syncResults = await _syncCalculationRawDataSafely(
|
|
|
|
hasAuthorization: hasAuthorization,
|
|
|
|
startTime: requestedStartTime,
|
|
|
|
endTime: effectiveEndTime,
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'calculate_sync_finish startTime=$requestedStartTime '
|
|
|
|
'endTime=$effectiveEndTime syncResults=$syncResults',
|
|
|
|
);
|
|
|
|
|
|
|
|
final hrvContextStart = await _localStore.latestHrvSourceStartTime(userId);
|
|
|
|
final realtimeContextStart =
|
|
|
|
await _localStore.latestRealtimeSourceStartTime(userId);
|
|
|
|
final latestHrvRawEndTime = await _localStore.latestHrvRawEndTime(userId);
|
|
|
|
final latestRealtimeRawEndTime =
|
|
|
|
await _localStore.latestRealtimeRawEndTime(userId);
|
|
|
|
final latestSleepResultTime = await _localStore.latestSleepResultTime(
|
|
|
|
userId,
|
|
|
|
);
|
|
|
|
final hrvStartTime = math.max(
|
|
|
|
hrvContextStart ?? requestedStartTime,
|
|
|
|
earliestStartTime,
|
|
|
|
);
|
|
|
|
final realtimeStartTime = math.max(
|
|
|
|
realtimeContextStart ?? requestedStartTime,
|
|
|
|
earliestStartTime,
|
|
|
|
);
|
|
|
|
final heartRateStartTime = math.max(
|
|
|
|
_minNullable(hrvStartTime, realtimeStartTime) ?? requestedStartTime,
|
|
|
|
earliestStartTime,
|
|
|
|
);
|
|
|
|
final sleepStartTime = math.max(
|
|
|
|
latestSleepResultTime == null
|
|
|
|
? requestedStartTime
|
|
|
|
: latestSleepResultTime - Duration.secondsPerDay,
|
|
|
|
earliestStartTime,
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'calculate_context userId=$userId '
|
|
|
|
'requestedStartTime=$requestedStartTime endTime=$effectiveEndTime '
|
|
|
|
'hrvContextStart=$hrvContextStart '
|
|
|
|
'realtimeContextStart=$realtimeContextStart '
|
|
|
|
'latestHrvRawEndTime=$latestHrvRawEndTime '
|
|
|
|
'latestRealtimeRawEndTime=$latestRealtimeRawEndTime '
|
|
|
|
'latestSleepResultTime=$latestSleepResultTime '
|
|
|
|
'hrvStartTime=$hrvStartTime heartRateStartTime=$heartRateStartTime '
|
|
|
|
'sleepStartTime=$sleepStartTime',
|
|
|
|
);
|
|
|
|
|
|
|
|
final hrvPoints = await _fetchRawDataInChunks(
|
|
|
|
HealthDataUploadType.hrv.type,
|
|
|
|
hrvStartTime,
|
|
|
|
effectiveEndTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
final heartRatePoints = await _fetchRawDataInChunks(
|
|
|
|
HealthDataUploadType.heartRate.type,
|
|
|
|
heartRateStartTime,
|
|
|
|
effectiveEndTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
final restingHeartRatePoints = await _fetchRawDataInChunks(
|
|
|
|
HealthDataUploadType.restingHeartRate.type,
|
|
|
|
heartRateStartTime,
|
|
|
|
effectiveEndTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
final sleepIntervals = await _fetchSleepIntervalsInChunks(
|
|
|
|
sleepStartTime,
|
|
|
|
effectiveEndTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
final workoutIntervals = await _fetchWorkoutIntervalsInChunks(
|
|
|
|
heartRateStartTime,
|
|
|
|
effectiveEndTime,
|
|
|
|
final storedResult = await _calculateAndStoreSafely(
|
|
|
|
userId: userId,
|
|
|
|
requestedStartTime: requestedStartTime,
|
|
|
|
effectiveEndTime: effectiveEndTime,
|
|
|
|
earliestStartTime: earliestStartTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'calculate_raw_counts userId=$userId '
|
|
|
|
'hrv=${hrvPoints.length} heartRate=${heartRatePoints.length} '
|
|
|
|
'restingHeartRate=${restingHeartRatePoints.length} '
|
|
|
|
'sleepIntervals=${sleepIntervals.length} '
|
|
|
|
'workoutIntervals=${workoutIntervals.length}',
|
|
|
|
);
|
|
|
|
|
|
|
|
final result = await Isolate.run(
|
|
|
|
() => HuaweiHealthRawStressCalculator(userId: userId).calculate(
|
|
|
|
hrvPoints: hrvPoints,
|
|
|
|
heartRatePoints: heartRatePoints,
|
|
|
|
restingHeartRatePoints: restingHeartRatePoints,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
workoutIntervals: workoutIntervals,
|
|
|
|
startTime: math.min(hrvStartTime, heartRateStartTime),
|
|
|
|
endTime: effectiveEndTime,
|
|
|
|
),
|
|
|
|
debugName: 'OHOSHealthRawStressCalculator',
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'calculate_result_counts userId=$userId '
|
|
|
|
'hrv=${result.hrvStressPoints.length} '
|
|
|
|
'realtime=${result.realtimeStressPoints.length}',
|
|
|
|
);
|
|
|
|
final newResult = result.copyWith(
|
|
|
|
hrvStressPoints: _filterNewHrvStressPoints(
|
|
|
|
result.hrvStressPoints,
|
|
|
|
latestHrvRawEndTime,
|
|
|
|
),
|
|
|
|
realtimeStressPoints: _filterNewRealtimeStressPoints(
|
|
|
|
result.realtimeStressPoints,
|
|
|
|
latestRealtimeRawEndTime,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'calculate_new_result_counts userId=$userId '
|
|
|
|
'hrv=${newResult.hrvStressPoints.length} '
|
|
|
|
'realtime=${newResult.realtimeStressPoints.length}',
|
|
|
|
);
|
|
|
|
await _localStore.upsertResult(newResult);
|
|
|
|
_logInfo(
|
|
|
|
'calculate_result_stored userId=$userId '
|
|
|
|
'hrv=${newResult.hrvStressPoints.length} '
|
|
|
|
'realtime=${newResult.realtimeStressPoints.length}',
|
|
|
|
_scheduleResultUpload();
|
|
|
|
await _sendLocalNotificationsAfterCalculationSafely(
|
|
|
|
result: storedResult.result,
|
|
|
|
hasExistingHrv: storedResult.hasExistingHrv,
|
|
|
|
hasExistingSleep: storedResult.hasExistingSleep,
|
|
|
|
);
|
|
|
|
return storedResult.result;
|
|
|
|
}
|
|
|
|
|
|
|
|
final dailyStressPoints = await _calculateAndStoreDailyStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
realtimePoints: newResult.realtimeStressPoints,
|
|
|
|
nowSeconds: effectiveEndTime,
|
|
|
|
);
|
|
|
|
final sleepResults = await _calculateAndStoreSleepResults(
|
|
|
|
Future<_OhosStoredCalculationResult> _calculateAndStoreSafely({
|
|
|
|
required int userId,
|
|
|
|
required int requestedStartTime,
|
|
|
|
required int effectiveEndTime,
|
|
|
|
required int earliestStartTime,
|
|
|
|
required int readChunkDays,
|
|
|
|
}) async {
|
|
|
|
var hasExistingHrv = false;
|
|
|
|
var hasExistingSleep = false;
|
|
|
|
final emptyResult = HealthRawStressCalculationResult(
|
|
|
|
userId: userId,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
latestSleepResultTime: latestSleepResultTime,
|
|
|
|
hrvStressPoints: const <HealthRawHrvStressPoint>[],
|
|
|
|
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
|
|
|
|
dailyStressPoints: const <HealthRawDailyStressPoint>[],
|
|
|
|
sleepResults: const <HealthRawSleepResult>[],
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'calculate_daily_sleep_stored userId=$userId '
|
|
|
|
'daily=${dailyStressPoints.length} sleep=${sleepResults.length}',
|
|
|
|
);
|
|
|
|
if (_uploadResultsAfterCalculation) {
|
|
|
|
await _uploadResults();
|
|
|
|
try {
|
|
|
|
final hrvContextStart =
|
|
|
|
await _localStore.latestHrvSourceStartTime(userId);
|
|
|
|
final realtimeContextStart =
|
|
|
|
await _localStore.latestRealtimeSourceStartTime(userId);
|
|
|
|
final latestHrvRawEndTime = await _localStore.latestHrvRawEndTime(userId);
|
|
|
|
final latestRealtimeRawEndTime =
|
|
|
|
await _localStore.latestRealtimeRawEndTime(userId);
|
|
|
|
final latestSleepResultTime = await _localStore.latestSleepResultTime(
|
|
|
|
userId,
|
|
|
|
);
|
|
|
|
hasExistingHrv = latestHrvRawEndTime != null;
|
|
|
|
hasExistingSleep = latestSleepResultTime != null;
|
|
|
|
|
|
|
|
final hrvStartTime = math.max(
|
|
|
|
hrvContextStart ?? requestedStartTime,
|
|
|
|
earliestStartTime,
|
|
|
|
);
|
|
|
|
final realtimeStartTime = math.max(
|
|
|
|
realtimeContextStart ?? requestedStartTime,
|
|
|
|
earliestStartTime,
|
|
|
|
);
|
|
|
|
final heartRateStartTime = math.max(
|
|
|
|
_minNullable(hrvStartTime, realtimeStartTime) ?? requestedStartTime,
|
|
|
|
earliestStartTime,
|
|
|
|
);
|
|
|
|
final sleepStartTime = math.max(
|
|
|
|
latestSleepResultTime == null
|
|
|
|
? requestedStartTime
|
|
|
|
: latestSleepResultTime - Duration.secondsPerDay,
|
|
|
|
earliestStartTime,
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'calculate_context userId=$userId '
|
|
|
|
'requestedStartTime=$requestedStartTime endTime=$effectiveEndTime '
|
|
|
|
'hrvContextStart=$hrvContextStart '
|
|
|
|
'realtimeContextStart=$realtimeContextStart '
|
|
|
|
'latestHrvRawEndTime=$latestHrvRawEndTime '
|
|
|
|
'latestRealtimeRawEndTime=$latestRealtimeRawEndTime '
|
|
|
|
'latestSleepResultTime=$latestSleepResultTime '
|
|
|
|
'hrvStartTime=$hrvStartTime heartRateStartTime=$heartRateStartTime '
|
|
|
|
'sleepStartTime=$sleepStartTime',
|
|
|
|
);
|
|
|
|
|
|
|
|
final hrvPoints = await _fetchRawDataInChunks(
|
|
|
|
HealthDataUploadType.hrv.type,
|
|
|
|
hrvStartTime,
|
|
|
|
effectiveEndTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
final heartRatePoints = await _fetchRawDataInChunks(
|
|
|
|
HealthDataUploadType.heartRate.type,
|
|
|
|
heartRateStartTime,
|
|
|
|
effectiveEndTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
final restingHeartRatePoints = await _fetchRawDataInChunks(
|
|
|
|
HealthDataUploadType.restingHeartRate.type,
|
|
|
|
heartRateStartTime,
|
|
|
|
effectiveEndTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
final sleepIntervals = await _fetchSleepIntervalsInChunks(
|
|
|
|
sleepStartTime,
|
|
|
|
effectiveEndTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
final workoutIntervals = await _fetchWorkoutIntervalsInChunks(
|
|
|
|
heartRateStartTime,
|
|
|
|
effectiveEndTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
final latestRawHrvTime = _latestRawDataTime(hrvPoints);
|
|
|
|
final latestRawHrTime = _latestRawDataTime(heartRatePoints);
|
|
|
|
final latestRawSleepTime = _latestRawDataTime(sleepIntervals);
|
|
|
|
_logInfo(
|
|
|
|
'$_calculateLogMarker raw_counts userId=$userId '
|
|
|
|
'hrv=${hrvPoints.length} heartRate=${heartRatePoints.length} '
|
|
|
|
'restingHeartRate=${restingHeartRatePoints.length} '
|
|
|
|
'sleepIntervals=${sleepIntervals.length} '
|
|
|
|
'workoutIntervals=${workoutIntervals.length} '
|
|
|
|
'latestRawHrvTime=$latestRawHrvTime '
|
|
|
|
'latestRawHrTime=$latestRawHrTime '
|
|
|
|
'latestRawSleepTime=$latestRawSleepTime '
|
|
|
|
'needHrv=${_needsNewResult(latestRawHrvTime, latestHrvRawEndTime)} '
|
|
|
|
'needRealtime=${_needsNewResult(latestRawHrTime, latestRealtimeRawEndTime)} '
|
|
|
|
'needSleep=${_needsNewResult(latestRawSleepTime, latestSleepResultTime)}',
|
|
|
|
);
|
|
|
|
|
|
|
|
final result = await Isolate.run(
|
|
|
|
() => HuaweiHealthRawStressCalculator(userId: userId).calculate(
|
|
|
|
hrvPoints: hrvPoints,
|
|
|
|
heartRatePoints: heartRatePoints,
|
|
|
|
restingHeartRatePoints: restingHeartRatePoints,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
workoutIntervals: workoutIntervals,
|
|
|
|
startTime: math.min(hrvStartTime, heartRateStartTime),
|
|
|
|
endTime: effectiveEndTime,
|
|
|
|
),
|
|
|
|
debugName: 'OHOSHealthRawStressCalculator',
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'$_calculateLogMarker result_counts userId=$userId '
|
|
|
|
'hrv=${result.hrvStressPoints.length} '
|
|
|
|
'realtime=${result.realtimeStressPoints.length} '
|
|
|
|
'daily=${result.dailyStressPoints.length}',
|
|
|
|
);
|
|
|
|
final newResult = result.copyWith(
|
|
|
|
hrvStressPoints: _filterNewHrvStressPoints(
|
|
|
|
result.hrvStressPoints,
|
|
|
|
latestHrvRawEndTime,
|
|
|
|
),
|
|
|
|
realtimeStressPoints: _filterNewRealtimeStressPoints(
|
|
|
|
result.realtimeStressPoints,
|
|
|
|
latestRealtimeRawEndTime,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'$_calculateLogMarker new_result_counts userId=$userId '
|
|
|
|
'latestHrvRawEndTime=$latestHrvRawEndTime '
|
|
|
|
'latestRealtimeRawEndTime=$latestRealtimeRawEndTime '
|
|
|
|
'hrv=${newResult.hrvStressPoints.length} '
|
|
|
|
'realtime=${newResult.realtimeStressPoints.length}',
|
|
|
|
);
|
|
|
|
await _localStore.upsertResult(newResult);
|
|
|
|
_logInfo(
|
|
|
|
'$_calculateLogMarker result_stored userId=$userId '
|
|
|
|
'hrv=${newResult.hrvStressPoints.length} '
|
|
|
|
'realtime=${newResult.realtimeStressPoints.length}',
|
|
|
|
);
|
|
|
|
|
|
|
|
final dailyStressPoints = await _calculateAndStoreDailyStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
realtimePoints: newResult.realtimeStressPoints,
|
|
|
|
nowSeconds: effectiveEndTime,
|
|
|
|
);
|
|
|
|
final sleepResults = await _calculateAndStoreSleepResults(
|
|
|
|
userId: userId,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
latestSleepResultTime: latestSleepResultTime,
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'calculate_daily_sleep_stored userId=$userId '
|
|
|
|
'daily=${dailyStressPoints.length} sleep=${sleepResults.length}',
|
|
|
|
);
|
|
|
|
return _OhosStoredCalculationResult(
|
|
|
|
result: newResult.copyWith(
|
|
|
|
dailyStressPoints: dailyStressPoints,
|
|
|
|
sleepResults: sleepResults,
|
|
|
|
),
|
|
|
|
hasExistingHrv: hasExistingHrv,
|
|
|
|
hasExistingSleep: hasExistingSleep,
|
|
|
|
);
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError(
|
|
|
|
'$_calculateLogMarker calculate_failed userId=$userId',
|
|
|
|
error,
|
|
|
|
stackTrace,
|
|
|
|
);
|
|
|
|
return _OhosStoredCalculationResult(
|
|
|
|
result: emptyResult,
|
|
|
|
hasExistingHrv: hasExistingHrv,
|
|
|
|
hasExistingSleep: hasExistingSleep,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return newResult.copyWith(
|
|
|
|
dailyStressPoints: dailyStressPoints,
|
|
|
|
sleepResults: sleepResults,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Stream<HealthKitRawDataPoint> streamRawData({
|
|
...
|
...
|
@@ -562,6 +626,267 @@ class OHOSHealthRawDataCoreService { |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _scheduleResultUpload() {
|
|
|
|
if (!_uploadResultsAfterCalculation) return;
|
|
|
|
_logInfo('upload results scheduled');
|
|
|
|
unawaited(
|
|
|
|
_uploadResults().catchError((Object error, StackTrace stackTrace) {
|
|
|
|
_logError('upload results async failed', error, stackTrace);
|
|
|
|
return false;
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _sendLocalNotificationsAfterCalculationSafely({
|
|
|
|
required HealthRawStressCalculationResult result,
|
|
|
|
required bool hasExistingHrv,
|
|
|
|
required bool hasExistingSleep,
|
|
|
|
}) async {
|
|
|
|
try {
|
|
|
|
await _sendLocalNotificationsAfterCalculation(
|
|
|
|
result: result,
|
|
|
|
hasExistingHrv: hasExistingHrv,
|
|
|
|
hasExistingSleep: hasExistingSleep,
|
|
|
|
);
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError('send local notifications failed', error, stackTrace);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _sendLocalNotificationsAfterCalculation({
|
|
|
|
required HealthRawStressCalculationResult result,
|
|
|
|
required bool hasExistingHrv,
|
|
|
|
required bool hasExistingSleep,
|
|
|
|
}) async {
|
|
|
|
_logInfo(
|
|
|
|
'local_notification_start userId=${result.userId} '
|
|
|
|
'hasExistingHrv=$hasExistingHrv hasExistingSleep=$hasExistingSleep '
|
|
|
|
'hrv=${result.hrvStressPoints.length} '
|
|
|
|
'realtime=${result.realtimeStressPoints.length} '
|
|
|
|
'sleep=${result.sleepResults.length}',
|
|
|
|
);
|
|
|
|
if (result.hrvStressPoints.isEmpty &&
|
|
|
|
result.realtimeStressPoints.isEmpty &&
|
|
|
|
result.sleepResults.isEmpty) {
|
|
|
|
_logInfo('local_notification_skip reason=empty_calculation_result');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
final latestRealtimeStressPoint = result.realtimeStressPoints.isEmpty
|
|
|
|
? null
|
|
|
|
: result.realtimeStressPoints.reduce(
|
|
|
|
(a, b) => a.rawEndTime >= b.rawEndTime ? a : b,
|
|
|
|
);
|
|
|
|
var notificationResult = result;
|
|
|
|
var realtimeWindow = const <HealthRawRealtimeStressPoint>[];
|
|
|
|
var realtimeSleepIntervals = const <HealthRawSleepInterval>[];
|
|
|
|
if (latestRealtimeStressPoint != null) {
|
|
|
|
if (latestRealtimeStressPoint.isWorkout ||
|
|
|
|
latestRealtimeStressPoint.isWorkoutRecovery) {
|
|
|
|
_logInfo(
|
|
|
|
'local_notification_realtime_suppressed_before_build '
|
|
|
|
'reason=workout_or_recovery '
|
|
|
|
'rawEndTime=${latestRealtimeStressPoint.rawEndTime}',
|
|
|
|
);
|
|
|
|
await _recordRealtimeStressTimeSafely(
|
|
|
|
userId: result.userId,
|
|
|
|
recordTime: latestRealtimeStressPoint.rawEndTime,
|
|
|
|
);
|
|
|
|
notificationResult = result.copyWith(
|
|
|
|
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
realtimeWindow = await _queryRealtimeStressNotificationWindowSafely(
|
|
|
|
userId: result.userId,
|
|
|
|
latestRawEndTime: latestRealtimeStressPoint.rawEndTime,
|
|
|
|
);
|
|
|
|
realtimeSleepIntervals = await _queryRealtimeStressSleepIntervalsSafely(
|
|
|
|
userId: result.userId,
|
|
|
|
latestRawEndTime: latestRealtimeStressPoint.rawEndTime,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
final record = await _readLocalNotificationRecordSafely(result.userId);
|
|
|
|
final notifications = HealthRawLocalNotificationBuilder(l10n).build(
|
|
|
|
result: notificationResult,
|
|
|
|
hasExistingHrv: hasExistingHrv,
|
|
|
|
hasExistingSleep: hasExistingSleep,
|
|
|
|
realtimeWindow: realtimeWindow,
|
|
|
|
realtimeSleepIntervals: realtimeSleepIntervals,
|
|
|
|
lastRealtimeStressPushSendTime: record.lastRealtimeStressTime,
|
|
|
|
record: record,
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'local_notification_built userId=${result.userId} '
|
|
|
|
'count=${notifications.length} '
|
|
|
|
'types=${notifications.map((e) => e.recordType.name).join(',')}',
|
|
|
|
);
|
|
|
|
if (notifications.isEmpty) return;
|
|
|
|
|
|
|
|
final sentNotifications = <HealthRawLocalNotification>[];
|
|
|
|
for (final notification in notifications) {
|
|
|
|
final sent = await _sendLocalNotificationSafely(
|
|
|
|
userId: result.userId,
|
|
|
|
notification: notification,
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'local_notification_send_finished userId=${result.userId} '
|
|
|
|
'sent=$sent type=${notification.recordType.name} '
|
|
|
|
'recordTime=${notification.recordTime}',
|
|
|
|
);
|
|
|
|
if (sent) {
|
|
|
|
sentNotifications.add(notification);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_scheduleRealtimeStressServerPush(sentNotifications);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<HealthRawLocalNotificationRecord> _readLocalNotificationRecordSafely(
|
|
|
|
int userId,
|
|
|
|
) async {
|
|
|
|
try {
|
|
|
|
return await _localNotificationDispatcher.readRecord(userId);
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError('read local notification record failed', error, stackTrace);
|
|
|
|
return const HealthRawLocalNotificationRecord();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _recordRealtimeStressTimeSafely({
|
|
|
|
required int userId,
|
|
|
|
required int recordTime,
|
|
|
|
}) async {
|
|
|
|
try {
|
|
|
|
await _localNotificationDispatcher.recordRealtimeStressTime(
|
|
|
|
userId: userId,
|
|
|
|
recordTime: recordTime,
|
|
|
|
);
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError('record realtime stress push time failed', error, stackTrace);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthRawRealtimeStressPoint>>
|
|
|
|
_queryRealtimeStressNotificationWindowSafely({
|
|
|
|
required int userId,
|
|
|
|
required int latestRawEndTime,
|
|
|
|
}) async {
|
|
|
|
try {
|
|
|
|
final points = await _localStore.queryRealtimeStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
startTime: latestRawEndTime - Duration.secondsPerHour + 1,
|
|
|
|
endTime: latestRawEndTime,
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'local_notification_realtime_window userId=$userId '
|
|
|
|
'latestRawEndTime=$latestRawEndTime count=${points.length} '
|
|
|
|
'valid=${points.where((e) => e.result >= 1 && e.result <= 100).length}',
|
|
|
|
);
|
|
|
|
return points;
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError(
|
|
|
|
'query realtime stress notification window failed',
|
|
|
|
error,
|
|
|
|
stackTrace,
|
|
|
|
);
|
|
|
|
return const <HealthRawRealtimeStressPoint>[];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthRawSleepInterval>>
|
|
|
|
_queryRealtimeStressSleepIntervalsSafely({
|
|
|
|
required int userId,
|
|
|
|
required int latestRawEndTime,
|
|
|
|
}) async {
|
|
|
|
try {
|
|
|
|
final (startTime, endTime) = _dayRangeFromDateKey(
|
|
|
|
_dateKeyFromUnixSeconds(latestRawEndTime),
|
|
|
|
);
|
|
|
|
final sleepResults = await _localStore.querySleepResults(
|
|
|
|
userId: userId,
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
);
|
|
|
|
final intervals = sleepResults
|
|
|
|
.where((result) => result.startDate <= result.date)
|
|
|
|
.map(
|
|
|
|
(result) => (
|
|
|
|
startTime: result.startDate,
|
|
|
|
endTime: result.date,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.toList(growable: false);
|
|
|
|
_logInfo(
|
|
|
|
'local_notification_realtime_sleep_intervals userId=$userId '
|
|
|
|
'latestRawEndTime=$latestRawEndTime count=${intervals.length}',
|
|
|
|
);
|
|
|
|
return intervals;
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError(
|
|
|
|
'query realtime stress sleep intervals failed',
|
|
|
|
error,
|
|
|
|
stackTrace,
|
|
|
|
);
|
|
|
|
return const <HealthRawSleepInterval>[];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> _sendLocalNotificationSafely({
|
|
|
|
required int userId,
|
|
|
|
required HealthRawLocalNotification notification,
|
|
|
|
}) async {
|
|
|
|
try {
|
|
|
|
return await _localNotificationDispatcher.sendOne(
|
|
|
|
userId: userId,
|
|
|
|
notification: notification,
|
|
|
|
);
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError('send local health notification failed', error, stackTrace);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _scheduleRealtimeStressServerPush(
|
|
|
|
Iterable<HealthRawLocalNotification> sentNotifications,
|
|
|
|
) {
|
|
|
|
final realtimeNotifications = sentNotifications.where(
|
|
|
|
(notification) =>
|
|
|
|
notification.recordType ==
|
|
|
|
HealthRawLocalNotificationRecordType.realtimeStress &&
|
|
|
|
notification.currentState != null,
|
|
|
|
);
|
|
|
|
for (final notification in realtimeNotifications) {
|
|
|
|
unawaited(_pushRealtimeStressServerNotification(notification));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _pushRealtimeStressServerNotification(
|
|
|
|
HealthRawLocalNotification notification,
|
|
|
|
) async {
|
|
|
|
final serverHealthApi = _serverHealthApi;
|
|
|
|
if (serverHealthApi == null) return;
|
|
|
|
try {
|
|
|
|
final result = await serverHealthApi.pushRealtimeStressNotification(
|
|
|
|
latestDataTime: notification.recordTime,
|
|
|
|
currentState: notification.currentState!,
|
|
|
|
);
|
|
|
|
if (result is AppFailure) {
|
|
|
|
_logError(
|
|
|
|
'push realtime stress server notification failed',
|
|
|
|
result.error,
|
|
|
|
StackTrace.current,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError(
|
|
|
|
'push realtime stress server notification failed',
|
|
|
|
error,
|
|
|
|
stackTrace,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<OhosHealthRawDataSyncResult>> _syncCalculationRawDataIfNeeded({
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
...
|
...
|
@@ -577,6 +902,47 @@ class OHOSHealthRawDataCoreService { |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<OhosHealthRawDataSyncResult>> _syncCalculationRawDataSafely({
|
|
|
|
required bool hasAuthorization,
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
}) async {
|
|
|
|
if (!hasAuthorization) {
|
|
|
|
_logInfo(
|
|
|
|
'$_calculateLogMarker sync_skipped reason=no_health_privacy_permission '
|
|
|
|
'startTime=$startTime endTime=$endTime',
|
|
|
|
);
|
|
|
|
return const <OhosHealthRawDataSyncResult>[];
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
return await _syncCalculationRawDataIfNeeded(
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
);
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError(
|
|
|
|
'$_calculateLogMarker sync_failed_continue '
|
|
|
|
'startTime=$startTime endTime=$endTime',
|
|
|
|
error,
|
|
|
|
stackTrace,
|
|
|
|
);
|
|
|
|
return const <OhosHealthRawDataSyncResult>[];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> _hasHealthReadAuthorizationSafely() async {
|
|
|
|
try {
|
|
|
|
return await _hasHealthReadAuthorization();
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError(
|
|
|
|
'$_calculateLogMarker privacy_authorization_failed_continue',
|
|
|
|
error,
|
|
|
|
stackTrace,
|
|
|
|
);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> _hasHealthReadAuthorization() async {
|
|
|
|
final checker = _healthReadAuthorizationChecker;
|
|
|
|
if (checker != null) {
|
|
...
|
...
|
@@ -594,6 +960,19 @@ class OHOSHealthRawDataCoreService { |
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
int? _latestRawDataTime(List<HealthKitRawDataPoint> points) {
|
|
|
|
if (points.isEmpty) return null;
|
|
|
|
return points
|
|
|
|
.map((point) => point.endTime)
|
|
|
|
.reduce((a, b) => a >= b ? a : b);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool _needsNewResult(int? latestRawTime, int? latestResultTime) {
|
|
|
|
if (latestRawTime == null) return false;
|
|
|
|
if (latestResultTime == null) return true;
|
|
|
|
return latestRawTime > latestResultTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool _logPrivacyAuthorizationFailure(Object error) {
|
|
|
|
_logInfo('OHOS health privacy authorization check failed: $error');
|
|
|
|
return false;
|
|
...
|
...
|
@@ -615,16 +994,31 @@ class OHOSHealthRawDataCoreService { |
|
|
|
userId: userId,
|
|
|
|
dates: affectedDates,
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'$_dailyStressLogMarker start userId=$userId nowSeconds=$nowSeconds '
|
|
|
|
'incomingRealtime=${realtimePoints.length} '
|
|
|
|
'affectedDates=$affectedDates existingDates=$existingDates',
|
|
|
|
);
|
|
|
|
final dailyStressPoints = <HealthRawDailyStressPoint>[];
|
|
|
|
final emptyDates = <int>[];
|
|
|
|
for (final date in affectedDates) {
|
|
|
|
if (date != todayDate && existingDates.contains(date)) continue;
|
|
|
|
if (date != todayDate && existingDates.contains(date)) {
|
|
|
|
_logInfo(
|
|
|
|
'$_dailyStressLogMarker skip_existing userId=$userId date=$date',
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
final (startTime, endTime) = _dayRangeFromDateKey(date);
|
|
|
|
final dayRealtimePoints = await _localStore.queryRealtimeStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'$_dailyStressLogMarker day_query userId=$userId date=$date '
|
|
|
|
'startTime=$startTime endTime=$endTime '
|
|
|
|
'dayRealtime=${dayRealtimePoints.length}',
|
|
|
|
);
|
|
|
|
final point = HuaweiHealthRawStressCalculator.calculateDailyStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
realtimePoints: dayRealtimePoints,
|
|
...
|
...
|
@@ -634,9 +1028,17 @@ class OHOSHealthRawDataCoreService { |
|
|
|
).firstOrNull;
|
|
|
|
if (point == null) {
|
|
|
|
emptyDates.add(date);
|
|
|
|
_logInfo(
|
|
|
|
'$_dailyStressLogMarker no_result userId=$userId date=$date',
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
dailyStressPoints.add(point);
|
|
|
|
_logInfo(
|
|
|
|
'$_dailyStressLogMarker result userId=$userId date=$date '
|
|
|
|
'stressValue=${point.stressValue} stressScore=${point.stressScore} '
|
|
|
|
'state=${point.state.value}',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
await _localStore.upsertDailyStressPoints(
|
|
|
|
userId: userId,
|
|
...
|
...
|
@@ -646,6 +1048,10 @@ class OHOSHealthRawDataCoreService { |
|
|
|
userId: userId,
|
|
|
|
dates: emptyDates,
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'$_dailyStressLogMarker stored userId=$userId '
|
|
|
|
'stored=${dailyStressPoints.length} deletedEmptyDates=$emptyDates',
|
|
|
|
);
|
|
|
|
return dailyStressPoints;
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -654,12 +1060,24 @@ class OHOSHealthRawDataCoreService { |
|
|
|
required List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
required int? latestSleepResultTime,
|
|
|
|
}) async {
|
|
|
|
if (sleepIntervals.isEmpty) return const <HealthRawSleepResult>[];
|
|
|
|
_logInfo(
|
|
|
|
'$_sleepCalcLogMarker start userId=$userId '
|
|
|
|
'sleepIntervals=${sleepIntervals.length} '
|
|
|
|
'latestSleepResultTime=$latestSleepResultTime',
|
|
|
|
);
|
|
|
|
if (sleepIntervals.isEmpty) {
|
|
|
|
_logInfo('$_sleepCalcLogMarker no_raw_sleep userId=$userId');
|
|
|
|
return const <HealthRawSleepResult>[];
|
|
|
|
}
|
|
|
|
final days = {
|
|
|
|
for (final interval in sleepIntervals)
|
|
|
|
DateTime.fromMillisecondsSinceEpoch(interval.endTime * 1000)
|
|
|
|
}.map((date) => DateTime(date.year, date.month, date.day)).toList()
|
|
|
|
..sort((a, b) => a.compareTo(b));
|
|
|
|
_logInfo(
|
|
|
|
'$_sleepCalcLogMarker days userId=$userId '
|
|
|
|
'days=${days.map(_dateKeyFromDateTime).toList()}',
|
|
|
|
);
|
|
|
|
final results = <HealthRawSleepResult>[];
|
|
|
|
for (final day in days) {
|
|
|
|
final calculation = HealthSleepCalculator.calculateDay(
|
|
...
|
...
|
@@ -669,10 +1087,30 @@ class OHOSHealthRawDataCoreService { |
|
|
|
final merged = calculation.mergeSleepTimeRange;
|
|
|
|
final score = calculation.score;
|
|
|
|
final state = calculation.state;
|
|
|
|
if (merged == null || score == null || state == null) continue;
|
|
|
|
if (!calculation.hasValidSleep) continue;
|
|
|
|
if (merged == null || score == null || state == null) {
|
|
|
|
_logInfo(
|
|
|
|
'$_sleepCalcLogMarker skip_invalid userId=$userId '
|
|
|
|
'date=${_dateKeyFromDateTime(day)} '
|
|
|
|
'reason=missing_score_or_state '
|
|
|
|
'score=$score state=${state?.value}',
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (!calculation.hasValidSleep) {
|
|
|
|
_logInfo(
|
|
|
|
'$_sleepCalcLogMarker skip_invalid userId=$userId '
|
|
|
|
'date=${_dateKeyFromDateTime(day)} reason=no_valid_sleep '
|
|
|
|
'durationSeconds=${calculation.durationSeconds}',
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (latestSleepResultTime != null &&
|
|
|
|
merged.endTime <= latestSleepResultTime) {
|
|
|
|
_logInfo(
|
|
|
|
'$_sleepCalcLogMarker skip_existing userId=$userId '
|
|
|
|
'date=${_dateKeyFromDateTime(day)} mergedEnd=${merged.endTime} '
|
|
|
|
'latestSleepResultTime=$latestSleepResultTime',
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
results.add(
|
|
...
|
...
|
@@ -688,8 +1126,17 @@ class OHOSHealthRawDataCoreService { |
|
|
|
uploaded: false,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
_logInfo(
|
|
|
|
'$_sleepCalcLogMarker result userId=$userId '
|
|
|
|
'date=${_dateKeyFromDateTime(day)} start=${merged.startTime} '
|
|
|
|
'end=${merged.endTime} score=$score state=${state.value} '
|
|
|
|
'sleepMinutes=${calculation.summary.sleepMinutes}',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
await _localStore.upsertSleepResults(userId: userId, results: results);
|
|
|
|
_logInfo(
|
|
|
|
'$_sleepCalcLogMarker stored userId=$userId stored=${results.length}',
|
|
|
|
);
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -774,6 +1221,10 @@ class OHOSHealthRawDataCoreService { |
|
|
|
|
|
|
|
static int _dateKeyFromUnixSeconds(int seconds) {
|
|
|
|
final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
|
|
|
|
return _dateKeyFromDateTime(date);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int _dateKeyFromDateTime(DateTime date) {
|
|
|
|
return date.year * 10000 + date.month * 100 + date.day;
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -819,3 +1270,15 @@ class OHOSHealthRawDataCoreService { |
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _OhosStoredCalculationResult {
|
|
|
|
const _OhosStoredCalculationResult({
|
|
|
|
required this.result,
|
|
|
|
required this.hasExistingHrv,
|
|
|
|
required this.hasExistingSleep,
|
|
|
|
});
|
|
|
|
|
|
|
|
final HealthRawStressCalculationResult result;
|
|
|
|
final bool hasExistingHrv;
|
|
|
|
final bool hasExistingSleep;
|
|
|
|
} |
...
|
...
|
|