Commit 08bbfa570ffc016e902c1f9e9ef7670a2f016b04

Authored by 权海
1 parent 0c4b2326

feat(ui):计算修改

... ... @@ -368,6 +368,8 @@ protocol HealthKitRawDataHostApi {
func hasHealthData(completion: @escaping (Result<Bool, Error>) -> Void)
/// 从AppleHealth 中读取原始数据
func getHealthKitRawData(dataType: Int64, startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthKitRawDataPoint], Error>) -> Void)
/// 原生数据上传
func performHealthDataUpload(completion: @escaping (Result<Bool, Error>) -> Void)
/// 从从AppleHealth中读取睡眠数据
func getHealthKitRawSleepData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthKitRawSleepDataPoint], Error>) -> Void)
/// 从从AppleHealth中读取活动统计数据
... ... @@ -420,6 +422,22 @@ class HealthKitRawDataHostApiSetup {
} else {
getHealthKitRawDataChannel.setMessageHandler(nil)
}
/// 原生数据上传
let performHealthDataUploadChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.performHealthDataUpload\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
performHealthDataUploadChannel.setMessageHandler { _, reply in
api.performHealthDataUpload { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
performHealthDataUploadChannel.setMessageHandler(nil)
}
/// 从从AppleHealth中读取睡眠数据
let getHealthKitRawSleepDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.getHealthKitRawSleepData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
... ...
... ... @@ -1224,13 +1224,10 @@ class HealthRawPointFlags {
isSleepLikely && !isWorkout && !isWorkoutRecovery;
bool get isAwakeHrBaselineEligible =>
!isSleepLikely &&
!isWorkout &&
!isWorkoutRecovery &&
!isSuspectedActivity;
!isSleepLikely && !isWorkout && !isWorkoutRecovery;
bool get isSleepHrBaselineEligible =>
isSleepLikely && !isWorkout && !isWorkoutRecovery && !isSuspectedActivity;
isSleepLikely && !isWorkout && !isWorkoutRecovery;
bool get isCurrentAwakeHrEligible => isAwakeHrBaselineEligible;
... ...
... ... @@ -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;
... ...
... ... @@ -31,10 +31,10 @@ class LocalHealthDataSource implements HealthDataSource {
);
final allDays = [...previousDays, ...days];
final queryStart = LocalHealthDataConvert.unixSeconds(
allDays.first.subtract(const Duration(hours: 12)),
allDays.first.subtract(const Duration(days: 1)),
);
final queryEnd = LocalHealthDataConvert.unixSeconds(
allDays.last.add(const Duration(days: 1, hours: 12)),
allDays.last.add(const Duration(days: 1)),
);
final sleepIntervals = await coreService.queryRawSleepIntervals(
startTime: queryStart,
... ... @@ -48,6 +48,7 @@ class LocalHealthDataSource implements HealthDataSource {
return AppSuccess(
LocalHealthDataConvert.sleepStatistics(
dateRangeType: dateRangeType,
days: days,
previousDays: previousDays,
sleepIntervals: sleepIntervals,
... ... @@ -87,10 +88,6 @@ class LocalHealthDataSource implements HealthDataSource {
startTime: queryStart,
endTime: queryEnd,
);
final sleepIntervals = await coreService.queryRawSleepIntervals(
startTime: queryStart,
endTime: queryEnd,
);
return AppSuccess(
LocalHealthDataConvert.activityBurnStatistics(
... ... @@ -98,7 +95,7 @@ class LocalHealthDataSource implements HealthDataSource {
previousDays: previousDays,
activity: activity,
heartRate: heartRate,
sleepIntervals: sleepIntervals,
sleepIntervals: [],
),
);
} catch (error) {
... ... @@ -188,10 +185,10 @@ class LocalHealthDataSource implements HealthDataSource {
) -
1;
final sleepStart = LocalHealthDataConvert.unixSeconds(
day.subtract(const Duration(hours: 12)),
day.subtract(const Duration(days: 1)),
);
final sleepEnd = LocalHealthDataConvert.unixSeconds(
day.add(const Duration(hours: 12)),
day.add(const Duration(days: 1)),
);
final hrvPoints = await coreService.queryHrvStressPoints(
startTime: dayStart,
... ... @@ -201,10 +198,10 @@ class LocalHealthDataSource implements HealthDataSource {
startTime: dayStart,
endTime: dayEnd,
);
final heartRate = await coreService.queryRawDataPoints(
final sleepingHeartRate = await coreService.queryRawDataPoints(
dataType: HealthDataUploadType.heartRate.type,
startTime: sleepStart,
endTime: dayEnd,
endTime: sleepEnd,
);
final restingHeartRate = await coreService.queryRawDataPoints(
dataType: HealthDataUploadType.restingHeartRate.type,
... ... @@ -220,7 +217,7 @@ class LocalHealthDataSource implements HealthDataSource {
day: day,
hrvPoints: hrvPoints,
activity: activity,
heartRate: heartRate,
sleepingHeartRate: sleepingHeartRate,
restingHeartRate: restingHeartRate,
sleepIntervals: sleepIntervals,
),
... ...
... ... @@ -438,6 +438,35 @@ class HealthKitRawDataHostApi {
}
}
/// 原生数据上传
Future<bool> performHealthDataUpload() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.performHealthDataUpload$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
/// 从从AppleHealth中读取睡眠数据
Future<List<HealthKitRawSleepDataPoint>> getHealthKitRawSleepData(int startTime, int endTime) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.getHealthKitRawSleepData$pigeonVar_messageChannelSuffix';
... ...
... ... @@ -98,6 +98,10 @@ abstract class HealthKitRawDataHostApi {
List<HealthKitRawDataPoint> getHealthKitRawData(
int dataType, int startTime, int endTime);
/// 原生数据上传
@async
bool performHealthDataUpload();
/// 从从AppleHealth中读取睡眠数据
@async
List<HealthKitRawSleepDataPoint> getHealthKitRawSleepData(
... ...