|
...
|
...
|
@@ -180,11 +180,14 @@ class HealthRawDataCoreService { |
|
|
|
userId: userId,
|
|
|
|
hrvStressPoints: const <HealthRawHrvStressPoint>[],
|
|
|
|
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
|
|
|
|
dailyStressPoints: const <HealthRawDailyStressPoint>[],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
final totalWatch = Stopwatch()..start();
|
|
|
|
final calculateStartedAt = DateTime.now();
|
|
|
|
_logInfo(
|
|
|
|
'calculateStartAt=${_formatDateTimeMilliseconds(calculateStartedAt)}',
|
|
|
|
);
|
|
|
|
await _localStore.ensureReadable(userId);
|
|
|
|
final readWatch = Stopwatch()..start();
|
|
|
|
final effectiveEndTime =
|
|
|
|
endTime ?? DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
|
|
final earliestStartTime = DateTime.now()
|
|
...
|
...
|
@@ -215,8 +218,6 @@ class HealthRawDataCoreService { |
|
|
|
earliestStartTime,
|
|
|
|
);
|
|
|
|
final restingHeartRateStartTime = heartRateStartTime;
|
|
|
|
final readStartTime =
|
|
|
|
math.min(math.min(hrvStartTime, heartRateStartTime), effectiveEndTime);
|
|
|
|
|
|
|
|
final hrvPoints = await _fetchRawDataInChunks(
|
|
|
|
HealthDataUploadType.hrv.type,
|
|
...
|
...
|
@@ -246,11 +247,7 @@ class HealthRawDataCoreService { |
|
|
|
effectiveEndTime,
|
|
|
|
readChunkDays: readChunkDays,
|
|
|
|
);
|
|
|
|
readWatch.stop();
|
|
|
|
|
|
|
|
final calculateAndStoreWatch = Stopwatch()..start();
|
|
|
|
final calculateStartedAt = DateTime.now();
|
|
|
|
final calculateWatch = Stopwatch()..start();
|
|
|
|
final result = calculate(
|
|
|
|
hrvPoints: hrvPoints,
|
|
|
|
heartRatePoints: heartRatePoints,
|
|
...
|
...
|
@@ -260,38 +257,21 @@ class HealthRawDataCoreService { |
|
|
|
startTime: math.min(hrvStartTime, heartRateStartTime),
|
|
|
|
endTime: effectiveEndTime,
|
|
|
|
);
|
|
|
|
calculateWatch.stop();
|
|
|
|
final calculateFinishedAt = DateTime.now();
|
|
|
|
await _localStore.upsertResult(result);
|
|
|
|
calculateAndStoreWatch.stop();
|
|
|
|
totalWatch.stop();
|
|
|
|
final dailyStressPoints = await _calculateAndStoreDailyStressPoints(
|
|
|
|
userId: userId,
|
|
|
|
realtimePoints: result.realtimeStressPoints,
|
|
|
|
nowSeconds: effectiveEndTime,
|
|
|
|
);
|
|
|
|
final storedResult = result.copyWith(dailyStressPoints: dailyStressPoints);
|
|
|
|
if (isFirstCalculation && (_environmentConfig?.isDebug ?? false)) {
|
|
|
|
AppToast.show(
|
|
|
|
'首次计算完成, 总耗时${_formatDurationMinutesSeconds(totalWatch.elapsed)}',
|
|
|
|
);
|
|
|
|
AppToast.show('首次计算完成');
|
|
|
|
}
|
|
|
|
|
|
|
|
final calculateFinishedAt = DateTime.now();
|
|
|
|
_logInfo(
|
|
|
|
'timing userId=$userId '
|
|
|
|
'rangeStart=$readStartTime rangeStartAt=${_formatUnixSeconds(readStartTime)} '
|
|
|
|
'rangeEnd=$effectiveEndTime rangeEndAt=${_formatUnixSeconds(effectiveEndTime)} '
|
|
|
|
'cursorHrvSourceStart=$hrvContextStart '
|
|
|
|
'cursorRealtimeSourceStart=$realtimeContextStart '
|
|
|
|
'hrvStart=$hrvStartTime realtimeStart=$realtimeStartTime '
|
|
|
|
'hrStart=$heartRateStartTime restingHrStart=$restingHeartRateStartTime '
|
|
|
|
'calculateStartAt=${calculateStartedAt.toIso8601String()} '
|
|
|
|
'calculateEndAt=${calculateFinishedAt.toIso8601String()} '
|
|
|
|
'raw(hrv=${hrvPoints.length}, hr=${heartRatePoints.length}, '
|
|
|
|
'restingHr=${restingHeartRatePoints.length}, '
|
|
|
|
'sleep=${sleepIntervals.length}, workout=${workoutIntervals.length}) '
|
|
|
|
'result(hrv=${result.hrvStressPoints.length}, '
|
|
|
|
'realtime=${result.realtimeStressPoints.length}) '
|
|
|
|
'readMs=${readWatch.elapsedMilliseconds} '
|
|
|
|
'calculateMs=${calculateWatch.elapsedMilliseconds} '
|
|
|
|
'calculateAndStoreMs=${calculateAndStoreWatch.elapsedMilliseconds} '
|
|
|
|
'totalMs=${totalWatch.elapsedMilliseconds}',
|
|
|
|
'calculateEndAt=${_formatDateTimeMilliseconds(calculateFinishedAt)}',
|
|
|
|
);
|
|
|
|
return result;
|
|
|
|
return storedResult;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> _hasHealthReadAuthorization() async {
|
|
...
|
...
|
@@ -392,6 +372,17 @@ class HealthRawDataCoreService { |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthRawDailyStressPoint>> queryDailyStressPoints({
|
|
|
|
required int startDate,
|
|
|
|
required int endDate,
|
|
|
|
}) {
|
|
|
|
return _localStore.queryDailyStressPoints(
|
|
|
|
userId: _userId,
|
|
|
|
startDate: startDate,
|
|
|
|
endDate: endDate,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthKitRawDataPoint>> queryRawDataPoints({
|
|
|
|
required int dataType,
|
|
|
|
required int startTime,
|
|
...
|
...
|
@@ -481,6 +472,57 @@ class HealthRawDataCoreService { |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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<HealthKitRawDataPoint>> _fetchRawDataInChunks(
|
|
|
|
int dataType,
|
|
|
|
int startTime,
|
|
...
|
...
|
@@ -540,26 +582,37 @@ class HealthRawDataCoreService { |
|
|
|
return math.min(a, b);
|
|
|
|
}
|
|
|
|
|
|
|
|
static String _formatUnixSeconds(int seconds) {
|
|
|
|
return DateTime.fromMillisecondsSinceEpoch(seconds * 1000)
|
|
|
|
.toIso8601String();
|
|
|
|
static int _dateKeyFromUnixSeconds(int seconds) {
|
|
|
|
final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
|
|
|
|
return date.year * 10000 + date.month * 100 + date.day;
|
|
|
|
}
|
|
|
|
|
|
|
|
static String _formatDurationMinutesSeconds(Duration duration) {
|
|
|
|
final minutes = duration.inMinutes;
|
|
|
|
final seconds = duration.inSeconds.remainder(60);
|
|
|
|
return '$minutes分钟$seconds秒';
|
|
|
|
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 String _formatDateTimeMilliseconds(DateTime dateTime) {
|
|
|
|
String two(int value) => value.toString().padLeft(2, '0');
|
|
|
|
String three(int value) => value.toString().padLeft(3, '0');
|
|
|
|
return '${dateTime.year.toString().padLeft(4, '0')}-'
|
|
|
|
'${two(dateTime.month)}-'
|
|
|
|
'${two(dateTime.day)}T'
|
|
|
|
'${two(dateTime.hour)}:'
|
|
|
|
'${two(dateTime.minute)}:'
|
|
|
|
'${two(dateTime.second)}.'
|
|
|
|
'${three(dateTime.millisecond)}';
|
|
|
|
}
|
|
|
|
|
|
|
|
static void _logInfo(String message) {
|
|
|
|
final tagged = 'HealthRawDataCoreService $message';
|
|
|
|
developer.log(tagged, name: 'HealthRawDataCoreService');
|
|
|
|
debugPrint(tagged);
|
|
|
|
try {
|
|
|
|
AppLogger.i(tagged);
|
|
|
|
} catch (_) {
|
|
|
|
// Logger may be unavailable in isolated unit tests.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void _logError(
|
|
...
|
...
|
@@ -588,11 +641,26 @@ class HealthRawStressCalculationResult { |
|
|
|
required this.userId,
|
|
|
|
required this.hrvStressPoints,
|
|
|
|
required this.realtimeStressPoints,
|
|
|
|
required this.dailyStressPoints,
|
|
|
|
});
|
|
|
|
|
|
|
|
final int userId;
|
|
|
|
final List<HealthRawHrvStressPoint> hrvStressPoints;
|
|
|
|
final List<HealthRawRealtimeStressPoint> realtimeStressPoints;
|
|
|
|
final List<HealthRawDailyStressPoint> dailyStressPoints;
|
|
|
|
|
|
|
|
HealthRawStressCalculationResult copyWith({
|
|
|
|
List<HealthRawHrvStressPoint>? hrvStressPoints,
|
|
|
|
List<HealthRawRealtimeStressPoint>? realtimeStressPoints,
|
|
|
|
List<HealthRawDailyStressPoint>? dailyStressPoints,
|
|
|
|
}) {
|
|
|
|
return HealthRawStressCalculationResult(
|
|
|
|
userId: userId,
|
|
|
|
hrvStressPoints: hrvStressPoints ?? this.hrvStressPoints,
|
|
|
|
realtimeStressPoints: realtimeStressPoints ?? this.realtimeStressPoints,
|
|
|
|
dailyStressPoints: dailyStressPoints ?? this.dailyStressPoints,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class HealthRawDataUpdatedEvent {
|
|
...
|
...
|
@@ -799,6 +867,140 @@ class HealthRawRealtimeStressPoint { |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class HealthRawDailyStressPoint {
|
|
|
|
const HealthRawDailyStressPoint({
|
|
|
|
required this.userId,
|
|
|
|
required this.date,
|
|
|
|
required this.stressValue,
|
|
|
|
required this.stressScore,
|
|
|
|
required this.state,
|
|
|
|
required this.dataTime,
|
|
|
|
this.uploaded = false,
|
|
|
|
});
|
|
|
|
|
|
|
|
final int userId;
|
|
|
|
final int date;
|
|
|
|
final double stressValue;
|
|
|
|
final int stressScore;
|
|
|
|
final HealthRawStressState state;
|
|
|
|
final int dataTime;
|
|
|
|
final bool uploaded;
|
|
|
|
|
|
|
|
factory HealthRawDailyStressPoint.fromDb(Map<String, Object?> row) {
|
|
|
|
final stressScore = row['stress_score'] as int;
|
|
|
|
return HealthRawDailyStressPoint(
|
|
|
|
userId: row['user_id'] as int,
|
|
|
|
date: row['date'] as int,
|
|
|
|
stressValue: (row['stress_value'] as num).toDouble(),
|
|
|
|
stressScore: stressScore,
|
|
|
|
state: _stressStateFromDb(row['state']) ??
|
|
|
|
healthRawRealtimeStressState(stressScore),
|
|
|
|
dataTime: row['data_time'] as int,
|
|
|
|
uploaded: (row['uploaded'] as int) == 1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class HealthRawDailyStressCalculator {
|
|
|
|
const HealthRawDailyStressCalculator._();
|
|
|
|
|
|
|
|
static const int _confirmedActivityBufferSeconds = 12 * 60;
|
|
|
|
|
|
|
|
static HealthRawDailyStressPoint? calculate({
|
|
|
|
required int userId,
|
|
|
|
required int date,
|
|
|
|
required List<HealthRawRealtimeStressPoint> realtimePoints,
|
|
|
|
required int dataTime,
|
|
|
|
}) {
|
|
|
|
final validValuePoints =
|
|
|
|
realtimePoints.where((e) => _isValidStressValue(e.result)).toList();
|
|
|
|
final activityBuffers = _confirmedActivityBuffers(realtimePoints);
|
|
|
|
final values = validValuePoints
|
|
|
|
.where((e) => !_isInTimeRanges(e.rawEndTime, activityBuffers))
|
|
|
|
.map((e) => e.result)
|
|
|
|
.toList()
|
|
|
|
..sort();
|
|
|
|
if (values.isEmpty) return null;
|
|
|
|
final stressValue = _average(values);
|
|
|
|
final stressScore = _dailyStressScore(values).round().clamp(1, 100).toInt();
|
|
|
|
return HealthRawDailyStressPoint(
|
|
|
|
userId: userId,
|
|
|
|
date: date,
|
|
|
|
stressValue: stressValue,
|
|
|
|
stressScore: stressScore,
|
|
|
|
state: healthRawRealtimeStressState(stressScore),
|
|
|
|
dataTime: dataTime,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static double _dailyStressScore(List<double> sortedValues) {
|
|
|
|
final median = _median(sortedValues);
|
|
|
|
final p75 = _percentile75(sortedValues);
|
|
|
|
return _round(_clamp(median * 0.60 + p75 * 0.40, 1, 100));
|
|
|
|
}
|
|
|
|
|
|
|
|
static List<({int start, int end})> _confirmedActivityBuffers(
|
|
|
|
List<HealthRawRealtimeStressPoint> realtimePoints,
|
|
|
|
) {
|
|
|
|
final ranges = realtimePoints
|
|
|
|
.where((e) =>
|
|
|
|
_isValidStressValue(e.result) &&
|
|
|
|
(e.isWorkout || e.isWorkoutRecovery))
|
|
|
|
.map((e) => (
|
|
|
|
start: e.rawEndTime - _confirmedActivityBufferSeconds,
|
|
|
|
end: e.rawEndTime + _confirmedActivityBufferSeconds,
|
|
|
|
))
|
|
|
|
.toList()
|
|
|
|
..sort((a, b) => a.start.compareTo(b.start));
|
|
|
|
if (ranges.isEmpty) return const <({int start, int end})>[];
|
|
|
|
|
|
|
|
final merged = <({int start, int end})>[];
|
|
|
|
var current = ranges.first;
|
|
|
|
for (final range in ranges.skip(1)) {
|
|
|
|
if (range.start <= current.end) {
|
|
|
|
current = (
|
|
|
|
start: current.start,
|
|
|
|
end: math.max(current.end, range.end),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
merged.add(current);
|
|
|
|
current = range;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
merged.add(current);
|
|
|
|
return merged;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool _isInTimeRanges(int time, List<({int start, int end})> ranges) {
|
|
|
|
return ranges.any((e) => time >= e.start && time <= e.end);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool _isValidStressValue(num value) => value >= 1 && value <= 100;
|
|
|
|
|
|
|
|
static double _average(List<double> values) =>
|
|
|
|
values.reduce((a, b) => a + b) / values.length;
|
|
|
|
|
|
|
|
static double _median(List<double> sortedValues) {
|
|
|
|
final length = sortedValues.length;
|
|
|
|
final middle = length ~/ 2;
|
|
|
|
if (length.isOdd) return sortedValues[middle];
|
|
|
|
return (sortedValues[middle - 1] + sortedValues[middle]) / 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
static double _percentile75(List<double> sortedValues) {
|
|
|
|
final rank = (sortedValues.length * 0.75).ceil().clamp(
|
|
|
|
1,
|
|
|
|
sortedValues.length,
|
|
|
|
);
|
|
|
|
return sortedValues[rank - 1];
|
|
|
|
}
|
|
|
|
|
|
|
|
static double _clamp(num value, num min, num max) =>
|
|
|
|
math.max(min.toDouble(), math.min(max.toDouble(), value.toDouble()));
|
|
|
|
|
|
|
|
static double _round(double value) => (value * 10).round() / 10;
|
|
|
|
}
|
|
|
|
|
|
|
|
HealthRawStressState healthRawRealtimeStressState(num value) {
|
|
|
|
if (value >= 81) return HealthRawStressState.overload;
|
|
|
|
if (value >= 61) return HealthRawStressState.attention;
|
|
...
|
...
|
@@ -834,10 +1036,12 @@ class HealthRawStressDbRows { |
|
|
|
const HealthRawStressDbRows({
|
|
|
|
required this.hrvRows,
|
|
|
|
required this.realtimeRows,
|
|
|
|
required this.dailyStressRows,
|
|
|
|
});
|
|
|
|
|
|
|
|
final List<Map<String, Object?>> hrvRows;
|
|
|
|
final List<Map<String, Object?>> realtimeRows;
|
|
|
|
final List<Map<String, Object?>> dailyStressRows;
|
|
|
|
}
|
|
|
|
|
|
|
|
class HealthRawStressLocalStore {
|
|
...
|
...
|
@@ -849,6 +1053,7 @@ class HealthRawStressLocalStore { |
|
|
|
|
|
|
|
static const hrvResultsTable = 'hrv_results';
|
|
|
|
static const realtimeStressResultsTable = 'realtime_stress_results';
|
|
|
|
static const dailyStressResultsTable = 'daily_stress_results';
|
|
|
|
|
|
|
|
final Directory? _rootDirectory;
|
|
|
|
final DatabaseFactory? _databaseFactory;
|
|
...
|
...
|
@@ -874,6 +1079,48 @@ class HealthRawStressLocalStore { |
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> upsertDailyStressPoints({
|
|
|
|
required int userId,
|
|
|
|
required Iterable<HealthRawDailyStressPoint> points,
|
|
|
|
}) async {
|
|
|
|
final db = await _database(userId);
|
|
|
|
await db.transaction((txn) async {
|
|
|
|
for (final point in points) {
|
|
|
|
await _upsertDailyStressResettingUploaded(txn, _dailyStressRow(point));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Set<int>> existingDailyStressDates({
|
|
|
|
required int userId,
|
|
|
|
required Iterable<int> dates,
|
|
|
|
}) async {
|
|
|
|
final dateList = dates.toSet().toList();
|
|
|
|
if (dateList.isEmpty) return const <int>{};
|
|
|
|
final db = await _database(userId);
|
|
|
|
final rows = await db.query(
|
|
|
|
dailyStressResultsTable,
|
|
|
|
columns: ['date'],
|
|
|
|
where: 'date IN (${List.filled(dateList.length, '?').join(',')})',
|
|
|
|
whereArgs: dateList,
|
|
|
|
);
|
|
|
|
return rows.map((row) => row['date'] as int).toSet();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> deleteDailyStressDates({
|
|
|
|
required int userId,
|
|
|
|
required Iterable<int> dates,
|
|
|
|
}) async {
|
|
|
|
final dateList = dates.toSet().toList();
|
|
|
|
if (dateList.isEmpty) return;
|
|
|
|
final db = await _database(userId);
|
|
|
|
await db.delete(
|
|
|
|
dailyStressResultsTable,
|
|
|
|
where: 'date IN (${List.filled(dateList.length, '?').join(',')})',
|
|
|
|
whereArgs: dateList,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> ensureReadable(int userId) async {
|
|
|
|
final db = await _database(userId);
|
|
|
|
await db.query(
|
|
...
|
...
|
@@ -929,6 +1176,21 @@ class HealthRawStressLocalStore { |
|
|
|
return rows.map(HealthRawRealtimeStressPoint.fromDb).toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<HealthRawDailyStressPoint>> queryDailyStressPoints({
|
|
|
|
required int userId,
|
|
|
|
required int startDate,
|
|
|
|
required int endDate,
|
|
|
|
}) async {
|
|
|
|
final db = await _database(userId);
|
|
|
|
final rows = await db.query(
|
|
|
|
dailyStressResultsTable,
|
|
|
|
where: 'date >= ? AND date <= ?',
|
|
|
|
whereArgs: [startDate, endDate],
|
|
|
|
orderBy: 'date ASC',
|
|
|
|
);
|
|
|
|
return rows.map(HealthRawDailyStressPoint.fromDb).toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<HealthRawStressDbRows> debugQueryAllRows(int userId) async {
|
|
|
|
final db = await _database(userId);
|
|
|
|
final hrvRows = await db.query(
|
|
...
|
...
|
@@ -939,9 +1201,14 @@ class HealthRawStressLocalStore { |
|
|
|
realtimeStressResultsTable,
|
|
|
|
orderBy: 'raw_end_time ASC',
|
|
|
|
);
|
|
|
|
final dailyStressRows = await db.query(
|
|
|
|
dailyStressResultsTable,
|
|
|
|
orderBy: 'date ASC',
|
|
|
|
);
|
|
|
|
return HealthRawStressDbRows(
|
|
|
|
hrvRows: hrvRows,
|
|
|
|
realtimeRows: realtimeRows,
|
|
|
|
dailyStressRows: dailyStressRows,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -1015,7 +1282,7 @@ class HealthRawStressLocalStore { |
|
|
|
final db = await factory.openDatabase(
|
|
|
|
path,
|
|
|
|
options: OpenDatabaseOptions(
|
|
|
|
version: 4,
|
|
|
|
version: 5,
|
|
|
|
onCreate: (db, version) async {
|
|
|
|
await _createTables(db);
|
|
|
|
},
|
|
...
|
...
|
@@ -1030,6 +1297,9 @@ class HealthRawStressLocalStore { |
|
|
|
if (oldVersion < 4) {
|
|
|
|
await _addHrvRawAndStateColumns(db);
|
|
|
|
}
|
|
|
|
if (oldVersion < 5) {
|
|
|
|
await _createDailyStressTable(db);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
...
|
...
|
@@ -1072,6 +1342,21 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( |
|
|
|
uploaded INTEGER NOT NULL DEFAULT 0
|
|
|
|
)
|
|
|
|
''');
|
|
|
|
await _createDailyStressTable(db);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _createDailyStressTable(DatabaseExecutor db) async {
|
|
|
|
await db.execute('''
|
|
|
|
CREATE TABLE IF NOT EXISTS $dailyStressResultsTable (
|
|
|
|
date INTEGER PRIMARY KEY,
|
|
|
|
user_id INTEGER NOT NULL,
|
|
|
|
stress_value REAL NOT NULL,
|
|
|
|
stress_score INTEGER NOT NULL,
|
|
|
|
state INTEGER NOT NULL,
|
|
|
|
data_time INTEGER NOT NULL,
|
|
|
|
uploaded INTEGER NOT NULL DEFAULT 0
|
|
|
|
)
|
|
|
|
''');
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _addFlagColumns(DatabaseExecutor db, String table) async {
|
|
...
|
...
|
@@ -1181,6 +1466,18 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( |
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _dailyStressRow(HealthRawDailyStressPoint point) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'date': point.date,
|
|
|
|
'user_id': point.userId,
|
|
|
|
'stress_value': point.stressValue,
|
|
|
|
'stress_score': point.stressScore,
|
|
|
|
'state': point.state.value,
|
|
|
|
'data_time': point.dataTime,
|
|
|
|
'uploaded': point.uploaded ? 1 : 0,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Map<String, Object?> _flagsRow(HealthRawPointFlags flags) {
|
|
|
|
return <String, Object?>{
|
|
|
|
'is_workout': flags.isWorkout ? 1 : 0,
|
|
...
|
...
|
@@ -1227,4 +1524,28 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( |
|
|
|
whereArgs: [row['raw_end_time']],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _upsertDailyStressResettingUploaded(
|
|
|
|
DatabaseExecutor db,
|
|
|
|
Map<String, Object?> row,
|
|
|
|
) async {
|
|
|
|
await db.insert(
|
|
|
|
dailyStressResultsTable,
|
|
|
|
row,
|
|
|
|
conflictAlgorithm: ConflictAlgorithm.ignore,
|
|
|
|
);
|
|
|
|
await db.update(
|
|
|
|
dailyStressResultsTable,
|
|
|
|
<String, Object?>{
|
|
|
|
'user_id': row['user_id'],
|
|
|
|
'stress_value': row['stress_value'],
|
|
|
|
'stress_score': row['stress_score'],
|
|
|
|
'state': row['state'],
|
|
|
|
'data_time': row['data_time'],
|
|
|
|
'uploaded': 0,
|
|
|
|
},
|
|
|
|
where: 'date = ?',
|
|
|
|
whereArgs: [row['date']],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} |
...
|
...
|
|