|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
|
|
|
import '../../../logging/app_logger.dart';
|
|
|
|
import '../../../network/api/health_api.dart';
|
|
|
|
import '../../../result/app_result.dart';
|
|
|
|
import '../health_raw_models.dart';
|
|
|
|
import '../platform_ios/apple_health_raw_data_core_service.dart';
|
|
|
|
|
|
|
|
class OhosHealthRawResultUploadService {
|
|
|
|
OhosHealthRawResultUploadService({
|
|
|
|
required HealthApi healthApi,
|
|
|
|
required HealthRawStressLocalStore localStore,
|
|
|
|
int batchSize = 200,
|
|
|
|
}) : _healthApi = healthApi,
|
|
|
|
_localStore = localStore,
|
|
|
|
_batchSize = batchSize;
|
|
|
|
|
|
|
|
static const logMarker = '[OHOS_HEALTH_RESULT_UPLOAD]';
|
|
|
|
|
|
|
|
final HealthApi _healthApi;
|
|
|
|
final HealthRawStressLocalStore _localStore;
|
|
|
|
final int _batchSize;
|
|
|
|
Future<OhosHealthRawResultUploadSummary>? _runningUpload;
|
|
|
|
|
|
|
|
Future<OhosHealthRawResultUploadSummary> uploadPendingResults({
|
|
|
|
required int userId,
|
|
|
|
}) async {
|
|
|
|
final running = _runningUpload;
|
|
|
|
if (running != null) {
|
|
|
|
_log('upload_duplicate_join userId=$userId');
|
|
|
|
return running;
|
|
|
|
}
|
|
|
|
final task = _uploadPendingResults(userId: userId);
|
|
|
|
_runningUpload = task;
|
|
|
|
try {
|
|
|
|
return await task;
|
|
|
|
} finally {
|
|
|
|
if (identical(_runningUpload, task)) {
|
|
|
|
_runningUpload = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<OhosHealthRawResultUploadSummary> _uploadPendingResults({
|
|
|
|
required int userId,
|
|
|
|
}) async {
|
|
|
|
_log('upload_start userId=$userId');
|
|
|
|
final summary = OhosHealthRawResultUploadSummary();
|
|
|
|
|
|
|
|
await _uploadKind(
|
|
|
|
kind: 'hrv',
|
|
|
|
summary: summary,
|
|
|
|
query: () => _localStore.queryPendingHrvStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
limit: _batchSize,
|
|
|
|
),
|
|
|
|
toBody: (points) => points.map(_hrvBody).toList(growable: false),
|
|
|
|
upload: _healthApi.uploadV2HrvTrendResults,
|
|
|
|
markUploadedUntil: (points) => _localStore.markHrvStressUploadedUntil(
|
|
|
|
userId: userId,
|
|
|
|
rawEndTime: points.last.rawEndTime,
|
|
|
|
),
|
|
|
|
latestTime: (points) => points.last.rawEndTime,
|
|
|
|
);
|
|
|
|
await _uploadKind(
|
|
|
|
kind: 'realtimeStress',
|
|
|
|
summary: summary,
|
|
|
|
query: () => _localStore.queryPendingRealtimeStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
limit: _batchSize,
|
|
|
|
),
|
|
|
|
toBody: (points) => points.map(_realtimeBody).toList(growable: false),
|
|
|
|
upload: _healthApi.uploadV2RealtimeStressResults,
|
|
|
|
markUploadedUntil: (points) =>
|
|
|
|
_localStore.markRealtimeStressUploadedUntil(
|
|
|
|
userId: userId,
|
|
|
|
rawEndTime: points.last.rawEndTime,
|
|
|
|
),
|
|
|
|
latestTime: (points) => points.last.rawEndTime,
|
|
|
|
);
|
|
|
|
await _uploadKind(
|
|
|
|
kind: 'dailyStress',
|
|
|
|
summary: summary,
|
|
|
|
query: () => _localStore.queryPendingDailyStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
limit: _batchSize,
|
|
|
|
),
|
|
|
|
toBody: (points) => points.map(_dailyStressBody).toList(growable: false),
|
|
|
|
upload: _healthApi.uploadV2DailyStressResults,
|
|
|
|
markUploadedUntil: (points) => _localStore.markDailyStressUploadedUntil(
|
|
|
|
userId: userId,
|
|
|
|
date: points.last.date,
|
|
|
|
),
|
|
|
|
latestTime: (points) => points.last.date,
|
|
|
|
);
|
|
|
|
await _uploadKind(
|
|
|
|
kind: 'sleepScore',
|
|
|
|
summary: summary,
|
|
|
|
query: () => _localStore.queryPendingSleepResults(
|
|
|
|
userId: userId,
|
|
|
|
limit: _batchSize,
|
|
|
|
),
|
|
|
|
toBody: (points) => points.map(_sleepBody).toList(growable: false),
|
|
|
|
upload: _healthApi.uploadV2SleepScoreResults,
|
|
|
|
markUploadedUntil: (points) => _localStore.markSleepResultsUploadedUntil(
|
|
|
|
userId: userId,
|
|
|
|
date: points.last.date,
|
|
|
|
),
|
|
|
|
latestTime: (points) => points.last.date,
|
|
|
|
);
|
|
|
|
|
|
|
|
_log(
|
|
|
|
'upload_finish userId=$userId success=${summary.success} '
|
|
|
|
'hrv=${summary.hrvCount} realtime=${summary.realtimeStressCount} '
|
|
|
|
'daily=${summary.dailyStressCount} sleep=${summary.sleepScoreCount} '
|
|
|
|
'errors=${summary.errors.length}',
|
|
|
|
);
|
|
|
|
return summary;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _uploadKind<T>({
|
|
|
|
required String kind,
|
|
|
|
required OhosHealthRawResultUploadSummary summary,
|
|
|
|
required Future<List<T>> Function() query,
|
|
|
|
required List<Map<String, Object?>> Function(List<T>) toBody,
|
|
|
|
required Future<AppResult<void>> Function(List<Map<String, Object?>>)
|
|
|
|
upload,
|
|
|
|
required Future<void> Function(List<T>) markUploadedUntil,
|
|
|
|
required int Function(List<T>) latestTime,
|
|
|
|
}) async {
|
|
|
|
while (true) {
|
|
|
|
final points = await query();
|
|
|
|
if (points.isEmpty) {
|
|
|
|
_log('upload_skip kind=$kind reason=no_pending');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
final body = toBody(points);
|
|
|
|
final until = latestTime(points);
|
|
|
|
_log('upload_batch_start kind=$kind count=${points.length} until=$until');
|
|
|
|
final result = await upload(body);
|
|
|
|
switch (result) {
|
|
|
|
case AppSuccess():
|
|
|
|
await markUploadedUntil(points);
|
|
|
|
summary.add(kind, points.length);
|
|
|
|
_log(
|
|
|
|
'upload_batch_success kind=$kind count=${points.length} '
|
|
|
|
'until=$until',
|
|
|
|
);
|
|
|
|
case AppFailure(:final error):
|
|
|
|
summary.addError(kind, error.toString());
|
|
|
|
_log(
|
|
|
|
'upload_batch_failed kind=$kind count=${points.length} '
|
|
|
|
'until=$until error=$error',
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _hrvBody(HealthRawHrvStressPoint point) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'data_time': point.rawEndTime,
|
|
|
|
'raw_hrv': point.rawHrv,
|
|
|
|
'trend_hrv': point.result,
|
|
|
|
'state': point.state.value,
|
|
|
|
'hr_baseline': point.baselineRestingHr,
|
|
|
|
'hrv_baseline': point.baselineHrv,
|
|
|
|
'is_asleep': point.flags.isSleepLikely ? 1 : 0,
|
|
|
|
'is_workout': point.flags.isWorkout ? 1 : 0,
|
|
|
|
'is_workout_recovery': point.flags.isWorkoutRecovery ? 1 : 0,
|
|
|
|
'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _realtimeBody(HealthRawRealtimeStressPoint point) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'data_time': point.rawEndTime,
|
|
|
|
'hr_value': point.rawHr,
|
|
|
|
'stress_value': point.result,
|
|
|
|
'state': point.state.value,
|
|
|
|
'is_asleep': point.flags.isSleepLikely ? 1 : 0,
|
|
|
|
'is_workout': point.flags.isWorkout ? 1 : 0,
|
|
|
|
'is_workout_recovery': point.flags.isWorkoutRecovery ? 1 : 0,
|
|
|
|
'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _dailyStressBody(HealthRawDailyStressPoint point) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'stress_value': point.stressValue,
|
|
|
|
'stress_score': point.stressScore,
|
|
|
|
'state': point.state.value,
|
|
|
|
'data_time': point.dataTime,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _sleepBody(HealthRawSleepResult result) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'date': result.date,
|
|
|
|
'sleep_score': result.sleepScore,
|
|
|
|
'sleep_state': result.sleepState,
|
|
|
|
'in_bed_minutes': result.inBedMinutes,
|
|
|
|
'awak_minutes': result.awakMinutes,
|
|
|
|
'sleep_minutes': result.sleepMinutes,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
void _log(String message) {
|
|
|
|
final taggedMessage = '$logMarker $message';
|
|
|
|
debugPrint(taggedMessage);
|
|
|
|
try {
|
|
|
|
AppLogger.i(taggedMessage);
|
|
|
|
} catch (_) {
|
|
|
|
// AppLogger may be unavailable in early bootstrap/test contexts.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class OhosHealthRawResultUploadSummary {
|
|
|
|
int hrvCount = 0;
|
|
|
|
int realtimeStressCount = 0;
|
|
|
|
int dailyStressCount = 0;
|
|
|
|
int sleepScoreCount = 0;
|
|
|
|
final Map<String, String> errors = <String, String>{};
|
|
|
|
|
|
|
|
bool get success => errors.isEmpty;
|
|
|
|
|
|
|
|
void add(String kind, int count) {
|
|
|
|
switch (kind) {
|
|
|
|
case 'hrv':
|
|
|
|
hrvCount += count;
|
|
|
|
case 'realtimeStress':
|
|
|
|
realtimeStressCount += count;
|
|
|
|
case 'dailyStress':
|
|
|
|
dailyStressCount += count;
|
|
|
|
case 'sleepScore':
|
|
|
|
sleepScoreCount += count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void addError(String kind, String error) {
|
|
|
|
errors[kind] = error;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
String toString() {
|
|
|
|
return 'OhosHealthRawResultUploadSummary('
|
|
|
|
'success=$success, '
|
|
|
|
'hrvCount=$hrvCount, '
|
|
|
|
'realtimeStressCount=$realtimeStressCount, '
|
|
|
|
'dailyStressCount=$dailyStressCount, '
|
|
|
|
'sleepScoreCount=$sleepScoreCount, '
|
|
|
|
'errors=$errors'
|
|
|
|
')';
|
|
|
|
}
|
|
|
|
} |
...
|
...
|
|