|
@@ -13,6 +13,9 @@ import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; |
|
@@ -13,6 +13,9 @@ import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; |
|
13
|
class LocalHealthDataConvert {
|
13
|
class LocalHealthDataConvert {
|
|
14
|
const LocalHealthDataConvert._();
|
14
|
const LocalHealthDataConvert._();
|
|
15
|
|
15
|
|
|
|
|
16
|
+ static const _sleepDayCutoffMinutes = 12 * 60;
|
|
|
|
17
|
+ static const _minutesPerDay = 24 * 60;
|
|
|
|
18
|
+
|
|
16
|
static List<DateTime> rangeDays(int dateRangeType, int startDate) {
|
19
|
static List<DateTime> rangeDays(int dateRangeType, int startDate) {
|
|
17
|
final start = dateFromKey(startDate);
|
20
|
final start = dateFromKey(startDate);
|
|
18
|
final count = switch (dateRangeType) {
|
21
|
final count = switch (dateRangeType) {
|
|
@@ -120,8 +123,14 @@ class LocalHealthDataConvert { |
|
@@ -120,8 +123,14 @@ class LocalHealthDataConvert { |
|
120
|
],
|
123
|
],
|
|
121
|
bestSleepInfo: _bestSleepInfo(validSleep),
|
124
|
bestSleepInfo: _bestSleepInfo(validSleep),
|
|
122
|
worstSleepInfo: _worstSleepInfo(validSleep),
|
125
|
worstSleepInfo: _worstSleepInfo(validSleep),
|
|
123
|
- earliestSleepInfo: _earliestSleepInfo(validSleep),
|
|
|
|
124
|
- latestSleepInfo: _latestSleepInfo(validSleep),
|
126
|
+ earliestSleepInfo: _earliestSleepInfo(
|
|
|
|
127
|
+ validSleep,
|
|
|
|
128
|
+ allowedSleepDays: days,
|
|
|
|
129
|
+ ),
|
|
|
|
130
|
+ latestSleepInfo: _latestSleepInfo(
|
|
|
|
131
|
+ validSleep,
|
|
|
|
132
|
+ allowedSleepDays: days,
|
|
|
|
133
|
+ ),
|
|
125
|
hrList: shouldIncludeHeartRate
|
134
|
hrList: shouldIncludeHeartRate
|
|
126
|
? [
|
135
|
? [
|
|
127
|
// SleepHrItem(time: 1784091398, value: 63),
|
136
|
// SleepHrItem(time: 1784091398, value: 63),
|
|
@@ -208,8 +217,14 @@ class LocalHealthDataConvert { |
|
@@ -208,8 +217,14 @@ class LocalHealthDataConvert { |
|
208
|
],
|
217
|
],
|
|
209
|
bestSleepInfo: _bestSleepResultInfo(validSleep, best: true),
|
218
|
bestSleepInfo: _bestSleepResultInfo(validSleep, best: true),
|
|
210
|
worstSleepInfo: _worstSleepResultInfo(validSleep),
|
219
|
worstSleepInfo: _worstSleepResultInfo(validSleep),
|
|
211
|
- earliestSleepInfo: _earliestSleepResultInfo(validSleep),
|
|
|
|
212
|
- latestSleepInfo: _latestSleepResultInfo(validSleep),
|
220
|
+ earliestSleepInfo: _earliestSleepResultInfo(
|
|
|
|
221
|
+ sleepResults,
|
|
|
|
222
|
+ allowedSleepDays: days,
|
|
|
|
223
|
+ ),
|
|
|
|
224
|
+ latestSleepInfo: _latestSleepResultInfo(
|
|
|
|
225
|
+ sleepResults,
|
|
|
|
226
|
+ allowedSleepDays: days,
|
|
|
|
227
|
+ ),
|
|
213
|
hrList: shouldIncludeHeartRate
|
228
|
hrList: shouldIncludeHeartRate
|
|
214
|
? [
|
229
|
? [
|
|
215
|
for (final point in allSleepHr)
|
230
|
for (final point in allSleepHr)
|
|
@@ -827,16 +842,23 @@ class LocalHealthDataConvert { |
|
@@ -827,16 +842,23 @@ class LocalHealthDataConvert { |
|
827
|
}
|
842
|
}
|
|
828
|
|
843
|
|
|
829
|
static EarliestSleepInfo? _earliestSleepInfo(
|
844
|
static EarliestSleepInfo? _earliestSleepInfo(
|
|
830
|
- List<HealthSleepDayCalculation> daily,
|
|
|
|
831
|
- ) {
|
|
|
|
832
|
- final sorted = daily.where((e) => e.asleepTime != null).toList()
|
|
|
|
833
|
- ..sort((a, b) => a.asleepTime!.compareTo(b.asleepTime!));
|
|
|
|
834
|
- final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
835
|
- return item == null
|
845
|
+ List<HealthSleepDayCalculation> daily, {
|
|
|
|
846
|
+ required Iterable<DateTime> allowedSleepDays,
|
|
|
|
847
|
+ }) {
|
|
|
|
848
|
+ final bedtime = _extremeBedtime(
|
|
|
|
849
|
+ _bedtimesBySleepDay(
|
|
|
|
850
|
+ daily,
|
|
|
|
851
|
+ (item) => item.asleepTime,
|
|
|
|
852
|
+ earliest: true,
|
|
|
|
853
|
+ allowedSleepDays: allowedSleepDays,
|
|
|
|
854
|
+ ),
|
|
|
|
855
|
+ earliest: true,
|
|
|
|
856
|
+ );
|
|
|
|
857
|
+ return bedtime == null
|
|
836
|
? null
|
858
|
? null
|
|
837
|
: EarliestSleepInfo(
|
859
|
: EarliestSleepInfo(
|
|
838
|
- timeKey: dateKey(item.day),
|
|
|
|
839
|
- asleepTime: item.asleepTime,
|
860
|
+ timeKey: dateKey(bedtime.sleepDay),
|
|
|
|
861
|
+ asleepTime: bedtime.timestamp,
|
|
840
|
);
|
862
|
);
|
|
841
|
}
|
863
|
}
|
|
842
|
|
864
|
|
|
@@ -878,45 +900,130 @@ class LocalHealthDataConvert { |
|
@@ -878,45 +900,130 @@ class LocalHealthDataConvert { |
|
878
|
}
|
900
|
}
|
|
879
|
|
901
|
|
|
880
|
static EarliestSleepInfo? _earliestSleepResultInfo(
|
902
|
static EarliestSleepInfo? _earliestSleepResultInfo(
|
|
881
|
- List<HealthRawSleepResult> sleepResults,
|
|
|
|
882
|
- ) {
|
|
|
|
883
|
- final sorted = [...sleepResults]
|
|
|
|
884
|
- ..sort((a, b) => a.startDate.compareTo(b.startDate));
|
|
|
|
885
|
- final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
886
|
- if (item == null) return null;
|
903
|
+ List<HealthRawSleepResult> sleepResults, {
|
|
|
|
904
|
+ required Iterable<DateTime> allowedSleepDays,
|
|
|
|
905
|
+ }) {
|
|
|
|
906
|
+ final bedtime = _extremeBedtime(
|
|
|
|
907
|
+ _bedtimesBySleepDay(
|
|
|
|
908
|
+ sleepResults,
|
|
|
|
909
|
+ (item) => item.startDate,
|
|
|
|
910
|
+ earliest: true,
|
|
|
|
911
|
+ allowedSleepDays: allowedSleepDays,
|
|
|
|
912
|
+ ),
|
|
|
|
913
|
+ earliest: true,
|
|
|
|
914
|
+ );
|
|
|
|
915
|
+ if (bedtime == null) return null;
|
|
887
|
return EarliestSleepInfo(
|
916
|
return EarliestSleepInfo(
|
|
888
|
- timeKey: dateKey(dayOf(item.date)),
|
|
|
|
889
|
- asleepTime: item.startDate,
|
917
|
+ timeKey: dateKey(bedtime.sleepDay),
|
|
|
|
918
|
+ asleepTime: bedtime.timestamp,
|
|
890
|
);
|
919
|
);
|
|
891
|
}
|
920
|
}
|
|
892
|
|
921
|
|
|
893
|
static LatestSleepInfo? _latestSleepResultInfo(
|
922
|
static LatestSleepInfo? _latestSleepResultInfo(
|
|
894
|
- List<HealthRawSleepResult> sleepResults,
|
|
|
|
895
|
- ) {
|
|
|
|
896
|
- final sorted = [...sleepResults]
|
|
|
|
897
|
- ..sort((a, b) => b.startDate.compareTo(a.startDate));
|
|
|
|
898
|
- final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
899
|
- if (item == null) return null;
|
923
|
+ List<HealthRawSleepResult> sleepResults, {
|
|
|
|
924
|
+ required Iterable<DateTime> allowedSleepDays,
|
|
|
|
925
|
+ }) {
|
|
|
|
926
|
+ final bedtime = _extremeBedtime(
|
|
|
|
927
|
+ _bedtimesBySleepDay(
|
|
|
|
928
|
+ sleepResults,
|
|
|
|
929
|
+ (item) => item.startDate,
|
|
|
|
930
|
+ earliest: false,
|
|
|
|
931
|
+ allowedSleepDays: allowedSleepDays,
|
|
|
|
932
|
+ ),
|
|
|
|
933
|
+ earliest: false,
|
|
|
|
934
|
+ );
|
|
|
|
935
|
+ if (bedtime == null) return null;
|
|
900
|
return LatestSleepInfo(
|
936
|
return LatestSleepInfo(
|
|
901
|
- timeKey: dateKey(dayOf(item.date)),
|
|
|
|
902
|
- asleepTime: item.startDate,
|
937
|
+ timeKey: dateKey(bedtime.sleepDay),
|
|
|
|
938
|
+ asleepTime: bedtime.timestamp,
|
|
903
|
);
|
939
|
);
|
|
904
|
}
|
940
|
}
|
|
905
|
|
941
|
|
|
906
|
static LatestSleepInfo? _latestSleepInfo(
|
942
|
static LatestSleepInfo? _latestSleepInfo(
|
|
907
|
- List<HealthSleepDayCalculation> daily,
|
|
|
|
908
|
- ) {
|
|
|
|
909
|
- final sorted = daily.where((e) => e.asleepTime != null).toList()
|
|
|
|
910
|
- ..sort((a, b) => b.asleepTime!.compareTo(a.asleepTime!));
|
|
|
|
911
|
- final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
912
|
- return item == null
|
943
|
+ List<HealthSleepDayCalculation> daily, {
|
|
|
|
944
|
+ required Iterable<DateTime> allowedSleepDays,
|
|
|
|
945
|
+ }) {
|
|
|
|
946
|
+ final bedtime = _extremeBedtime(
|
|
|
|
947
|
+ _bedtimesBySleepDay(
|
|
|
|
948
|
+ daily,
|
|
|
|
949
|
+ (item) => item.asleepTime,
|
|
|
|
950
|
+ earliest: false,
|
|
|
|
951
|
+ allowedSleepDays: allowedSleepDays,
|
|
|
|
952
|
+ ),
|
|
|
|
953
|
+ earliest: false,
|
|
|
|
954
|
+ );
|
|
|
|
955
|
+ return bedtime == null
|
|
913
|
? null
|
956
|
? null
|
|
914
|
: LatestSleepInfo(
|
957
|
: LatestSleepInfo(
|
|
915
|
- timeKey: dateKey(item.day),
|
|
|
|
916
|
- asleepTime: item.asleepTime,
|
958
|
+ timeKey: dateKey(bedtime.sleepDay),
|
|
|
|
959
|
+ asleepTime: bedtime.timestamp,
|
|
917
|
);
|
960
|
);
|
|
918
|
}
|
961
|
}
|
|
919
|
|
962
|
|
|
|
|
963
|
+ static List<_SleepBedtime> _bedtimesBySleepDay<T>(
|
|
|
|
964
|
+ Iterable<T> records,
|
|
|
|
965
|
+ int? Function(T record) timestampOf, {
|
|
|
|
966
|
+ required bool earliest,
|
|
|
|
967
|
+ required Iterable<DateTime> allowedSleepDays,
|
|
|
|
968
|
+ }) {
|
|
|
|
969
|
+ final allowedSleepDayKeys = {
|
|
|
|
970
|
+ for (final day in allowedSleepDays) dateKey(day),
|
|
|
|
971
|
+ };
|
|
|
|
972
|
+ final bedtimesBySleepDay = <int, _SleepBedtime>{};
|
|
|
|
973
|
+ for (final record in records) {
|
|
|
|
974
|
+ final timestamp = timestampOf(record);
|
|
|
|
975
|
+ if (timestamp == null) continue;
|
|
|
|
976
|
+ final bedtime = _SleepBedtime(
|
|
|
|
977
|
+ timestamp: timestamp,
|
|
|
|
978
|
+ sleepDay: _sleepDayOf(timestamp),
|
|
|
|
979
|
+ normalizedMinutes: _normalizedBedtimeMinutes(timestamp),
|
|
|
|
980
|
+ );
|
|
|
|
981
|
+ final key = dateKey(bedtime.sleepDay);
|
|
|
|
982
|
+ if (!allowedSleepDayKeys.contains(key)) continue;
|
|
|
|
983
|
+ final existing = bedtimesBySleepDay[key];
|
|
|
|
984
|
+ if (existing == null ||
|
|
|
|
985
|
+ (earliest
|
|
|
|
986
|
+ ? bedtime.timestamp < existing.timestamp
|
|
|
|
987
|
+ : bedtime.timestamp > existing.timestamp)) {
|
|
|
|
988
|
+ bedtimesBySleepDay[key] = bedtime;
|
|
|
|
989
|
+ }
|
|
|
|
990
|
+ }
|
|
|
|
991
|
+ return bedtimesBySleepDay.values.toList();
|
|
|
|
992
|
+ }
|
|
|
|
993
|
+
|
|
|
|
994
|
+ static _SleepBedtime? _extremeBedtime(
|
|
|
|
995
|
+ Iterable<_SleepBedtime> bedtimes, {
|
|
|
|
996
|
+ required bool earliest,
|
|
|
|
997
|
+ }) {
|
|
|
|
998
|
+ _SleepBedtime? selected;
|
|
|
|
999
|
+ for (final bedtime in bedtimes) {
|
|
|
|
1000
|
+ if (selected == null ||
|
|
|
|
1001
|
+ (earliest
|
|
|
|
1002
|
+ ? bedtime.normalizedMinutes < selected.normalizedMinutes
|
|
|
|
1003
|
+ : bedtime.normalizedMinutes > selected.normalizedMinutes)) {
|
|
|
|
1004
|
+ selected = bedtime;
|
|
|
|
1005
|
+ }
|
|
|
|
1006
|
+ }
|
|
|
|
1007
|
+ return selected;
|
|
|
|
1008
|
+ }
|
|
|
|
1009
|
+
|
|
|
|
1010
|
+ static DateTime _sleepDayOf(int timestamp) {
|
|
|
|
1011
|
+ final time = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
|
|
|
1012
|
+ final day = DateTime(time.year, time.month, time.day);
|
|
|
|
1013
|
+ return time.hour * 60 + time.minute < _sleepDayCutoffMinutes
|
|
|
|
1014
|
+ ? day.subtract(const Duration(days: 1))
|
|
|
|
1015
|
+ : day;
|
|
|
|
1016
|
+ }
|
|
|
|
1017
|
+
|
|
|
|
1018
|
+ /// Maps 12:00–23:59 to 0–719 and 00:00–11:59 to 720–1439.
|
|
|
|
1019
|
+ static int _normalizedBedtimeMinutes(int timestamp) {
|
|
|
|
1020
|
+ final time = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
|
|
|
1021
|
+ final minutes = time.hour * 60 + time.minute;
|
|
|
|
1022
|
+ return minutes < _sleepDayCutoffMinutes
|
|
|
|
1023
|
+ ? minutes + _minutesPerDay - _sleepDayCutoffMinutes
|
|
|
|
1024
|
+ : minutes - _sleepDayCutoffMinutes;
|
|
|
|
1025
|
+ }
|
|
|
|
1026
|
+
|
|
920
|
static int? _modeState(Iterable<int> states) {
|
1027
|
static int? _modeState(Iterable<int> states) {
|
|
921
|
final counts = <int, int>{};
|
1028
|
final counts = <int, int>{};
|
|
922
|
for (final state in states) {
|
1029
|
for (final state in states) {
|
|
@@ -1057,3 +1164,15 @@ class _HrvDaySummary { |
|
@@ -1057,3 +1164,15 @@ class _HrvDaySummary { |
|
1057
|
|
1164
|
|
|
1058
|
bool get hasData => hrvAverage != null || hrAverage != null || state != null;
|
1165
|
bool get hasData => hrvAverage != null || hrAverage != null || state != null;
|
|
1059
|
}
|
1166
|
}
|
|
|
|
1167
|
+
|
|
|
|
1168
|
+class _SleepBedtime {
|
|
|
|
1169
|
+ const _SleepBedtime({
|
|
|
|
1170
|
+ required this.timestamp,
|
|
|
|
1171
|
+ required this.sleepDay,
|
|
|
|
1172
|
+ required this.normalizedMinutes,
|
|
|
|
1173
|
+ });
|
|
|
|
1174
|
+
|
|
|
|
1175
|
+ final int timestamp;
|
|
|
|
1176
|
+ final DateTime sleepDay;
|
|
|
|
1177
|
+ final int normalizedMinutes;
|
|
|
|
1178
|
+} |