Commit 8250ab7087e609cb7ca45588cdfc74e9420f6514

Authored by 权海
1 parent c159902a

feat(ui):只要能拿到数据也开始计算趋势

... ... @@ -996,7 +996,15 @@ class AppleHealthRawDataCoreService {
Future<bool> _hasHealthReadAuthorization() async {
final authorization = await _healthApi.checkHealthAppAuthorization();
return authorization.status == 1;
if (authorization.status == 1) {
return true;
}
try {
return await _rawDataApi.hasHealthData();
} catch (error, stackTrace) {
_logError('check health data availability failed', error, stackTrace);
return false;
}
}
Stream<HealthKitRawDataPoint> streamRawData({
... ...
... ... @@ -391,9 +391,10 @@ void main() {
expect(daily.single.uploaded, isTrue);
});
test('startCoreCaculate skips raw reads when health auth is missing',
test(
'startCoreCaculate skips raw reads when health auth and local data are missing',
() async {
final api = _FakeHealthKitRawDataHostApi();
final api = _FakeHealthKitRawDataHostApi()..hasData = false;
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(status: 2),
rawDataApi: api,
... ... @@ -411,6 +412,79 @@ void main() {
expect(api.workoutCallCount, 0);
});
test(
'startCoreCaculate reads raw data when health auth is missing but data exists',
() async {
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final api = _FakeHealthKitRawDataHostApi()..hasData = true;
api.setPoints(HealthDataUploadType.hrv.type, [
_point(now - 120, 35),
]);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(status: 2),
rawDataApi: api,
localStore: _MemoryHealthRawStressLocalStore(),
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
);
final result = await service.startCoreCaculate(
endTime: now,
readChunkDays: 1,
);
expect(result.hrvStressPoints.map((e) => e.rawEndTime), [now - 120]);
expect(api.calls, isNotEmpty);
expect(api.hasHealthDataCallCount, 1);
});
test('startCoreCaculate skips raw reads when local data check fails',
() async {
final api = _FakeHealthKitRawDataHostApi()
..hasDataError = StateError('health data unavailable');
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(status: 2),
rawDataApi: api,
localStore: _MemoryHealthRawStressLocalStore(),
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
);
final result = await service.startCoreCaculate(readChunkDays: 1);
expect(result.hrvStressPoints, isEmpty);
expect(result.realtimeStressPoints, isEmpty);
expect(api.calls, isEmpty);
expect(api.hasHealthDataCallCount, 1);
});
test(
'startCoreCaculate does not call local data fallback when auth is granted',
() async {
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final api = _FakeHealthKitRawDataHostApi()
..hasData = false
..hasDataError = StateError('should not call hasHealthData');
api.setPoints(HealthDataUploadType.hrv.type, [
_point(now - 120, 35),
]);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(status: 1),
rawDataApi: api,
localStore: _MemoryHealthRawStressLocalStore(),
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
);
final result = await service.startCoreCaculate(
endTime: now,
readChunkDays: 1,
);
expect(result.hrvStressPoints.map((e) => e.rawEndTime), [now - 120]);
expect(api.hasHealthDataCallCount, 0);
});
test('onHealthDataUpdated emits event with data types after calculation',
() async {
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
... ... @@ -943,6 +1017,8 @@ class _FakeHealthKitRawDataHostApi extends HealthKitRawDataHostApi {
Completer<int>? hrUploadCompleter;
Completer<int>? hrvUploadCompleter;
var hasData = true;
Object? hasDataError;
var hasHealthDataCallCount = 0;
void setPoints(int dataType, List<HealthKitRawDataPoint> points) {
_pointsByType[dataType] = points;
... ... @@ -965,7 +1041,14 @@ class _FakeHealthKitRawDataHostApi extends HealthKitRawDataHostApi {
}
@override
Future<bool> hasHealthData() async => hasData;
Future<bool> hasHealthData() async {
hasHealthDataCallCount += 1;
final error = hasDataError;
if (error != null) {
throw error;
}
return hasData;
}
@override
Future<List<HealthKitRawDataPoint>> getHealthKitRawData(
... ...
import 'dart:convert';
import 'dart:io';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_stress_calculator.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('generate hrv trend results from exported raw health rows', () async {
final jsonPath = Platform.environment['HEALTH_RAW_ROWS_JSON'];
if (jsonPath == null || jsonPath.isEmpty) {
markTestSkipped(
'Set HEALTH_RAW_ROWS_JSON to a JSON export of the raw health rows.',
);
return;
}
final hrvValueDivisor = double.tryParse(
Platform.environment['HRV_VALUE_DIVISOR'] ?? '',
) ??
1;
final rows = (jsonDecode(await File(jsonPath).readAsString()) as List)
.cast<Map<String, Object?>>();
final userId = (rows.firstOrNull?['user_id'] as num?)?.toInt() ?? 0;
final hrvPoints = <HealthKitRawDataPoint>[];
final heartRatePoints = <HealthKitRawDataPoint>[];
final restingHeartRatePoints = <HealthKitRawDataPoint>[];
for (final row in rows) {
final dataType = (row['data_type'] as num).toInt();
final dataTime = (row['data_time'] as num).toInt();
final rawValue = (row['value'] as num).toDouble();
final value = dataType == 1 ? rawValue / hrvValueDivisor : rawValue;
final point = HealthKitRawDataPoint(
dataType: dataType,
startTime: dataTime,
endTime: dataTime,
value: value,
isMotionLike: false,
);
switch (dataType) {
case 1:
hrvPoints.add(point);
break;
case 2:
heartRatePoints.add(point);
break;
case 9:
restingHeartRatePoints.add(point);
break;
}
}
final times = rows.map((row) => (row['data_time'] as num).toInt()).toList()
..sort();
final result = HealthRawStressCalculator(userId: userId).calculate(
hrvPoints: hrvPoints,
heartRatePoints: heartRatePoints,
restingHeartRatePoints: restingHeartRatePoints,
startTime: times.first,
endTime: times.last,
);
debugPrint('raw rows: ${rows.length}');
debugPrint('user_id: $userId');
debugPrint('hrv raw points: ${hrvPoints.length}');
debugPrint('heart rate raw points: ${heartRatePoints.length}');
debugPrint(
'resting heart rate raw points: ${restingHeartRatePoints.length}');
debugPrint('hrv value divisor: $hrvValueDivisor');
debugPrint('hrv trend result count: ${result.hrvStressPoints.length}');
debugPrint(jsonEncode(result.hrvStressPoints.map(_hrvResultJson).toList()));
expect(result.hrvStressPoints, isNotNull);
});
}
Map<String, Object?> _hrvResultJson(HealthRawHrvStressPoint point) {
return <String, Object?>{
'raw_end_time': point.rawEndTime,
'raw_end_time_text': DateTime.fromMillisecondsSinceEpoch(
point.rawEndTime * 1000,
).toIso8601String(),
'raw_hrv': point.rawHrv,
'result': point.result,
'state': point.state.name,
'state_value': point.state.value,
'baseline_hrv': point.baselineHrv,
'baseline_awake_hrv': point.baselineAwakeHrv,
'baseline_sleep_hrv': point.baselineSleepHrv,
'baseline_resting_hr': point.baselineRestingHr,
'source_start_time': point.sourceStartTime,
'source_end_time': point.sourceEndTime,
};
}
... ...