|
...
|
...
|
@@ -43,7 +43,76 @@ class LocalHealthDataConvert { |
|
|
|
return rangeDays(dateRangeType, dateKey(previousStart));
|
|
|
|
}
|
|
|
|
|
|
|
|
static List<HealthKitRawDataPoint> completeSleepIntervalsForDay(
|
|
|
|
DateTime day,
|
|
|
|
List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
) {
|
|
|
|
final dayStart = unixSeconds(day);
|
|
|
|
final dayEnd = unixSeconds(day.add(const Duration(days: 1)));
|
|
|
|
final queryStart = unixSeconds(day.subtract(const Duration(days: 1)));
|
|
|
|
final candidates = sleepIntervals
|
|
|
|
.where((e) => e.endTime > queryStart && e.startTime < dayEnd)
|
|
|
|
.toList()
|
|
|
|
..sort((a, b) {
|
|
|
|
final startCompare = a.startTime.compareTo(b.startTime);
|
|
|
|
if (startCompare != 0) return startCompare;
|
|
|
|
return a.endTime.compareTo(b.endTime);
|
|
|
|
});
|
|
|
|
if (candidates.isEmpty) return const <HealthKitRawDataPoint>[];
|
|
|
|
|
|
|
|
final anchorIndex = candidates.indexWhere(
|
|
|
|
(e) => e.endTime > dayStart && e.startTime < dayEnd,
|
|
|
|
);
|
|
|
|
if (anchorIndex < 0) return const <HealthKitRawDataPoint>[];
|
|
|
|
|
|
|
|
var rangeStart = candidates[anchorIndex].startTime;
|
|
|
|
var rangeEnd = candidates[anchorIndex].endTime;
|
|
|
|
var expanded = true;
|
|
|
|
while (expanded) {
|
|
|
|
expanded = false;
|
|
|
|
for (final interval in candidates) {
|
|
|
|
final isConnected =
|
|
|
|
interval.startTime <= rangeEnd && interval.endTime >= rangeStart;
|
|
|
|
if (!isConnected) continue;
|
|
|
|
final nextStart = math.min(rangeStart, interval.startTime);
|
|
|
|
final nextEnd = math.max(rangeEnd, interval.endTime);
|
|
|
|
if (nextStart != rangeStart || nextEnd != rangeEnd) {
|
|
|
|
rangeStart = nextStart;
|
|
|
|
rangeEnd = nextEnd;
|
|
|
|
expanded = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return candidates
|
|
|
|
.where((e) => e.endTime > rangeStart && e.startTime < rangeEnd)
|
|
|
|
.toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
static HealthKitRawDataPoint? mergeContinuousSleepIntervals(
|
|
|
|
List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
) {
|
|
|
|
if (sleepIntervals.isEmpty) return null;
|
|
|
|
final sorted = [...sleepIntervals]..sort((a, b) {
|
|
|
|
final startCompare = a.startTime.compareTo(b.startTime);
|
|
|
|
if (startCompare != 0) return startCompare;
|
|
|
|
return a.endTime.compareTo(b.endTime);
|
|
|
|
});
|
|
|
|
var start = sorted.first.startTime;
|
|
|
|
var end = sorted.first.endTime;
|
|
|
|
for (final interval in sorted.skip(1)) {
|
|
|
|
if (interval.startTime > end) break;
|
|
|
|
end = math.max(end, interval.endTime);
|
|
|
|
}
|
|
|
|
return HealthKitRawDataPoint(
|
|
|
|
dataType: sorted.first.dataType,
|
|
|
|
startTime: start,
|
|
|
|
endTime: end,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static SleepStatisticsData sleepStatistics({
|
|
|
|
required int dateRangeType,
|
|
|
|
required List<DateTime> days,
|
|
|
|
required List<DateTime> previousDays,
|
|
|
|
required List<HealthKitRawDataPoint> sleepIntervals,
|
|
...
|
...
|
@@ -69,6 +138,7 @@ class LocalHealthDataConvert { |
|
|
|
final previousValidSleep =
|
|
|
|
previousDaily.where((e) => e.durationSeconds > 0).toList();
|
|
|
|
final allSleepHr = validSleep.expand((e) => e.sleepHr).toList();
|
|
|
|
final shouldIncludeHeartRate = dateRangeType == 3;
|
|
|
|
|
|
|
|
return SleepStatisticsData(
|
|
|
|
avgSleepDuration: _averageOrNull(
|
|
...
|
...
|
@@ -109,19 +179,28 @@ class LocalHealthDataConvert { |
|
|
|
worstSleepInfo: _worstSleepInfo(validSleep),
|
|
|
|
earliestSleepInfo: _earliestSleepInfo(validSleep),
|
|
|
|
latestSleepInfo: _latestSleepInfo(validSleep),
|
|
|
|
hrList: [
|
|
|
|
for (final point in allSleepHr)
|
|
|
|
SleepHrItem(time: point.endTime, value: point.value),
|
|
|
|
],
|
|
|
|
avgHr: _averageOrNull(
|
|
|
|
allSleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
),
|
|
|
|
maxHr: _maxOrNull(
|
|
|
|
allSleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
),
|
|
|
|
minHr: _minOrNull(
|
|
|
|
allSleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
),
|
|
|
|
hrList: shouldIncludeHeartRate
|
|
|
|
? [
|
|
|
|
for (final point in allSleepHr)
|
|
|
|
SleepHrItem(time: point.endTime, value: point.value),
|
|
|
|
]
|
|
|
|
: null,
|
|
|
|
avgHr: shouldIncludeHeartRate
|
|
|
|
? _averageOrNull(
|
|
|
|
allSleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
)
|
|
|
|
: null,
|
|
|
|
maxHr: shouldIncludeHeartRate
|
|
|
|
? _maxOrNull(
|
|
|
|
allSleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
)
|
|
|
|
: null,
|
|
|
|
minHr: shouldIncludeHeartRate
|
|
|
|
? _minOrNull(
|
|
|
|
allSleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
)
|
|
|
|
: null,
|
|
|
|
sleepTargetDuration: (_sleepGoalMinutes * 60).round(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -274,7 +353,7 @@ class LocalHealthDataConvert { |
|
|
|
? null
|
|
|
|
: (latestWithGoal!.exerciseTimeGoal! * 60).round(),
|
|
|
|
updateTime: latestWithGoal?.endTime,
|
|
|
|
sleepTargetDuration: _sleepGoalMinutes.round(),
|
|
|
|
sleepTargetDuration: (60 * _sleepGoalMinutes).round(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -282,30 +361,26 @@ class LocalHealthDataConvert { |
|
|
|
required DateTime day,
|
|
|
|
required List<HealthRawHrvStressPoint> hrvPoints,
|
|
|
|
required List<HealthKitRawActivityDataPoint> activity,
|
|
|
|
required List<HealthKitRawDataPoint> heartRate,
|
|
|
|
required List<HealthKitRawDataPoint> sleepingHeartRate,
|
|
|
|
required List<HealthKitRawDataPoint> restingHeartRate,
|
|
|
|
required List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
}) {
|
|
|
|
final activitySummary = _activityDaySummary(day, activity);
|
|
|
|
final sleepSummary = _sleepDaySummary(day, sleepIntervals, heartRate);
|
|
|
|
final sleepSummary = _sleepDaySummary(
|
|
|
|
day,
|
|
|
|
sleepIntervals,
|
|
|
|
sleepingHeartRate,
|
|
|
|
);
|
|
|
|
final dayStart = unixSeconds(day);
|
|
|
|
final dayEnd = unixSeconds(day.add(const Duration(days: 1)));
|
|
|
|
final sleepWindowStart =
|
|
|
|
unixSeconds(day.subtract(const Duration(hours: 12)));
|
|
|
|
final sleepWindowEnd = unixSeconds(day.add(const Duration(hours: 12)));
|
|
|
|
final sleepTimeList = sleepIntervals
|
|
|
|
.where(
|
|
|
|
(e) => e.endTime > sleepWindowStart && e.startTime < sleepWindowEnd)
|
|
|
|
.map(
|
|
|
|
(e) => V2SleepTimeRange(
|
|
|
|
fromTime: math.max(e.startTime, sleepWindowStart),
|
|
|
|
toTime: math.min(e.endTime, sleepWindowEnd),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.toList();
|
|
|
|
final dayHr = heartRate
|
|
|
|
.where((e) => e.endTime >= dayStart && e.endTime < dayEnd)
|
|
|
|
.toList();
|
|
|
|
final sleepTimeList = sleepSummary.mergeSleepTimeRange == null
|
|
|
|
? <V2SleepTimeRange>[]
|
|
|
|
: [
|
|
|
|
V2SleepTimeRange(
|
|
|
|
fromTime: sleepSummary.mergeSleepTimeRange!.startTime,
|
|
|
|
toTime: sleepSummary.mergeSleepTimeRange!.endTime,
|
|
|
|
),
|
|
|
|
];
|
|
|
|
final dayRestingHr = restingHeartRate
|
|
|
|
.where((e) => e.endTime >= dayStart && e.endTime < dayEnd)
|
|
|
|
.toList()
|
|
...
|
...
|
@@ -315,8 +390,9 @@ class LocalHealthDataConvert { |
|
|
|
hrvAvg: _averageOrNull(hrvPoints.map((e) => e.result).toList())?.round(),
|
|
|
|
lastRestingHrValue:
|
|
|
|
dayRestingHr.isEmpty ? null : dayRestingHr.last.value?.round(),
|
|
|
|
hrAvg: _averageOrNull(dayHr.map((e) => e.value).whereType<num>().toList())
|
|
|
|
?.round(),
|
|
|
|
hrAvg: _averageOrNull(
|
|
|
|
sleepSummary.sleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
)?.round(),
|
|
|
|
move: activitySummary.move.round(),
|
|
|
|
exercise: activitySummary.exerciseSeconds.round(),
|
|
|
|
stand: activitySummary.standHours.round(),
|
|
...
|
...
|
@@ -388,25 +464,22 @@ class LocalHealthDataConvert { |
|
|
|
List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
List<HealthKitRawDataPoint> heartRate,
|
|
|
|
) {
|
|
|
|
final windowStart = unixSeconds(day.subtract(const Duration(hours: 12)));
|
|
|
|
final windowEnd = unixSeconds(day.add(const Duration(hours: 12)));
|
|
|
|
final intervals = sleepIntervals
|
|
|
|
.where((e) => e.endTime > windowStart && e.startTime < windowEnd)
|
|
|
|
.toList();
|
|
|
|
final intervals = completeSleepIntervalsForDay(day, sleepIntervals);
|
|
|
|
final mergedInterval = mergeContinuousSleepIntervals(intervals);
|
|
|
|
final windowStart = mergedInterval?.startTime ?? unixSeconds(day);
|
|
|
|
final windowEnd = mergedInterval?.endTime ??
|
|
|
|
unixSeconds(day.add(const Duration(days: 1)));
|
|
|
|
final summary = _sleepSummary(intervals, windowStart, windowEnd);
|
|
|
|
final score = _sleepScore(summary);
|
|
|
|
final sleepStageIntervals = intervals
|
|
|
|
.where((e) => _isAsleepSleepType(e.dataType))
|
|
|
|
.toList(growable: false);
|
|
|
|
final heartRateIntervals =
|
|
|
|
sleepStageIntervals.isEmpty ? intervals : sleepStageIntervals;
|
|
|
|
final sleepHr = heartRate
|
|
|
|
.where((point) => heartRateIntervals.any((interval) =>
|
|
|
|
.where((point) => intervals.any((interval) =>
|
|
|
|
point.endTime > interval.startTime &&
|
|
|
|
point.endTime <= interval.endTime))
|
|
|
|
.toList();
|
|
|
|
return _SleepDaySummary(
|
|
|
|
day: day,
|
|
|
|
sleepIntervals: intervals,
|
|
|
|
mergeSleepTimeRange: mergedInterval,
|
|
|
|
durationSeconds: (summary.totalAsleepMinutes * 60).round(),
|
|
|
|
asleepTime: intervals.isEmpty
|
|
|
|
? null
|
|
...
|
...
|
@@ -711,13 +784,6 @@ class LocalHealthDataConvert { |
|
|
|
return 3;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool _isAsleepSleepType(int type) {
|
|
|
|
return type == _sleepTypeAsleepUnspecified ||
|
|
|
|
type == _sleepTypeAsleepCore ||
|
|
|
|
type == _sleepTypeAsleepDeep ||
|
|
|
|
type == _sleepTypeAsleepRem;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int _hrvStateFromValue(num value) {
|
|
|
|
if (value >= 30) return 4;
|
|
|
|
if (value >= 21) return 3;
|
|
...
|
...
|
@@ -778,6 +844,8 @@ class LocalHealthDataConvert { |
|
|
|
class _SleepDaySummary {
|
|
|
|
const _SleepDaySummary({
|
|
|
|
required this.day,
|
|
|
|
required this.sleepIntervals,
|
|
|
|
required this.mergeSleepTimeRange,
|
|
|
|
required this.durationSeconds,
|
|
|
|
required this.asleepTime,
|
|
|
|
required this.score,
|
|
...
|
...
|
@@ -786,6 +854,8 @@ class _SleepDaySummary { |
|
|
|
});
|
|
|
|
|
|
|
|
final DateTime day;
|
|
|
|
final List<HealthKitRawDataPoint> sleepIntervals;
|
|
|
|
final HealthKitRawDataPoint? mergeSleepTimeRange;
|
|
|
|
final num durationSeconds;
|
|
|
|
final num? asleepTime;
|
|
|
|
final num? score;
|
...
|
...
|
|