|
...
|
...
|
@@ -13,6 +13,9 @@ import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; |
|
|
|
class LocalHealthDataConvert {
|
|
|
|
const LocalHealthDataConvert._();
|
|
|
|
|
|
|
|
static const _sleepDayCutoffMinutes = 12 * 60;
|
|
|
|
static const _minutesPerDay = 24 * 60;
|
|
|
|
|
|
|
|
static List<DateTime> rangeDays(int dateRangeType, int startDate) {
|
|
|
|
final start = dateFromKey(startDate);
|
|
|
|
final count = switch (dateRangeType) {
|
|
...
|
...
|
@@ -120,8 +123,14 @@ class LocalHealthDataConvert { |
|
|
|
],
|
|
|
|
bestSleepInfo: _bestSleepInfo(validSleep),
|
|
|
|
worstSleepInfo: _worstSleepInfo(validSleep),
|
|
|
|
earliestSleepInfo: _earliestSleepInfo(validSleep),
|
|
|
|
latestSleepInfo: _latestSleepInfo(validSleep),
|
|
|
|
earliestSleepInfo: _earliestSleepInfo(
|
|
|
|
validSleep,
|
|
|
|
allowedSleepDays: days,
|
|
|
|
),
|
|
|
|
latestSleepInfo: _latestSleepInfo(
|
|
|
|
validSleep,
|
|
|
|
allowedSleepDays: days,
|
|
|
|
),
|
|
|
|
hrList: shouldIncludeHeartRate
|
|
|
|
? [
|
|
|
|
// SleepHrItem(time: 1784091398, value: 63),
|
|
...
|
...
|
@@ -208,8 +217,14 @@ class LocalHealthDataConvert { |
|
|
|
],
|
|
|
|
bestSleepInfo: _bestSleepResultInfo(validSleep, best: true),
|
|
|
|
worstSleepInfo: _worstSleepResultInfo(validSleep),
|
|
|
|
earliestSleepInfo: _earliestSleepResultInfo(validSleep),
|
|
|
|
latestSleepInfo: _latestSleepResultInfo(validSleep),
|
|
|
|
earliestSleepInfo: _earliestSleepResultInfo(
|
|
|
|
sleepResults,
|
|
|
|
allowedSleepDays: days,
|
|
|
|
),
|
|
|
|
latestSleepInfo: _latestSleepResultInfo(
|
|
|
|
sleepResults,
|
|
|
|
allowedSleepDays: days,
|
|
|
|
),
|
|
|
|
hrList: shouldIncludeHeartRate
|
|
|
|
? [
|
|
|
|
for (final point in allSleepHr)
|
|
...
|
...
|
@@ -827,16 +842,23 @@ class LocalHealthDataConvert { |
|
|
|
}
|
|
|
|
|
|
|
|
static EarliestSleepInfo? _earliestSleepInfo(
|
|
|
|
List<HealthSleepDayCalculation> daily,
|
|
|
|
) {
|
|
|
|
final sorted = daily.where((e) => e.asleepTime != null).toList()
|
|
|
|
..sort((a, b) => a.asleepTime!.compareTo(b.asleepTime!));
|
|
|
|
final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
return item == null
|
|
|
|
List<HealthSleepDayCalculation> daily, {
|
|
|
|
required Iterable<DateTime> allowedSleepDays,
|
|
|
|
}) {
|
|
|
|
final bedtime = _extremeBedtime(
|
|
|
|
_bedtimesBySleepDay(
|
|
|
|
daily,
|
|
|
|
(item) => item.asleepTime,
|
|
|
|
earliest: true,
|
|
|
|
allowedSleepDays: allowedSleepDays,
|
|
|
|
),
|
|
|
|
earliest: true,
|
|
|
|
);
|
|
|
|
return bedtime == null
|
|
|
|
? null
|
|
|
|
: EarliestSleepInfo(
|
|
|
|
timeKey: dateKey(item.day),
|
|
|
|
asleepTime: item.asleepTime,
|
|
|
|
timeKey: dateKey(bedtime.sleepDay),
|
|
|
|
asleepTime: bedtime.timestamp,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -878,45 +900,130 @@ class LocalHealthDataConvert { |
|
|
|
}
|
|
|
|
|
|
|
|
static EarliestSleepInfo? _earliestSleepResultInfo(
|
|
|
|
List<HealthRawSleepResult> sleepResults,
|
|
|
|
) {
|
|
|
|
final sorted = [...sleepResults]
|
|
|
|
..sort((a, b) => a.startDate.compareTo(b.startDate));
|
|
|
|
final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
if (item == null) return null;
|
|
|
|
List<HealthRawSleepResult> sleepResults, {
|
|
|
|
required Iterable<DateTime> allowedSleepDays,
|
|
|
|
}) {
|
|
|
|
final bedtime = _extremeBedtime(
|
|
|
|
_bedtimesBySleepDay(
|
|
|
|
sleepResults,
|
|
|
|
(item) => item.startDate,
|
|
|
|
earliest: true,
|
|
|
|
allowedSleepDays: allowedSleepDays,
|
|
|
|
),
|
|
|
|
earliest: true,
|
|
|
|
);
|
|
|
|
if (bedtime == null) return null;
|
|
|
|
return EarliestSleepInfo(
|
|
|
|
timeKey: dateKey(dayOf(item.date)),
|
|
|
|
asleepTime: item.startDate,
|
|
|
|
timeKey: dateKey(bedtime.sleepDay),
|
|
|
|
asleepTime: bedtime.timestamp,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static LatestSleepInfo? _latestSleepResultInfo(
|
|
|
|
List<HealthRawSleepResult> sleepResults,
|
|
|
|
) {
|
|
|
|
final sorted = [...sleepResults]
|
|
|
|
..sort((a, b) => b.startDate.compareTo(a.startDate));
|
|
|
|
final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
if (item == null) return null;
|
|
|
|
List<HealthRawSleepResult> sleepResults, {
|
|
|
|
required Iterable<DateTime> allowedSleepDays,
|
|
|
|
}) {
|
|
|
|
final bedtime = _extremeBedtime(
|
|
|
|
_bedtimesBySleepDay(
|
|
|
|
sleepResults,
|
|
|
|
(item) => item.startDate,
|
|
|
|
earliest: false,
|
|
|
|
allowedSleepDays: allowedSleepDays,
|
|
|
|
),
|
|
|
|
earliest: false,
|
|
|
|
);
|
|
|
|
if (bedtime == null) return null;
|
|
|
|
return LatestSleepInfo(
|
|
|
|
timeKey: dateKey(dayOf(item.date)),
|
|
|
|
asleepTime: item.startDate,
|
|
|
|
timeKey: dateKey(bedtime.sleepDay),
|
|
|
|
asleepTime: bedtime.timestamp,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static LatestSleepInfo? _latestSleepInfo(
|
|
|
|
List<HealthSleepDayCalculation> daily,
|
|
|
|
) {
|
|
|
|
final sorted = daily.where((e) => e.asleepTime != null).toList()
|
|
|
|
..sort((a, b) => b.asleepTime!.compareTo(a.asleepTime!));
|
|
|
|
final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
return item == null
|
|
|
|
List<HealthSleepDayCalculation> daily, {
|
|
|
|
required Iterable<DateTime> allowedSleepDays,
|
|
|
|
}) {
|
|
|
|
final bedtime = _extremeBedtime(
|
|
|
|
_bedtimesBySleepDay(
|
|
|
|
daily,
|
|
|
|
(item) => item.asleepTime,
|
|
|
|
earliest: false,
|
|
|
|
allowedSleepDays: allowedSleepDays,
|
|
|
|
),
|
|
|
|
earliest: false,
|
|
|
|
);
|
|
|
|
return bedtime == null
|
|
|
|
? null
|
|
|
|
: LatestSleepInfo(
|
|
|
|
timeKey: dateKey(item.day),
|
|
|
|
asleepTime: item.asleepTime,
|
|
|
|
timeKey: dateKey(bedtime.sleepDay),
|
|
|
|
asleepTime: bedtime.timestamp,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static List<_SleepBedtime> _bedtimesBySleepDay<T>(
|
|
|
|
Iterable<T> records,
|
|
|
|
int? Function(T record) timestampOf, {
|
|
|
|
required bool earliest,
|
|
|
|
required Iterable<DateTime> allowedSleepDays,
|
|
|
|
}) {
|
|
|
|
final allowedSleepDayKeys = {
|
|
|
|
for (final day in allowedSleepDays) dateKey(day),
|
|
|
|
};
|
|
|
|
final bedtimesBySleepDay = <int, _SleepBedtime>{};
|
|
|
|
for (final record in records) {
|
|
|
|
final timestamp = timestampOf(record);
|
|
|
|
if (timestamp == null) continue;
|
|
|
|
final bedtime = _SleepBedtime(
|
|
|
|
timestamp: timestamp,
|
|
|
|
sleepDay: _sleepDayOf(timestamp),
|
|
|
|
normalizedMinutes: _normalizedBedtimeMinutes(timestamp),
|
|
|
|
);
|
|
|
|
final key = dateKey(bedtime.sleepDay);
|
|
|
|
if (!allowedSleepDayKeys.contains(key)) continue;
|
|
|
|
final existing = bedtimesBySleepDay[key];
|
|
|
|
if (existing == null ||
|
|
|
|
(earliest
|
|
|
|
? bedtime.timestamp < existing.timestamp
|
|
|
|
: bedtime.timestamp > existing.timestamp)) {
|
|
|
|
bedtimesBySleepDay[key] = bedtime;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return bedtimesBySleepDay.values.toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
static _SleepBedtime? _extremeBedtime(
|
|
|
|
Iterable<_SleepBedtime> bedtimes, {
|
|
|
|
required bool earliest,
|
|
|
|
}) {
|
|
|
|
_SleepBedtime? selected;
|
|
|
|
for (final bedtime in bedtimes) {
|
|
|
|
if (selected == null ||
|
|
|
|
(earliest
|
|
|
|
? bedtime.normalizedMinutes < selected.normalizedMinutes
|
|
|
|
: bedtime.normalizedMinutes > selected.normalizedMinutes)) {
|
|
|
|
selected = bedtime;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return selected;
|
|
|
|
}
|
|
|
|
|
|
|
|
static DateTime _sleepDayOf(int timestamp) {
|
|
|
|
final time = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
|
|
|
final day = DateTime(time.year, time.month, time.day);
|
|
|
|
return time.hour * 60 + time.minute < _sleepDayCutoffMinutes
|
|
|
|
? day.subtract(const Duration(days: 1))
|
|
|
|
: day;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Maps 12:00–23:59 to 0–719 and 00:00–11:59 to 720–1439.
|
|
|
|
static int _normalizedBedtimeMinutes(int timestamp) {
|
|
|
|
final time = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
|
|
|
final minutes = time.hour * 60 + time.minute;
|
|
|
|
return minutes < _sleepDayCutoffMinutes
|
|
|
|
? minutes + _minutesPerDay - _sleepDayCutoffMinutes
|
|
|
|
: minutes - _sleepDayCutoffMinutes;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int? _modeState(Iterable<int> states) {
|
|
|
|
final counts = <int, int>{};
|
|
|
|
for (final state in states) {
|
|
...
|
...
|
@@ -1057,3 +1164,15 @@ class _HrvDaySummary { |
|
|
|
|
|
|
|
bool get hasData => hrvAverage != null || hrAverage != null || state != null;
|
|
|
|
}
|
|
|
|
|
|
|
|
class _SleepBedtime {
|
|
|
|
const _SleepBedtime({
|
|
|
|
required this.timestamp,
|
|
|
|
required this.sleepDay,
|
|
|
|
required this.normalizedMinutes,
|
|
|
|
});
|
|
|
|
|
|
|
|
final int timestamp;
|
|
|
|
final DateTime sleepDay;
|
|
|
|
final int normalizedMinutes;
|
|
|
|
} |
...
|
...
|
|