|
...
|
...
|
@@ -25,22 +25,29 @@ class HealthRawDataCoreService { |
|
|
|
HealthRawStressLocalStore? localStore,
|
|
|
|
AppEnvironmentConfig? environmentConfig,
|
|
|
|
int Function()? userIdProvider,
|
|
|
|
bool uploadResultsAfterCalculation = true,
|
|
|
|
}) : _healthApi = healthApi ?? HealthKitHostApi(),
|
|
|
|
_rawDataApi = rawDataApi ?? HealthKitRawDataHostApi(),
|
|
|
|
_localStore = localStore ?? HealthRawStressLocalStore(),
|
|
|
|
_environmentConfig = environmentConfig,
|
|
|
|
_userIdProvider = userIdProvider;
|
|
|
|
_userIdProvider = userIdProvider,
|
|
|
|
_uploadResultsAfterCalculation = uploadResultsAfterCalculation;
|
|
|
|
|
|
|
|
final HealthKitHostApi _healthApi;
|
|
|
|
final HealthKitRawDataHostApi _rawDataApi;
|
|
|
|
final HealthRawStressLocalStore _localStore;
|
|
|
|
final AppEnvironmentConfig? _environmentConfig;
|
|
|
|
final int Function()? _userIdProvider;
|
|
|
|
final bool _uploadResultsAfterCalculation;
|
|
|
|
final StreamController<HealthRawDataUpdatedEvent>
|
|
|
|
_healthDataUpdatedController =
|
|
|
|
StreamController<HealthRawDataUpdatedEvent>.broadcast();
|
|
|
|
Future<void>? _healthDataUpdatedCalculation;
|
|
|
|
Future<HealthRawStressCalculationResult>? _coreCalculation;
|
|
|
|
List<int> _pendingHealthDataUpdatedTypes = const <int>[];
|
|
|
|
bool _isUploadingHrvResults = false;
|
|
|
|
bool _isUploadingRealtimeStressResults = false;
|
|
|
|
bool _isUploadingDailyStressResults = false;
|
|
|
|
|
|
|
|
int get _userId {
|
|
|
|
final userId = _userIdProvider?.call() ?? 0;
|
|
...
|
...
|
@@ -127,7 +134,11 @@ class HealthRawDataCoreService { |
|
|
|
int? endTime,
|
|
|
|
int readChunkDays = defaultReadChunkDays,
|
|
|
|
}) async {
|
|
|
|
return _runWithLog(
|
|
|
|
final running = _coreCalculation;
|
|
|
|
if (running != null) {
|
|
|
|
return running;
|
|
|
|
}
|
|
|
|
final task = _runWithLog(
|
|
|
|
action: 'startCoreCaculate',
|
|
|
|
body: () => _readCalculateAndStore(
|
|
|
|
endTime: endTime,
|
|
...
|
...
|
@@ -135,6 +146,14 @@ class HealthRawDataCoreService { |
|
|
|
forceStartTime: null,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
_coreCalculation = task;
|
|
|
|
try {
|
|
|
|
return await task;
|
|
|
|
} finally {
|
|
|
|
if (identical(_coreCalculation, task)) {
|
|
|
|
_coreCalculation = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<HealthRawStressCalculationResult> syncAndStore({
|
|
...
|
...
|
@@ -258,11 +277,18 @@ class HealthRawDataCoreService { |
|
|
|
endTime: effectiveEndTime,
|
|
|
|
);
|
|
|
|
await _localStore.upsertResult(result);
|
|
|
|
if (_uploadResultsAfterCalculation) {
|
|
|
|
await _uploadHrvResults();
|
|
|
|
await _uploadRealtimeStressResults();
|
|
|
|
}
|
|
|
|
final dailyStressPoints = await _calculateAndStoreDailyStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
realtimePoints: result.realtimeStressPoints,
|
|
|
|
nowSeconds: effectiveEndTime,
|
|
|
|
);
|
|
|
|
if (_uploadResultsAfterCalculation) {
|
|
|
|
await _uploadDailyStressResults();
|
|
|
|
}
|
|
|
|
final storedResult = result.copyWith(dailyStressPoints: dailyStressPoints);
|
|
|
|
if (isFirstCalculation && (_environmentConfig?.isDebug ?? false)) {
|
|
|
|
AppToast.show('首次计算完成');
|
|
...
|
...
|
@@ -450,6 +476,78 @@ class HealthRawDataCoreService { |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _uploadHrvResults() async {
|
|
|
|
if (_isUploadingHrvResults) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!await _localStore.hasPendingHrvStressUploads(userId: _userId)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_isUploadingHrvResults = true;
|
|
|
|
try {
|
|
|
|
final uploadedUntil = await _rawDataApi.performHRVDataUpload(
|
|
|
|
sqliteFilePath: await _localStore.dbPath(_userId),
|
|
|
|
);
|
|
|
|
if (uploadedUntil > 0) {
|
|
|
|
await _localStore.markHrvStressUploadedUntil(
|
|
|
|
userId: _userId,
|
|
|
|
rawEndTime: uploadedUntil,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError('upload hrv results failed', error, stackTrace);
|
|
|
|
} finally {
|
|
|
|
_isUploadingHrvResults = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _uploadRealtimeStressResults() async {
|
|
|
|
if (_isUploadingRealtimeStressResults) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!await _localStore.hasPendingRealtimeStressUploads(userId: _userId)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_isUploadingRealtimeStressResults = true;
|
|
|
|
try {
|
|
|
|
final uploadedUntil = await _rawDataApi.performHRDataUpload(
|
|
|
|
sqliteFilePath: await _localStore.dbPath(_userId),
|
|
|
|
);
|
|
|
|
if (uploadedUntil > 0) {
|
|
|
|
await _localStore.markRealtimeStressUploadedUntil(
|
|
|
|
userId: _userId,
|
|
|
|
rawEndTime: uploadedUntil,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError('upload realtime stress results failed', error, stackTrace);
|
|
|
|
} finally {
|
|
|
|
_isUploadingRealtimeStressResults = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _uploadDailyStressResults() async {
|
|
|
|
if (_isUploadingDailyStressResults) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!await _localStore.hasPendingDailyStressUploads(userId: _userId)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_isUploadingDailyStressResults = true;
|
|
|
|
try {
|
|
|
|
final success = await _rawDataApi.performAvgRealtimeStressDataUpload(
|
|
|
|
sqliteFilePath: await _localStore.dbPath(_userId),
|
|
|
|
);
|
|
|
|
if (success) {
|
|
|
|
await _localStore.markDailyStressUploaded(userId: _userId);
|
|
|
|
}
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
_logError('upload daily stress results failed', error, stackTrace);
|
|
|
|
} finally {
|
|
|
|
_isUploadingDailyStressResults = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
HealthRawStressCalculationResult calculate({
|
|
|
|
required List<HealthKitRawDataPoint> hrvPoints,
|
|
|
|
required List<HealthKitRawDataPoint> heartRatePoints,
|
|
...
|
...
|
@@ -833,6 +931,7 @@ class HealthRawRealtimeStressPoint { |
|
|
|
const HealthRawRealtimeStressPoint({
|
|
|
|
required this.userId,
|
|
|
|
required this.rawEndTime,
|
|
|
|
required this.rawHr,
|
|
|
|
required this.result,
|
|
|
|
required this.sourceStartTime,
|
|
|
|
required this.sourceEndTime,
|
|
...
|
...
|
@@ -842,6 +941,7 @@ class HealthRawRealtimeStressPoint { |
|
|
|
|
|
|
|
final int userId;
|
|
|
|
final int rawEndTime;
|
|
|
|
final double rawHr;
|
|
|
|
final double result;
|
|
|
|
final int sourceStartTime;
|
|
|
|
final int sourceEndTime;
|
|
...
|
...
|
@@ -858,6 +958,7 @@ class HealthRawRealtimeStressPoint { |
|
|
|
return HealthRawRealtimeStressPoint(
|
|
|
|
userId: row['user_id'] as int,
|
|
|
|
rawEndTime: row['raw_end_time'] as int,
|
|
|
|
rawHr: (row['raw_hr'] as num?)?.toDouble() ?? 0,
|
|
|
|
result: (row['result'] as num).toDouble(),
|
|
|
|
sourceStartTime: row['source_start_time'] as int,
|
|
|
|
sourceEndTime: row['source_end_time'] as int,
|
|
...
|
...
|
@@ -1231,6 +1332,18 @@ class HealthRawStressLocalStore { |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> markHrvStressUploadedUntil({
|
|
|
|
required int userId,
|
|
|
|
required int rawEndTime,
|
|
|
|
}) {
|
|
|
|
return _markUploadedUntil(
|
|
|
|
userId: userId,
|
|
|
|
table: hrvResultsTable,
|
|
|
|
timeColumn: 'raw_end_time',
|
|
|
|
time: rawEndTime,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> markRealtimeStressUploaded({
|
|
|
|
required int userId,
|
|
|
|
required Iterable<int> rawEndTimes,
|
|
...
|
...
|
@@ -1242,6 +1355,40 @@ class HealthRawStressLocalStore { |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> markRealtimeStressUploadedUntil({
|
|
|
|
required int userId,
|
|
|
|
required int rawEndTime,
|
|
|
|
}) {
|
|
|
|
return _markUploadedUntil(
|
|
|
|
userId: userId,
|
|
|
|
table: realtimeStressResultsTable,
|
|
|
|
timeColumn: 'raw_end_time',
|
|
|
|
time: rawEndTime,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> markDailyStressUploaded({required int userId}) async {
|
|
|
|
final db = await _database(userId);
|
|
|
|
await db.update(
|
|
|
|
dailyStressResultsTable,
|
|
|
|
{'uploaded': 1},
|
|
|
|
where: 'uploaded != 1',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> hasPendingHrvStressUploads({required int userId}) {
|
|
|
|
return _hasPendingUploads(userId: userId, table: hrvResultsTable);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> hasPendingRealtimeStressUploads({required int userId}) {
|
|
|
|
return _hasPendingUploads(
|
|
|
|
userId: userId, table: realtimeStressResultsTable);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> hasPendingDailyStressUploads({required int userId}) {
|
|
|
|
return _hasPendingUploads(userId: userId, table: dailyStressResultsTable);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<String> dbPath(int userId) async {
|
|
|
|
final dir = _rootDirectory ?? await getApplicationDocumentsDirectory();
|
|
|
|
return '${dir.path}/hrv_result_$userId.sqlite';
|
|
...
|
...
|
@@ -1282,7 +1429,7 @@ class HealthRawStressLocalStore { |
|
|
|
final db = await factory.openDatabase(
|
|
|
|
path,
|
|
|
|
options: OpenDatabaseOptions(
|
|
|
|
version: 5,
|
|
|
|
version: 6,
|
|
|
|
onCreate: (db, version) async {
|
|
|
|
await _createTables(db);
|
|
|
|
},
|
|
...
|
...
|
@@ -1300,6 +1447,9 @@ class HealthRawStressLocalStore { |
|
|
|
if (oldVersion < 5) {
|
|
|
|
await _createDailyStressTable(db);
|
|
|
|
}
|
|
|
|
if (oldVersion < 6) {
|
|
|
|
await _addRealtimeRawHrColumn(db);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
...
|
...
|
@@ -1332,6 +1482,7 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable ( |
|
|
|
CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable (
|
|
|
|
raw_end_time INTEGER PRIMARY KEY,
|
|
|
|
user_id INTEGER NOT NULL,
|
|
|
|
raw_hr REAL NOT NULL DEFAULT 0,
|
|
|
|
result REAL NOT NULL,
|
|
|
|
source_start_time INTEGER NOT NULL,
|
|
|
|
source_end_time INTEGER NOT NULL,
|
|
...
|
...
|
@@ -1408,6 +1559,16 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _addRealtimeRawHrColumn(DatabaseExecutor db) async {
|
|
|
|
try {
|
|
|
|
await db.execute(
|
|
|
|
'ALTER TABLE $realtimeStressResultsTable ADD COLUMN raw_hr REAL NOT NULL DEFAULT 0',
|
|
|
|
);
|
|
|
|
} on DatabaseException catch (error) {
|
|
|
|
if (!error.isDuplicateColumnError()) rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<int?> _latestSourceStartTime(int userId, String table) async {
|
|
|
|
final db = await _database(userId);
|
|
|
|
final rows = await db.query(
|
|
...
|
...
|
@@ -1436,6 +1597,36 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _markUploadedUntil({
|
|
|
|
required int userId,
|
|
|
|
required String table,
|
|
|
|
required String timeColumn,
|
|
|
|
required int time,
|
|
|
|
}) async {
|
|
|
|
if (time <= 0) return;
|
|
|
|
final db = await _database(userId);
|
|
|
|
await db.update(
|
|
|
|
table,
|
|
|
|
{'uploaded': 1},
|
|
|
|
where: '$timeColumn <= ? AND uploaded != 1',
|
|
|
|
whereArgs: [time],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> _hasPendingUploads({
|
|
|
|
required int userId,
|
|
|
|
required String table,
|
|
|
|
}) async {
|
|
|
|
final db = await _database(userId);
|
|
|
|
final rows = await db.query(
|
|
|
|
table,
|
|
|
|
columns: ['uploaded'],
|
|
|
|
where: 'uploaded != 1',
|
|
|
|
limit: 1,
|
|
|
|
);
|
|
|
|
return rows.isNotEmpty;
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _hrvRow(HealthRawHrvStressPoint point) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'raw_end_time': point.rawEndTime,
|
|
...
|
...
|
@@ -1458,6 +1649,7 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( |
|
|
|
return <String, Object?>{
|
|
|
|
'raw_end_time': point.rawEndTime,
|
|
|
|
'user_id': point.userId,
|
|
|
|
'raw_hr': point.rawHr,
|
|
|
|
'result': point.result,
|
|
|
|
'source_start_time': point.sourceStartTime,
|
|
|
|
'source_end_time': point.sourceEndTime,
|
|
...
|
...
|
@@ -1492,16 +1684,32 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( |
|
|
|
String table,
|
|
|
|
Map<String, Object?> row,
|
|
|
|
) async {
|
|
|
|
await db.insert(
|
|
|
|
final existing = await db.query(
|
|
|
|
table,
|
|
|
|
where: 'raw_end_time = ?',
|
|
|
|
whereArgs: [row['raw_end_time']],
|
|
|
|
limit: 1,
|
|
|
|
);
|
|
|
|
if (existing.isEmpty) {
|
|
|
|
await db.insert(table, row);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
final existingRow = existing.first;
|
|
|
|
if (_matchesStoredValues(
|
|
|
|
existingRow, row, _rawResultStoredValueKeys(row))) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
final uploadPayloadChanged = !_matchesStoredValues(
|
|
|
|
existingRow,
|
|
|
|
row,
|
|
|
|
conflictAlgorithm: ConflictAlgorithm.ignore,
|
|
|
|
_rawResultUploadValueKeys(row),
|
|
|
|
);
|
|
|
|
await db.update(
|
|
|
|
table,
|
|
|
|
<String, Object?>{
|
|
|
|
'user_id': row['user_id'],
|
|
|
|
if (row.containsKey('raw_hrv')) 'raw_hrv': row['raw_hrv'],
|
|
|
|
if (row.containsKey('raw_hr')) 'raw_hr': row['raw_hr'],
|
|
|
|
'result': row['result'],
|
|
|
|
'source_start_time': row['source_start_time'],
|
|
|
|
'source_end_time': row['source_end_time'],
|
|
...
|
...
|
@@ -1518,7 +1726,7 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( |
|
|
|
'is_workout_recovery': row['is_workout_recovery'],
|
|
|
|
'is_sleep_likely': row['is_sleep_likely'],
|
|
|
|
'is_suspected_activity': row['is_suspected_activity'],
|
|
|
|
'uploaded': 0,
|
|
|
|
'uploaded': uploadPayloadChanged ? 0 : existingRow['uploaded'],
|
|
|
|
},
|
|
|
|
where: 'raw_end_time = ?',
|
|
|
|
whereArgs: [row['raw_end_time']],
|
|
...
|
...
|
@@ -1529,10 +1737,21 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( |
|
|
|
DatabaseExecutor db,
|
|
|
|
Map<String, Object?> row,
|
|
|
|
) async {
|
|
|
|
await db.insert(
|
|
|
|
final existing = await db.query(
|
|
|
|
dailyStressResultsTable,
|
|
|
|
where: 'date = ?',
|
|
|
|
whereArgs: [row['date']],
|
|
|
|
limit: 1,
|
|
|
|
);
|
|
|
|
if (existing.isEmpty) {
|
|
|
|
await db.insert(dailyStressResultsTable, row);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
final existingRow = existing.first;
|
|
|
|
final valueChanged = !_matchesStoredValues(
|
|
|
|
existingRow,
|
|
|
|
row,
|
|
|
|
conflictAlgorithm: ConflictAlgorithm.ignore,
|
|
|
|
const ['user_id', 'stress_value', 'stress_score', 'state'],
|
|
|
|
);
|
|
|
|
await db.update(
|
|
|
|
dailyStressResultsTable,
|
|
...
|
...
|
@@ -1542,10 +1761,68 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( |
|
|
|
'stress_score': row['stress_score'],
|
|
|
|
'state': row['state'],
|
|
|
|
'data_time': row['data_time'],
|
|
|
|
'uploaded': 0,
|
|
|
|
'uploaded': valueChanged ? 0 : existingRow['uploaded'],
|
|
|
|
},
|
|
|
|
where: 'date = ?',
|
|
|
|
whereArgs: [row['date']],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
List<String> _rawResultStoredValueKeys(Map<String, Object?> row) {
|
|
|
|
return <String>[
|
|
|
|
'user_id',
|
|
|
|
if (row.containsKey('raw_hrv')) 'raw_hrv',
|
|
|
|
if (row.containsKey('raw_hr')) 'raw_hr',
|
|
|
|
'result',
|
|
|
|
'source_start_time',
|
|
|
|
'source_end_time',
|
|
|
|
if (row.containsKey('state')) 'state',
|
|
|
|
if (row.containsKey('baseline_hrv')) 'baseline_hrv',
|
|
|
|
if (row.containsKey('baseline_awake_hrv')) 'baseline_awake_hrv',
|
|
|
|
if (row.containsKey('baseline_sleep_hrv')) 'baseline_sleep_hrv',
|
|
|
|
if (row.containsKey('baseline_resting_hr')) 'baseline_resting_hr',
|
|
|
|
'is_workout',
|
|
|
|
'is_workout_recovery',
|
|
|
|
'is_sleep_likely',
|
|
|
|
'is_suspected_activity',
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
List<String> _rawResultUploadValueKeys(Map<String, Object?> row) {
|
|
|
|
return <String>[
|
|
|
|
'user_id',
|
|
|
|
if (row.containsKey('raw_hrv')) 'raw_hrv',
|
|
|
|
if (row.containsKey('raw_hr')) 'raw_hr',
|
|
|
|
'result',
|
|
|
|
if (row.containsKey('state')) 'state',
|
|
|
|
if (row.containsKey('baseline_hrv')) 'baseline_hrv',
|
|
|
|
if (row.containsKey('baseline_awake_hrv')) 'baseline_awake_hrv',
|
|
|
|
if (row.containsKey('baseline_sleep_hrv')) 'baseline_sleep_hrv',
|
|
|
|
if (row.containsKey('baseline_resting_hr')) 'baseline_resting_hr',
|
|
|
|
'is_workout',
|
|
|
|
'is_workout_recovery',
|
|
|
|
'is_sleep_likely',
|
|
|
|
'is_suspected_activity',
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
bool _matchesStoredValues(
|
|
|
|
Map<String, Object?> existing,
|
|
|
|
Map<String, Object?> next,
|
|
|
|
Iterable<String> keys,
|
|
|
|
) {
|
|
|
|
for (final key in keys) {
|
|
|
|
if (!_storedValueEquals(existing[key], next[key])) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool _storedValueEquals(Object? a, Object? b) {
|
|
|
|
if (a is num && b is num) {
|
|
|
|
return (a.toDouble() - b.toDouble()).abs() < 0.000001;
|
|
|
|
}
|
|
|
|
return a == b;
|
|
|
|
}
|
|
|
|
} |
...
|
...
|
|