|
|
|
import 'dart:async';
|
|
|
|
import 'dart:developer' as developer;
|
|
|
|
import 'dart:io';
|
|
|
|
import 'dart:isolate';
|
|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
|
|
|
import '../../../../data/models/enums/app_enums.dart';
|
|
|
|
import '../../../../pigeon/health_kit_raw_data_api.g.dart';
|
|
|
|
import '../../../config/app_environment_config.dart';
|
|
|
|
import '../../../logging/app_logger.dart';
|
|
|
|
import '../../../network/api/health_api.dart';
|
|
|
|
import '../health_raw_data_source.dart';
|
|
|
|
import '../health_raw_models.dart';
|
|
|
|
import '../health_raw_stress_calculator.dart';
|
|
|
|
import '../health_sleep_calculator.dart';
|
|
|
|
import '../platform_ios/apple_health_raw_data_core_service.dart';
|
|
|
|
|
|
|
|
class OHOSHealthRawDataCoreService {
|
|
|
|
static const int defaultLookbackDays = 183;
|
|
|
|
static const int defaultReadChunkDays = 7;
|
|
|
|
|
|
|
|
OHOSHealthRawDataCoreService({
|
|
|
|
HealthRawDataSource? rawDataSource,
|
|
|
|
HealthRawStressLocalStore? localStore,
|
|
|
|
AppEnvironmentConfig? environmentConfig,
|
|
|
|
int Function()? userIdProvider,
|
|
|
|
bool uploadResultsAfterCalculation = true,
|
|
|
|
HealthApi? serverHealthApi,
|
|
|
|
}) : _rawDataSource = rawDataSource ?? OhosHealthRawDataSource(),
|
|
|
|
_localStore = localStore ??
|
|
|
|
HealthRawStressLocalStore(databaseNamePrefix: 'ohos_'),
|
|
|
|
_environmentConfig = environmentConfig,
|
|
|
|
_userIdProvider = userIdProvider,
|
|
|
|
_uploadResultsAfterCalculation = uploadResultsAfterCalculation,
|
|
|
|
_serverHealthApi = serverHealthApi;
|
|
|
|
|
|
|
|
final HealthRawDataSource _rawDataSource;
|
|
|
|
final HealthRawStressLocalStore _localStore;
|
|
|
|
final AppEnvironmentConfig? _environmentConfig;
|
|
|
|
final int Function()? _userIdProvider;
|
|
|
|
final bool _uploadResultsAfterCalculation;
|
|
|
|
final HealthApi? _serverHealthApi;
|
|
|
|
final StreamController<HealthRawDataUpdatedEvent>
|
|
|
|
_healthDataUpdatedController =
|
|
|
|
StreamController<HealthRawDataUpdatedEvent>.broadcast();
|
|
|
|
Future<HealthRawStressCalculationResult>? _coreCalculation;
|
|
|
|
|
|
|
|
bool get _isDebug => _environmentConfig?.isDebug ?? false;
|
|
|
|
|
|
|
|
int get _userId {
|
|
|
|
final userId = _userIdProvider?.call() ?? 0;
|
|
|
|
if (userId <= 0) {
|
|
|
|
throw StateError('OHOSHealthRawDataCoreService requires a valid userId');
|
|
|
|
}
|
|
|
|
return userId;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> openDatabase() {
|
|
|
|
return _localStore.openDatabase(_userId);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> closeDatabase() {
|
|
|
|
return _localStore.closeDatabase(userId: _userId);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> hasHealthData() {
|
|
|
|
return _rawDataSource.hasHealthData();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> performHealthDataUpload() async {
|
|
|
|
// TODO: Trigger OHOS raw-data upload when the OHOS upload API is ready.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> syncDatabaseToNativeIfNeeded() async {
|
|
|
|
// OHOS must not call the Apple Pigeon database sync path.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Stream<HealthRawDataUpdatedEvent> get healthDataUpdatedStream =>
|
|
|
|
_healthDataUpdatedController.stream;
|
|
|
|
|
|
|
|
Future<void> onHealthDataUpdated({List<int>? dataTypes}) async {
|
|
|
|
await startCoreCaculate();
|
|
|
|
_healthDataUpdatedController.add(
|
|
|
|
HealthRawDataUpdatedEvent(dataTypes: dataTypes ?? const <int>[]),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<String> databaseFilePath() {
|
|
|
|
return _localStore.dbPath(_userId);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Uint8List> readDatabaseFileBytes() async {
|
|
|
|
final userId = _userId;
|
|
|
|
await _localStore.prepareDatabaseFileForShare(userId);
|
|
|
|
final path = await _localStore.dbPath(userId);
|
|
|
|
final file = File(path);
|
|
|
|
if (!await file.exists()) {
|
|
|
|
throw StateError('OHOSHealthRawDataCoreService database file not found');
|
|
|
|
}
|
|
|
|
return file.readAsBytes();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<String> readUploadApiLogText() async {
|
|
|
|
return _isDebug ? 'OHOS upload API log is not implemented yet.' : '';
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> clearLocalDatabaseAndUploadLog() {
|
|
|
|
return _localStore.clearTables(_userId);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> clearUploadApiLog() async {
|
|
|
|
// TODO: Clear OHOS upload log after the OHOS upload/debug store exists.
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> shareNativeAppleHealthObserverRecord() async {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> shareFlutterObserverRecord() async {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> shareUploadTaskRecord() async {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<String> readLocalNotificationDebugLogText() async {
|
|
|
|
return _isDebug
|
|
|
|
? 'OHOS notification debug log is not implemented yet.'
|
|
|
|
: '';
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> shareLocalNotificationDebugRecord() async {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<HealthRawStressCalculationResult> startCoreCaculate({
|
|
|
|
int? endTime,
|
|
|
|
int readChunkDays = defaultReadChunkDays,
|
|
|
|
}) async {
|
|
|
|
final running = _coreCalculation;
|
|
|
|
if (running != null) return running;
|
|
|
|
final task = _readCalculateAndStore(
|
|
|
|
endTime: endTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
forceStartTime: null,
|
|
|
|
);
|
|
|
|
_coreCalculation = task;
|
|
|
|
try {
|
|
|
|
return await task;
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError('startCoreCaculate failed', error, stackTrace);
|
|
|
|
rethrow;
|
|
|
|
} finally {
|
|
|
|
if (identical(_coreCalculation, task)) {
|
|
|
|
_coreCalculation = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<HealthRawStressCalculationResult> syncAndStore({
|
|
|
|
int? startTime,
|
|
|
|
int? endTime,
|
|
|
|
int readChunkDays = defaultReadChunkDays,
|
|
|
|
}) async {
|
|
|
|
try {
|
|
|
|
return await _readCalculateAndStore(
|
|
|
|
endTime: endTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
forceStartTime: startTime,
|
|
|
|
);
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError('syncAndStore failed', error, stackTrace);
|
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<HealthRawStressCalculationResult> _readCalculateAndStore({
|
|
|
|
required int? endTime,
|
|
|
|
required int readChunkDays,
|
|
|
|
required int? forceStartTime,
|
|
|
|
}) async {
|
|
|
|
if (readChunkDays <= 0) {
|
|
|
|
throw ArgumentError.value(readChunkDays, 'readChunkDays');
|
|
|
|
}
|
|
|
|
|
|
|
|
final userId = _userId;
|
|
|
|
await _localStore.ensureReadable(userId);
|
|
|
|
final effectiveEndTime =
|
|
|
|
endTime ?? DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
|
|
final earliestStartTime = DateTime.now()
|
|
|
|
.subtract(const Duration(days: defaultLookbackDays))
|
|
|
|
.millisecondsSinceEpoch ~/
|
|
|
|
1000;
|
|
|
|
final requestedStartTime =
|
|
|
|
math.max(forceStartTime ?? earliestStartTime, earliestStartTime);
|
|
|
|
if (effectiveEndTime < requestedStartTime) {
|
|
|
|
throw ArgumentError.value(endTime, 'endTime');
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
);
|
|
|
|
|
|
|
|
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 result = await Isolate.run(
|
|
|
|
() => HealthRawStressCalculator(userId: userId).calculate(
|
|
|
|
hrvPoints: hrvPoints,
|
|
|
|
heartRatePoints: heartRatePoints,
|
|
|
|
restingHeartRatePoints: restingHeartRatePoints,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
workoutIntervals: workoutIntervals,
|
|
|
|
startTime: math.min(hrvStartTime, heartRateStartTime),
|
|
|
|
endTime: effectiveEndTime,
|
|
|
|
),
|
|
|
|
debugName: 'OHOSHealthRawStressCalculator',
|
|
|
|
);
|
|
|
|
final newResult = result.copyWith(
|
|
|
|
hrvStressPoints: _filterNewHrvStressPoints(
|
|
|
|
result.hrvStressPoints,
|
|
|
|
latestHrvRawEndTime,
|
|
|
|
),
|
|
|
|
realtimeStressPoints: _filterNewRealtimeStressPoints(
|
|
|
|
result.realtimeStressPoints,
|
|
|
|
latestRealtimeRawEndTime,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
await _localStore.upsertResult(newResult);
|
|
|
|
|
|
|
|
final dailyStressPoints = await _calculateAndStoreDailyStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
realtimePoints: newResult.realtimeStressPoints,
|
|
|
|
nowSeconds: effectiveEndTime,
|
|
|
|
);
|
|
|
|
final sleepResults = await _calculateAndStoreSleepResults(
|
|
|
|
userId: userId,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
latestSleepResultTime: latestSleepResultTime,
|
|
|
|
);
|
|
|
|
if (_uploadResultsAfterCalculation) {
|
|
|
|
await _uploadResults();
|
|
|
|
}
|
|
|
|
return newResult.copyWith(
|
|
|
|
dailyStressPoints: dailyStressPoints,
|
|
|
|
sleepResults: sleepResults,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Stream<HealthKitRawDataPoint> streamRawData({
|
|
|
|
required int dataType,
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
int readChunkDays = defaultReadChunkDays,
|
|
|
|
}) async* {
|
|
|
|
final chunkSeconds = readChunkDays * Duration.secondsPerDay;
|
|
|
|
var cursor = startTime;
|
|
|
|
while (cursor <= endTime) {
|
|
|
|
final chunkEnd = math.min(cursor + chunkSeconds - 1, endTime);
|
|
|
|
final points = await _rawDataSource.getRawData(
|
|
|
|
dataType,
|
|
|
|
cursor,
|
|
|
|
chunkEnd,
|
|
|
|
);
|
|
|
|
for (final point in points) {
|
|
|
|
yield point;
|
|
|
|
}
|
|
|
|
cursor = chunkEnd + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Stream<HealthKitRawDataPoint> streamRawSleepData({
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
int readChunkDays = defaultReadChunkDays,
|
|
|
|
}) async* {
|
|
|
|
final chunkSeconds = readChunkDays * Duration.secondsPerDay;
|
|
|
|
var cursor = startTime;
|
|
|
|
while (cursor <= endTime) {
|
|
|
|
final chunkEnd = math.min(cursor + chunkSeconds - 1, endTime);
|
|
|
|
final groups = await _rawDataSource.getRawSleepData(cursor, chunkEnd);
|
|
|
|
final points = groups
|
|
|
|
.expand((group) => group.sleepDataPoints)
|
|
|
|
.where(
|
|
|
|
(point) => point.endTime >= cursor && point.startTime <= chunkEnd)
|
|
|
|
.toList();
|
|
|
|
for (final point in points) {
|
|
|
|
yield point;
|
|
|
|
}
|
|
|
|
cursor = chunkEnd + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Stream<HealthKitRawWorkoutDataPoint> streamRawWorkoutData({
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
int readChunkDays = defaultReadChunkDays,
|
|
|
|
}) async* {
|
|
|
|
final chunkSeconds = readChunkDays * Duration.secondsPerDay;
|
|
|
|
var cursor = startTime;
|
|
|
|
while (cursor <= endTime) {
|
|
|
|
final chunkEnd = math.min(cursor + chunkSeconds - 1, endTime);
|
|
|
|
final points = await _rawDataSource.getRawWorkoutData(cursor, chunkEnd);
|
|
|
|
for (final point in points.where(
|
|
|
|
(point) => point.endTime >= cursor && point.startTime <= chunkEnd)) {
|
|
|
|
yield point;
|
|
|
|
}
|
|
|
|
cursor = chunkEnd + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthRawHrvStressPoint>> queryHrvStressPoints({
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
}) {
|
|
|
|
return _localStore.queryHrvStressPoints(
|
|
|
|
userId: _userId,
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthRawRealtimeStressPoint>> queryRealtimeStressPoints({
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
}) {
|
|
|
|
return _localStore.queryRealtimeStressPoints(
|
|
|
|
userId: _userId,
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<int?> queryEarliestHrRawEndTime() {
|
|
|
|
return _localStore.earliestRealtimeRawEndTime(_userId);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthRawDailyStressPoint>> queryDailyStressPoints({
|
|
|
|
required int startDate,
|
|
|
|
required int endDate,
|
|
|
|
}) {
|
|
|
|
return _localStore.queryDailyStressPoints(
|
|
|
|
userId: _userId,
|
|
|
|
startDate: startDate,
|
|
|
|
endDate: endDate,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthRawSleepResult>> querySleepResults({
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
}) {
|
|
|
|
return _localStore.querySleepResults(
|
|
|
|
userId: _userId,
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthKitRawDataPoint>> queryRawDataPoints({
|
|
|
|
required int dataType,
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
int readChunkDays = defaultReadChunkDays,
|
|
|
|
}) {
|
|
|
|
return _fetchRawDataInChunks(
|
|
|
|
dataType,
|
|
|
|
startTime,
|
|
|
|
endTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthKitRawDataPoint>> queryRawSleepIntervals({
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
int readChunkDays = defaultReadChunkDays,
|
|
|
|
}) {
|
|
|
|
return _fetchSleepIntervalsInChunks(
|
|
|
|
startTime,
|
|
|
|
endTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthKitRawActivityDataPoint>> queryRawActivitySummaries({
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
}) async {
|
|
|
|
final points = await _rawDataSource.getRawActivityData(startTime, endTime);
|
|
|
|
return points
|
|
|
|
.where((e) => e.endTime >= startTime && e.endTime <= endTime)
|
|
|
|
.toList()
|
|
|
|
..sort((a, b) => a.endTime.compareTo(b.endTime));
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> markHrvStressUploaded({
|
|
|
|
required Iterable<int> rawEndTimes,
|
|
|
|
}) {
|
|
|
|
return _localStore.markHrvStressUploaded(
|
|
|
|
userId: _userId,
|
|
|
|
rawEndTimes: rawEndTimes,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> markRealtimeStressUploaded({
|
|
|
|
required Iterable<int> rawEndTimes,
|
|
|
|
}) {
|
|
|
|
return _localStore.markRealtimeStressUploaded(
|
|
|
|
userId: _userId,
|
|
|
|
rawEndTimes: rawEndTimes,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _uploadResults() async {
|
|
|
|
// TODO: Upload OHOS result rows through OHOS/backend APIs and mark uploaded
|
|
|
|
// rows in _localStore after the contract is available.
|
|
|
|
if (_serverHealthApi == null) return;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthRawDailyStressPoint>> _calculateAndStoreDailyStressPoints({
|
|
|
|
required int userId,
|
|
|
|
required List<HealthRawRealtimeStressPoint> realtimePoints,
|
|
|
|
required int nowSeconds,
|
|
|
|
}) async {
|
|
|
|
final todayDate = _dateKeyFromUnixSeconds(nowSeconds);
|
|
|
|
final affectedDates = <int>{
|
|
|
|
todayDate,
|
|
|
|
for (final point in realtimePoints)
|
|
|
|
_dateKeyFromUnixSeconds(point.rawEndTime),
|
|
|
|
}.toList()
|
|
|
|
..sort();
|
|
|
|
final existingDates = await _localStore.existingDailyStressDates(
|
|
|
|
userId: userId,
|
|
|
|
dates: affectedDates,
|
|
|
|
);
|
|
|
|
final dailyStressPoints = <HealthRawDailyStressPoint>[];
|
|
|
|
final emptyDates = <int>[];
|
|
|
|
for (final date in affectedDates) {
|
|
|
|
if (date != todayDate && existingDates.contains(date)) continue;
|
|
|
|
final (startTime, endTime) = _dayRangeFromDateKey(date);
|
|
|
|
final dayRealtimePoints = await _localStore.queryRealtimeStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
);
|
|
|
|
final point = HealthRawDailyStressCalculator.calculate(
|
|
|
|
userId: userId,
|
|
|
|
date: date,
|
|
|
|
realtimePoints: dayRealtimePoints,
|
|
|
|
dataTime: date == todayDate ? nowSeconds : startTime,
|
|
|
|
);
|
|
|
|
if (point == null) {
|
|
|
|
emptyDates.add(date);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
dailyStressPoints.add(point);
|
|
|
|
}
|
|
|
|
await _localStore.upsertDailyStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
points: dailyStressPoints,
|
|
|
|
);
|
|
|
|
await _localStore.deleteDailyStressDates(
|
|
|
|
userId: userId,
|
|
|
|
dates: emptyDates,
|
|
|
|
);
|
|
|
|
return dailyStressPoints;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthRawSleepResult>> _calculateAndStoreSleepResults({
|
|
|
|
required int userId,
|
|
|
|
required List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
required int? latestSleepResultTime,
|
|
|
|
}) async {
|
|
|
|
if (sleepIntervals.isEmpty) 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));
|
|
|
|
final results = <HealthRawSleepResult>[];
|
|
|
|
for (final day in days) {
|
|
|
|
final calculation = HealthSleepCalculator.calculateDay(
|
|
|
|
day: day,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
);
|
|
|
|
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 (latestSleepResultTime != null &&
|
|
|
|
merged.endTime <= latestSleepResultTime) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
results.add(
|
|
|
|
HealthRawSleepResult(
|
|
|
|
userId: userId,
|
|
|
|
date: merged.endTime,
|
|
|
|
startDate: merged.startTime,
|
|
|
|
sleepScore: score,
|
|
|
|
sleepState: state.value,
|
|
|
|
inBedMinutes: calculation.summary.timeInBedMinutes,
|
|
|
|
awakMinutes: calculation.summary.awakeMinutes,
|
|
|
|
sleepMinutes: calculation.summary.sleepMinutes,
|
|
|
|
uploaded: false,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
await _localStore.upsertSleepResults(userId: userId, results: results);
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<HealthRawHrvStressPoint> _filterNewHrvStressPoints(
|
|
|
|
List<HealthRawHrvStressPoint> points,
|
|
|
|
int? latestRawEndTime,
|
|
|
|
) {
|
|
|
|
if (latestRawEndTime == null) return points;
|
|
|
|
return points
|
|
|
|
.where((point) => point.rawEndTime > latestRawEndTime)
|
|
|
|
.toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
List<HealthRawRealtimeStressPoint> _filterNewRealtimeStressPoints(
|
|
|
|
List<HealthRawRealtimeStressPoint> points,
|
|
|
|
int? latestRawEndTime,
|
|
|
|
) {
|
|
|
|
if (latestRawEndTime == null) return points;
|
|
|
|
return points
|
|
|
|
.where((point) => point.rawEndTime > latestRawEndTime)
|
|
|
|
.toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthKitRawDataPoint>> _fetchRawDataInChunks(
|
|
|
|
int dataType,
|
|
|
|
int startTime,
|
|
|
|
int endTime, {
|
|
|
|
required int readChunkDays,
|
|
|
|
}) async {
|
|
|
|
final points = <HealthKitRawDataPoint>[];
|
|
|
|
await for (final point in streamRawData(
|
|
|
|
dataType: dataType,
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
)) {
|
|
|
|
points.add(point);
|
|
|
|
}
|
|
|
|
points.sort((a, b) => a.endTime.compareTo(b.endTime));
|
|
|
|
return points;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthKitRawDataPoint>> _fetchSleepIntervalsInChunks(
|
|
|
|
int startTime,
|
|
|
|
int endTime, {
|
|
|
|
required int readChunkDays,
|
|
|
|
}) async {
|
|
|
|
final points = <HealthKitRawDataPoint>[];
|
|
|
|
await for (final point in streamRawSleepData(
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
)) {
|
|
|
|
points.add(point);
|
|
|
|
}
|
|
|
|
points.sort((a, b) => a.endTime.compareTo(b.endTime));
|
|
|
|
return points;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthKitRawWorkoutDataPoint>> _fetchWorkoutIntervalsInChunks(
|
|
|
|
int startTime,
|
|
|
|
int endTime, {
|
|
|
|
required int readChunkDays,
|
|
|
|
}) async {
|
|
|
|
final points = <HealthKitRawWorkoutDataPoint>[];
|
|
|
|
await for (final point in streamRawWorkoutData(
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
)) {
|
|
|
|
points.add(point);
|
|
|
|
}
|
|
|
|
points.sort((a, b) => a.endTime.compareTo(b.endTime));
|
|
|
|
return points;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int? _minNullable(int? a, int? b) {
|
|
|
|
if (a == null) return b;
|
|
|
|
if (b == null) return a;
|
|
|
|
return math.min(a, b);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int _dateKeyFromUnixSeconds(int seconds) {
|
|
|
|
final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
|
|
|
|
return date.year * 10000 + date.month * 100 + date.day;
|
|
|
|
}
|
|
|
|
|
|
|
|
static (int startTime, int endTime) _dayRangeFromDateKey(int dateKey) {
|
|
|
|
final year = dateKey ~/ 10000;
|
|
|
|
final month = (dateKey ~/ 100) % 100;
|
|
|
|
final day = dateKey % 100;
|
|
|
|
final start = DateTime(year, month, day);
|
|
|
|
return (
|
|
|
|
start.millisecondsSinceEpoch ~/ 1000,
|
|
|
|
start.add(const Duration(days: 1)).millisecondsSinceEpoch ~/ 1000 - 1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void _logError(
|
|
|
|
String message,
|
|
|
|
Object error,
|
|
|
|
StackTrace stackTrace,
|
|
|
|
) {
|
|
|
|
final tagged = 'OHOSHealthRawDataCoreService $message: $error';
|
|
|
|
developer.log(
|
|
|
|
tagged,
|
|
|
|
name: 'OHOSHealthRawDataCoreService',
|
|
|
|
error: error,
|
|
|
|
stackTrace: stackTrace,
|
|
|
|
);
|
|
|
|
debugPrint(tagged);
|
|
|
|
try {
|
|
|
|
AppLogger.e(tagged, error, stackTrace);
|
|
|
|
} catch (_) {
|
|
|
|
// Logger may be unavailable in isolated unit tests.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} |
...
|
...
|
|