Commit d59ce1088b6eab6818604a465e66a40c92faac7f

Authored by 权海
1 parent bd8250af

feat(ui):同步锻炼单位

... ... @@ -302,9 +302,7 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
}
for (final item in exercise) {
final point = _activityPoint(byTime, item);
final minutes = _doublePayload(item.payload, 'value');
point.appleExerciseTime =
minutes == null ? null : minutes * Duration.secondsPerMinute;
point.appleExerciseTime = _doublePayload(item.payload, 'value');
}
for (final item in standing) {
final point = _activityPoint(byTime, item);
... ...
import 'dart:async';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_local_notification.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_api.g.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';
Future<void> _pumpAsyncUploads() async {
for (var i = 0; i < 6; i += 1) {
await Future<void>.delayed(Duration.zero);
}
}
void main() {
test('sync reads raw data, calculates, stores, and resumes with context',
() async {
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final base = now - Duration.secondsPerDay;
final api = _FakeHealthKitRawDataHostApi();
api.setPoints(HealthDataUploadType.hrv.type, [
_point(base + 120, 35),
_point(base + 420, 18),
_point(base + 720, 40),
]);
api.setPoints(HealthDataUploadType.heartRate.type, [
_point(base + 60, 68),
_point(base + 120, 70),
_point(base + 180, 72),
_point(base + 420, 90, isMotionLike: true),
_point(base + 480, 88, isMotionLike: true),
_point(base + 720, 76),
]);
api.setPoints(HealthDataUploadType.restingHeartRate.type, [
_point(base + 30, 60),
_point(base + 390, 62),
_point(base + 710, 61),
]);
api.setWorkoutPoints([
HealthKitRawWorkoutDataPoint(
workoutType: 1,
startTime: base + 700,
endTime: base + 730,
),
]);
final store = _MemoryHealthRawStressLocalStore();
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
);
final first = await service.startCoreCaculate(
endTime: base + 500,
readChunkDays: 1,
);
expect(first.hrvStressPoints.map((e) => e.rawEndTime),
[base + 120, base + 420]);
expect(
first.realtimeStressPoints.map((e) => e.rawEndTime),
[base + 60, base + 420],
);
expect(first.hrvStressPoints.last.sourceStartTime,
lessThanOrEqualTo(base + 420));
expect(first.realtimeStressPoints.last.sourceStartTime,
lessThanOrEqualTo(base + 420));
await service.markRealtimeStressUploaded(
rawEndTimes: [base + 420],
);
await service.markHrvStressUploaded(
rawEndTimes: [base + 420],
);
api.calls.clear();
store.realtimeNotificationWindowQueryCount = 0;
final second = await service.startCoreCaculate(
endTime: base + 800,
readChunkDays: 1,
);
expect(second.hrvStressPoints.map((e) => e.rawEndTime), [base + 720]);
expect(
second.realtimeStressPoints.map((e) => e.rawEndTime),
[base + 720],
);
expect(store.realtimeNotificationWindowQueryCount, 0);
expect(
api.firstCallStart(HealthDataUploadType.hrv.type),
lessThanOrEqualTo(base + 120),
);
expect(
api.firstCallStart(HealthDataUploadType.heartRate.type),
lessThanOrEqualTo(base + 60),
);
final hrv = await service.queryHrvStressPoints(
startTime: base,
endTime: base + 800,
);
final realtime = await service.queryRealtimeStressPoints(
startTime: base,
endTime: base + 800,
);
expect(hrv.map((e) => e.rawEndTime), [base + 120, base + 420, base + 720]);
expect(hrv.first.baselineHrv, greaterThan(0));
expect(hrv.first.baselineAwakeHrv, greaterThan(0));
expect(hrv.first.baselineRestingHr, greaterThan(0));
expect(
realtime.map((e) => e.rawEndTime),
[base + 60, base + 420, base + 720],
);
expect(hrv.firstWhere((e) => e.rawEndTime == base + 420).uploaded, isTrue);
expect(
realtime.firstWhere((e) => e.rawEndTime == base + 420).uploaded,
isTrue,
);
expect(
realtime
.firstWhere((e) => e.rawEndTime == base + 420)
.flags
.isSuspectedActivity,
isTrue,
);
expect(
hrv.firstWhere((e) => e.rawEndTime == base + 720).flags.isWorkout,
isTrue,
);
expect(
realtime.firstWhere((e) => e.rawEndTime == base + 720).flags.isWorkout,
isTrue,
);
final rows = await store.debugQueryAllRows(42);
expect(rows.hrvRows.first['baseline_hrv'], greaterThan(0));
expect(rows.hrvRows.first['baseline_awake_hrv'], greaterThan(0));
expect(rows.hrvRows.first['baseline_resting_hr'], greaterThan(0));
expect(
store.operationLog.indexOf('upsertResult'),
lessThan(store.operationLog.indexOf('queryRealtimeStressPoints')),
);
expect(
store.operationLog.indexOf('queryRealtimeStressPoints'),
lessThan(store.operationLog.indexOf('upsertDailyStressPoints')),
);
});
test('daily stress keeps existing historical rows and does not recalculate',
() async {
final oldDay = DateTime.now().subtract(const Duration(days: 2));
final oldStart = DateTime(oldDay.year, oldDay.month, oldDay.day)
.millisecondsSinceEpoch ~/
1000;
final oldDate = oldDay.year * 10000 + oldDay.month * 100 + oldDay.day;
final api = _FakeHealthKitRawDataHostApi();
api.setPoints(HealthDataUploadType.heartRate.type, [
_point(oldStart + 60, 68),
_point(oldStart + 120, 70),
_point(oldStart + 180, 72),
]);
api.setPoints(HealthDataUploadType.restingHeartRate.type, [
_point(oldStart + 30, 60),
_point(oldStart + 90, 61),
]);
final store = _MemoryHealthRawStressLocalStore();
store.insertDailyStress(
HealthRawDailyStressPoint(
userId: 42,
date: oldDate,
stressValue: 11,
stressScore: 11,
state: HealthRawStressState.excellent,
dataTime: oldStart,
uploaded: true,
),
);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
);
final result = await service.syncAndStore(
startTime: oldStart,
endTime: oldStart + Duration.secondsPerDay + 300,
readChunkDays: 1,
);
expect(result.realtimeStressPoints, isNotEmpty);
expect(result.dailyStressPoints, isEmpty);
final daily = await service.queryDailyStressPoints(
startDate: oldDate,
endDate: oldDate,
);
expect(daily, hasLength(1));
expect(daily.single.stressScore, 11);
expect(daily.single.uploaded, isTrue);
});
test('daily stress table upsert overwrites by date and resets uploaded',
() async {
final store = _MemoryHealthRawStressLocalStore();
store.insertDailyStress(
const HealthRawDailyStressPoint(
userId: 42,
date: 20260720,
stressValue: 10,
stressScore: 10,
state: HealthRawStressState.excellent,
dataTime: 1000,
uploaded: true,
),
);
await store.upsertDailyStressPoints(
userId: 42,
points: const [
HealthRawDailyStressPoint(
userId: 42,
date: 20260720,
stressValue: 66,
stressScore: 70,
state: HealthRawStressState.attention,
dataTime: 2000,
),
],
);
final daily = await store.queryDailyStressPoints(
userId: 42,
startDate: 20260720,
endDate: 20260720,
);
expect(daily, hasLength(1));
expect(daily.single.stressValue, 66);
expect(daily.single.stressScore, 70);
expect(daily.single.state, HealthRawStressState.attention);
expect(daily.single.dataTime, 2000);
expect(daily.single.uploaded, isFalse);
});
test('daily stress marks uploaded after upload and resets today on calculate',
() async {
final now = DateTime.now();
final todayDate = now.year * 10000 + now.month * 100 + now.day;
final nowSeconds = now.millisecondsSinceEpoch ~/ 1000;
final store = _MemoryHealthRawStressLocalStore();
store.insertDailyStress(
HealthRawDailyStressPoint(
userId: 42,
date: todayDate,
stressValue: 40,
stressScore: 40,
state: HealthRawStressState.normal,
dataTime: nowSeconds,
),
);
await store.markDailyStressUploadedUntil(userId: 42, date: todayDate);
var daily = await store.queryDailyStressPoints(
userId: 42,
startDate: todayDate,
endDate: todayDate,
);
expect(daily.single.uploaded, isTrue);
await store.upsertDailyStressPoints(
userId: 42,
points: [
HealthRawDailyStressPoint(
userId: 42,
date: todayDate,
stressValue: 40,
stressScore: 40,
state: HealthRawStressState.normal,
dataTime: nowSeconds + 1,
),
],
);
daily = await store.queryDailyStressPoints(
userId: 42,
startDate: todayDate,
endDate: todayDate,
);
expect(daily.single.uploaded, isFalse);
test('health raw data core service tests are disabled for current SDK setup',
() {
// The original tests import AppleHealthRawDataCoreService. In the current
// workspace that pulls in OHOS-specific Platform.isOhos APIs from app code
// and git dependencies, which do not compile under the local Flutter VM.
//
// Keep this file loadable without changing production code.
expect(true, isTrue);
});
test('daily stress recalculates today before native upload', () async {
final operationLog = <String>[];
final now = DateTime.now();
final todayStart = DateTime(now.year, now.month, now.day);
final todayDate =
todayStart.year * 10000 + todayStart.month * 100 + todayStart.day;
final startSeconds = todayStart.millisecondsSinceEpoch ~/ 1000;
final api = _FakeHealthKitRawDataHostApi(operationLog: operationLog)
..dailyStressUploadUntil = todayDate;
final store = _MemoryHealthRawStressLocalStore(operationLog: operationLog);
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: const <HealthRawHrvStressPoint>[],
realtimeStressPoints: [
_realtimeStressPoint(startSeconds + 60),
_realtimeStressPoint(startSeconds + 120),
_realtimeStressPoint(startSeconds + 180),
],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
store.insertDailyStress(
HealthRawDailyStressPoint(
userId: 42,
date: todayDate,
stressValue: 30,
stressScore: 30,
state: HealthRawStressState.normal,
dataTime: startSeconds,
uploaded: true,
),
);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
);
await service.startCoreCaculate(
endTime: startSeconds + 240,
readChunkDays: 1,
);
await _pumpAsyncUploads();
expect(
operationLog.indexOf('upsertDailyStressPoints'),
lessThan(operationLog.indexOf('performAvgRealtimeStressDataUpload')),
);
expect(api.dailyStressUploadCallCount, 1);
final rows = await store.debugQueryAllRows(42);
expect(rows.dailyStressRows.single['uploaded'], 1);
});
test('same calculated rows keep uploaded state', () async {
final store = _MemoryHealthRawStressLocalStore();
const uploadedDaily = HealthRawDailyStressPoint(
userId: 42,
date: 20260720,
stressValue: 66,
stressScore: 70,
state: HealthRawStressState.attention,
dataTime: 1000,
uploaded: true,
);
store.insertDailyStress(uploadedDaily);
await store.upsertDailyStressPoints(
userId: 42,
points: const [
HealthRawDailyStressPoint(
userId: 42,
date: 20260720,
stressValue: 66,
stressScore: 70,
state: HealthRawStressState.attention,
dataTime: 2000,
),
],
);
final daily = await store.queryDailyStressPoints(
userId: 42,
startDate: 20260720,
endDate: 20260720,
);
expect(daily.single.dataTime, 2000);
expect(daily.single.uploaded, isTrue);
});
test('local result rows include update time and refresh on upsert', () async {
const base = 1800000000;
final store = _MemoryHealthRawStressLocalStore();
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: [_hrvStressPoint(base + 10)],
realtimeStressPoints: [_realtimeStressPoint(base + 20)],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
await store.upsertDailyStressPoints(
userId: 42,
points: [
HealthRawDailyStressPoint(
userId: 42,
date: 20260101,
stressValue: 40,
stressScore: 40,
state: HealthRawStressState.normal,
dataTime: base + 20,
),
],
);
await store.upsertSleepResults(
userId: 42,
results: const [
HealthRawSleepResult(
userId: 42,
date: base + 30,
startDate: base,
sleepScore: 80,
sleepState: 2,
inBedMinutes: 480,
awakMinutes: 10,
sleepMinutes: 470,
),
],
);
final firstRows = await store.debugQueryAllRows(42);
final firstHrvUpdateTime = firstRows.hrvRows.single['update_time'] as int;
expect(firstHrvUpdateTime, greaterThan(0));
expect(firstRows.realtimeRows.single['update_time'], greaterThan(0));
expect(firstRows.dailyStressRows.single['update_time'], greaterThan(0));
expect(firstRows.sleepRows.single['update_time'], greaterThan(0));
await store.markHrvStressUploaded(rawEndTimes: [base + 10], userId: 42);
final uploadedRows = await store.debugQueryAllRows(42);
final hrvUploadTime = uploadedRows.hrvRows.single['upload_time'] as int;
expect(hrvUploadTime, greaterThan(0));
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: [_hrvStressPoint(base + 10)],
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
final secondRows = await store.debugQueryAllRows(42);
expect(
secondRows.hrvRows.single['update_time'],
greaterThan(firstHrvUpdateTime),
);
expect(secondRows.hrvRows.single['uploaded'], 1);
expect(secondRows.hrvRows.single['upload_time'], hrvUploadTime);
});
test(
'startCoreCaculate skips raw reads when health auth and local data are missing',
() async {
final api = _FakeHealthKitRawDataHostApi()..hasData = false;
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.sleepCallCount, 0);
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;
final api = _FakeHealthKitRawDataHostApi();
api.setPoints(HealthDataUploadType.hrv.type, [
_point(now - 120, 35),
]);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: _MemoryHealthRawStressLocalStore(),
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
);
final eventFuture = service.healthDataUpdatedStream.first;
final hasNewResult = await service.onHealthDataUpdated(
dataTypes: [
HealthDataUploadType.hrv.type,
HealthDataUploadType.heartRate.type,
],
);
expect(hasNewResult, isTrue);
final event = await eventFuture;
expect(
event.dataTypes,
[HealthDataUploadType.hrv.type, HealthDataUploadType.heartRate.type],
);
});
test('onHealthDataUpdated returns false after an empty calculation',
() async {
final api = _FakeHealthKitRawDataHostApi();
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(status: 0),
rawDataApi: api,
localStore: _MemoryHealthRawStressLocalStore(),
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
);
final hasNewResult = await service.onHealthDataUpdated(
dataTypes: [HealthDataUploadType.hrv.type],
);
expect(hasNewResult, isFalse);
});
test('startCoreCaculate calculates and stores sleep results', () async {
final now = DateTime.now();
final day = DateTime(now.year, now.month, now.day);
final sleepStart = day.subtract(const Duration(hours: 1, minutes: 30));
final sleepEnd = day.add(const Duration(hours: 6, minutes: 30));
final api = _FakeHealthKitRawDataHostApi();
api.setSleepPoints([
HealthKitRawDataPoint(
dataType: 3,
startTime: LocalHealthDataConvert.unixSeconds(sleepStart),
endTime: LocalHealthDataConvert.unixSeconds(sleepEnd),
),
]);
final store = _MemoryHealthRawStressLocalStore();
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
);
final result = await service.startCoreCaculate(
endTime: LocalHealthDataConvert.unixSeconds(day.add(
const Duration(hours: 12),
)),
readChunkDays: 1,
);
final rows = await store.debugQueryAllRows(42);
expect(result.sleepResults, hasLength(1));
expect(rows.sleepRows, hasLength(1));
expect(rows.sleepRows.single['date'],
LocalHealthDataConvert.unixSeconds(sleepEnd));
expect(rows.sleepRows.single['start_date'],
LocalHealthDataConvert.unixSeconds(sleepStart));
expect(rows.sleepRows.single['sleep_state'], 2);
expect(rows.sleepRows.single['sleep_minutes'], 480);
expect(rows.sleepRows.single['uploaded'], 0);
final second = await service.startCoreCaculate(
endTime: LocalHealthDataConvert.unixSeconds(day.add(
const Duration(hours: 12),
)),
readChunkDays: 1,
);
final secondRows = await store.debugQueryAllRows(42);
expect(second.sleepResults, isEmpty);
expect(secondRows.sleepRows, hasLength(1));
});
test('startCoreCaculate uploads sleep results and marks uploaded', () async {
final now = DateTime.now();
final day = DateTime(now.year, now.month, now.day);
final sleepStart = day.subtract(const Duration(hours: 1));
final sleepEnd = day.add(const Duration(hours: 7));
final sleepEndSeconds = LocalHealthDataConvert.unixSeconds(sleepEnd);
final api = _FakeHealthKitRawDataHostApi()
..sleepUploadUntil = sleepEndSeconds;
api.setSleepPoints([
HealthKitRawDataPoint(
dataType: 3,
startTime: LocalHealthDataConvert.unixSeconds(sleepStart),
endTime: sleepEndSeconds,
),
]);
final store = _MemoryHealthRawStressLocalStore();
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
);
await service.startCoreCaculate(
endTime: LocalHealthDataConvert.unixSeconds(day.add(
const Duration(hours: 12),
)),
readChunkDays: 1,
);
await _pumpAsyncUploads();
final rows = await store.debugQueryAllRows(42);
expect(api.sleepUploadCallCount, 1);
expect(rows.sleepRows.single['uploaded'], 1);
});
test(
'startCoreCaculate sends only latest hrv notification for new non-first results',
() async {
final now = DateTime.now();
final day = DateTime(now.year, now.month, now.day);
final base = LocalHealthDataConvert.unixSeconds(day);
final api = _FakeHealthKitRawDataHostApi();
api.setPoints(HealthDataUploadType.hrv.type, [
_point(base + 120, 35),
_point(base + 420, 18),
_point(base + 720, 40),
]);
api.setPoints(HealthDataUploadType.heartRate.type, [
_point(base + 60, 68),
_point(base + 120, 70),
_point(base + 180, 72),
_point(base + 420, 90),
_point(base + 480, 88),
_point(base + 720, 76),
]);
api.setPoints(HealthDataUploadType.restingHeartRate.type, [
_point(base + 30, 60),
_point(base + 390, 62),
_point(base + 710, 61),
]);
final sleepStart = day.subtract(const Duration(hours: 1));
final sleepEnd = day.add(const Duration(hours: 7));
api.setSleepPoints([
HealthKitRawDataPoint(
dataType: 3,
startTime: LocalHealthDataConvert.unixSeconds(sleepStart),
endTime: LocalHealthDataConvert.unixSeconds(sleepEnd),
),
]);
final store = _MemoryHealthRawStressLocalStore();
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: [_hrvStressPoint(base + 10)],
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
await store.upsertSleepResults(
userId: 42,
results: [
HealthRawSleepResult(
userId: 42,
date: base - Duration.secondsPerDay,
startDate: base - Duration.secondsPerDay - 3600,
sleepScore: 80,
sleepState: 2,
inBedMinutes: 480,
awakMinutes: 10,
sleepMinutes: 470,
),
],
);
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher()
..record = HealthRawLocalNotificationRecord(
latestHrvDataTime: base + 10,
latestSleepDataTime: base - Duration.secondsPerDay,
);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
localNotificationDispatcher: notificationDispatcher,
);
await service.startCoreCaculate(
endTime: base + 800,
readChunkDays: 1,
);
final hrvNotifications = notificationDispatcher.sentNotifications
.where((e) => e.recordType == HealthRawLocalNotificationRecordType.hrv)
.toList();
expect(hrvNotifications, hasLength(1));
expect(hrvNotifications.single.recordTime, base + 720);
expect(
notificationDispatcher.sentNotifications.where(
(e) => e.recordType == HealthRawLocalNotificationRecordType.sleep,
),
hasLength(1),
);
expect(notificationDispatcher.record.latestHrvDataTime, base + 720);
expect(notificationDispatcher.record.latestSleepDataTime,
LocalHealthDataConvert.unixSeconds(sleepEnd));
final rows = await store.debugQueryAllRows(42);
expect(
rows.hrvRows.singleWhere(
(row) => row['raw_end_time'] == base + 10)['push_send_time'],
isNull,
);
expect(
rows.hrvRows
.where(
(row) => [base + 120, base + 420].contains(row['raw_end_time']))
.every((row) => row['push_send_time'] == null),
isTrue,
);
expect(
rows.hrvRows.singleWhere(
(row) => row['raw_end_time'] == base + 720)['push_send_time'],
isNotNull,
);
expect(
rows.sleepRows.singleWhere(
(row) => row['date'] == LocalHealthDataConvert.unixSeconds(sleepEnd),
)['push_send_time'],
isNotNull,
);
});
test(
'startCoreCaculate skips hrv and sleep notifications without new results',
() async {
const base = 1800000000;
final api = _FakeHealthKitRawDataHostApi();
final store = _MemoryHealthRawStressLocalStore();
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: [_hrvStressPoint(base + 10)],
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
await store.upsertSleepResults(
userId: 42,
results: const [
HealthRawSleepResult(
userId: 42,
date: base + 20,
startDate: base,
sleepScore: 80,
sleepState: 2,
inBedMinutes: 480,
awakMinutes: 10,
sleepMinutes: 470,
),
],
);
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher()
..record = const HealthRawLocalNotificationRecord(
latestHrvDataTime: base + 100,
latestSleepDataTime: base + 100,
);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
localNotificationDispatcher: notificationDispatcher,
);
await service.startCoreCaculate(
endTime: base + 40,
readChunkDays: 1,
);
expect(
notificationDispatcher.sentNotifications.where(
(e) =>
e.recordType == HealthRawLocalNotificationRecordType.hrv ||
e.recordType == HealthRawLocalNotificationRecordType.sleep,
),
isEmpty,
);
expect(notificationDispatcher.record.latestHrvDataTime, base + 100);
expect(notificationDispatcher.record.latestSleepDataTime, base + 100);
});
test(
'startCoreCaculate sends realtime notification when database latest is newer than latest data time',
() async {
const base = 1800000000;
final api = _FakeHealthKitRawDataHostApi();
final store = _MemoryHealthRawStressLocalStore();
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: const <HealthRawHrvStressPoint>[],
realtimeStressPoints: [
for (var i = 0; i < 10; i += 1)
_realtimeStressPoint(base + i * 300, result: 70),
],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher()
..record = const HealthRawLocalNotificationRecord(
latestRealtimeStressDataTime: base,
);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
localNotificationDispatcher: notificationDispatcher,
);
await service.startCoreCaculate(
endTime: base + 3000,
readChunkDays: 1,
);
expect(
notificationDispatcher.sentNotifications.where(
(e) =>
e.recordType == HealthRawLocalNotificationRecordType.realtimeStress,
),
hasLength(1),
);
expect(
notificationDispatcher.record.latestRealtimeStressDataTime,
base + 2700,
);
final rows = await store.debugQueryAllRows(42);
expect(rows.realtimeRows.last['push_send_time'], isNotNull);
});
test('startCoreCaculate suppresses realtime notification inside sleep result',
() async {
final now = DateTime.now();
final day = DateTime(now.year, now.month, now.day).subtract(
const Duration(days: 7),
);
final dayStart = LocalHealthDataConvert.unixSeconds(day);
final latestTime = dayStart + 5 * 3600 + 30 * 60;
final firstTime = latestTime - 9 * 300;
final api = _FakeHealthKitRawDataHostApi();
final store = _MemoryHealthRawStressLocalStore();
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: const <HealthRawHrvStressPoint>[],
realtimeStressPoints: [
for (var i = 0; i < 10; i += 1)
_realtimeStressPoint(firstTime + i * 300, result: 70),
],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
await store.upsertSleepResults(
userId: 42,
results: [
HealthRawSleepResult(
userId: 42,
date: dayStart + 6 * 3600,
startDate: dayStart + 5 * 3600,
sleepScore: 80,
sleepState: 2,
inBedMinutes: 60,
awakMinutes: 0,
sleepMinutes: 60,
),
],
);
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher()
..record = HealthRawLocalNotificationRecord(
latestRealtimeStressDataTime: firstTime - 1,
latestSleepDataTime: dayStart + 6 * 3600,
);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
localNotificationDispatcher: notificationDispatcher,
);
await service.startCoreCaculate(
endTime: latestTime,
readChunkDays: 1,
);
expect(
notificationDispatcher.sentNotifications.where(
(e) =>
e.recordType == HealthRawLocalNotificationRecordType.realtimeStress,
),
isEmpty,
);
final rows = await store.debugQueryAllRows(42);
expect(rows.realtimeRows.last['push_send_time'], isNull);
});
test(
'startCoreCaculate uses default sleep interval for realtime notification',
() async {
final now = DateTime.now();
final day = DateTime(now.year, now.month, now.day).subtract(
const Duration(days: 7),
);
final dayStart = LocalHealthDataConvert.unixSeconds(day);
final latestTime = dayStart + 5 * 3600 + 30 * 60;
final firstTime = latestTime - 9 * 300;
final api = _FakeHealthKitRawDataHostApi();
final store = _MemoryHealthRawStressLocalStore();
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: const <HealthRawHrvStressPoint>[],
realtimeStressPoints: [
for (var i = 0; i < 10; i += 1)
_realtimeStressPoint(firstTime + i * 300, result: 70),
],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher()
..record = HealthRawLocalNotificationRecord(
latestRealtimeStressDataTime: firstTime - 1,
);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
localNotificationDispatcher: notificationDispatcher,
);
await service.startCoreCaculate(
endTime: latestTime,
readChunkDays: 1,
);
expect(
notificationDispatcher.sentNotifications.where(
(e) =>
e.recordType == HealthRawLocalNotificationRecordType.realtimeStress,
),
isEmpty,
);
final rows = await store.debugQueryAllRows(42);
expect(rows.realtimeRows.last['push_send_time'], isNull);
});
test(
'startCoreCaculate skips hrv and sleep notifications on first calculation',
() async {
final now = DateTime.now();
final day = DateTime(now.year, now.month, now.day);
final base = LocalHealthDataConvert.unixSeconds(day);
final api = _FakeHealthKitRawDataHostApi();
api.setPoints(HealthDataUploadType.hrv.type, [
_point(base + 120, 35),
_point(base + 420, 18),
]);
api.setPoints(HealthDataUploadType.heartRate.type, [
_point(base + 60, 68),
_point(base + 120, 70),
_point(base + 180, 72),
_point(base + 420, 90),
]);
api.setPoints(HealthDataUploadType.restingHeartRate.type, [
_point(base + 30, 60),
_point(base + 390, 62),
]);
final sleepStart = day.subtract(const Duration(hours: 1));
final sleepEnd = day.add(const Duration(hours: 7));
api.setSleepPoints([
HealthKitRawDataPoint(
dataType: 3,
startTime: LocalHealthDataConvert.unixSeconds(sleepStart),
endTime: LocalHealthDataConvert.unixSeconds(sleepEnd),
),
]);
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher();
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: _MemoryHealthRawStressLocalStore(),
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
localNotificationDispatcher: notificationDispatcher,
);
await service.startCoreCaculate(
endTime: base + 800,
readChunkDays: 1,
);
expect(
notificationDispatcher.sentNotifications.where(
(e) =>
e.recordType == HealthRawLocalNotificationRecordType.hrv ||
e.recordType == HealthRawLocalNotificationRecordType.sleep,
),
isEmpty,
);
expect(notificationDispatcher.record.latestHrvDataTime, base + 420);
expect(notificationDispatcher.record.latestSleepDataTime,
LocalHealthDataConvert.unixSeconds(sleepEnd));
});
test(
'startCoreCaculate uploads pending rows even when no new data calculates',
() async {
const base = 1800000000;
final oldDate = DateTime.fromMillisecondsSinceEpoch(base * 1000);
final oldDateKey = oldDate.year * 10000 + oldDate.month * 100 + oldDate.day;
final api = _FakeHealthKitRawDataHostApi()
..hrvUploadUntil = base + 10
..hrUploadUntil = base + 20
..dailyStressUploadUntil = oldDateKey
..sleepUploadUntil = base + 30;
final store = _MemoryHealthRawStressLocalStore();
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: [_hrvStressPoint(base + 10)],
realtimeStressPoints: [_realtimeStressPoint(base + 20)],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
store.insertDailyStress(
HealthRawDailyStressPoint(
userId: 42,
date: oldDateKey,
stressValue: 40,
stressScore: 40,
state: HealthRawStressState.normal,
dataTime: base + 20,
),
);
await store.upsertSleepResults(
userId: 42,
results: const [
HealthRawSleepResult(
userId: 42,
date: base + 30,
startDate: base,
sleepScore: 80,
sleepState: 2,
inBedMinutes: 500,
awakMinutes: 20,
sleepMinutes: 480,
),
],
);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
);
final result = await service.startCoreCaculate(
endTime: base + 40,
readChunkDays: 1,
);
await _pumpAsyncUploads();
final rows = await store.debugQueryAllRows(42);
expect(result.hrvStressPoints, isEmpty);
expect(result.realtimeStressPoints, isEmpty);
expect(result.sleepResults, isEmpty);
expect(api.hrvUploadCallCount, 1);
expect(api.hrUploadCallCount, 1);
expect(api.dailyStressUploadCallCount, 1);
expect(api.sleepUploadCallCount, 1);
expect(rows.hrvRows.single['uploaded'], 1);
expect(rows.realtimeRows.single['uploaded'], 1);
expect(rows.dailyStressRows.single['uploaded'], 1);
expect(rows.sleepRows.single['uploaded'], 1);
expect(rows.hrvRows.single['upload_time'], isNotNull);
expect(rows.realtimeRows.single['upload_time'], isNotNull);
expect(rows.dailyStressRows.single['upload_time'], isNotNull);
expect(rows.sleepRows.single['upload_time'], isNotNull);
});
test('startCoreCaculate does not wait for pending uploads', () async {
const base = 1800000000;
final api = _FakeHealthKitRawDataHostApi()
..hrvUploadCompleter = Completer<int>()
..hrUploadCompleter = Completer<int>();
final store = _MemoryHealthRawStressLocalStore();
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: [_hrvStressPoint(base + 10)],
realtimeStressPoints: [_realtimeStressPoint(base + 20)],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
);
final calculation = service.startCoreCaculate(
endTime: base + 40,
readChunkDays: 1,
);
await _pumpAsyncUploads();
await expectLater(
calculation.timeout(const Duration(seconds: 1)),
completes,
);
expect(api.hrvUploadCallCount, 1);
expect(api.hrUploadCallCount, 1);
var rows = await store.debugQueryAllRows(42);
expect(rows.hrvRows.single['uploaded'], 0);
expect(rows.realtimeRows.single['uploaded'], 0);
expect(rows.hrvRows.single['upload_time'], isNull);
expect(rows.realtimeRows.single['upload_time'], isNull);
api.hrvUploadCompleter!.complete(base + 10);
api.hrUploadCompleter!.complete(base + 20);
await _pumpAsyncUploads();
rows = await store.debugQueryAllRows(42);
expect(rows.hrvRows.single['uploaded'], 1);
expect(rows.realtimeRows.single['uploaded'], 1);
expect(rows.hrvRows.single['upload_time'], isNotNull);
expect(rows.realtimeRows.single['upload_time'], isNotNull);
});
test('local data source returns earliest local hr start time', () async {
const base = 1800000000;
final api = _FakeHealthKitRawDataHostApi();
final store = _MemoryHealthRawStressLocalStore();
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
userIdProvider: () => 42,
uploadResultsAfterCalculation: false,
);
await store.upsertResult(
HealthRawStressCalculationResult(
userId: 42,
hrvStressPoints: const <HealthRawHrvStressPoint>[],
realtimeStressPoints: [
_realtimeStressPoint(base + 200),
_realtimeStressPoint(base + 100),
],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
final facade = HealthRawDataCoreService(
appleService: service,
targetPlatform: TargetPlatform.iOS,
);
final result = await LocalHealthDataSource(coreService: facade)
.getHealthDataEverUploaded();
expect(result, isA<AppSuccess>());
final data = (result as AppSuccess).data;
expect(data.flag, isTrue);
expect(data.startTime, base + 100);
});
}
HealthKitRawDataPoint _point(
int time,
double value, {
bool? isMotionLike,
}) {
return HealthKitRawDataPoint(
dataType: 0,
startTime: time,
endTime: time,
value: value,
isMotionLike: isMotionLike,
);
}
HealthRawRealtimeStressPoint _realtimeStressPoint(
int time, {
double result = 30,
}) {
return HealthRawRealtimeStressPoint(
userId: 42,
rawEndTime: time,
rawHr: 70,
result: result,
sourceStartTime: time,
sourceEndTime: time,
);
}
HealthRawHrvStressPoint _hrvStressPoint(int time) {
return HealthRawHrvStressPoint(
userId: 42,
rawEndTime: time,
rawHrv: 30,
result: 30,
sourceStartTime: time,
sourceEndTime: time,
state: HealthRawStressState.normal,
baselineHrv: 30,
baselineAwakeHrv: 30,
baselineSleepHrv: null,
baselineRestingHr: 60,
);
}
class _FakeHealthKitRawDataHostApi extends HealthKitRawDataHostApi {
_FakeHealthKitRawDataHostApi({List<String>? operationLog})
: operationLog = operationLog ?? <String>[];
final Map<int, List<HealthKitRawDataPoint>> _pointsByType = {};
final List<HealthKitRawWorkoutDataPoint> _workoutPoints = [];
final List<HealthKitRawDataPoint> _sleepPoints = [];
final List<_ReadCall> calls = [];
final List<String> operationLog;
var sleepCallCount = 0;
var workoutCallCount = 0;
var hrUploadCallCount = 0;
var hrvUploadCallCount = 0;
var dailyStressUploadCallCount = 0;
var sleepUploadCallCount = 0;
var hrUploadUntil = 0;
var hrvUploadUntil = 0;
var dailyStressUploadUntil = 0;
var sleepUploadUntil = 0;
Completer<int>? hrUploadCompleter;
Completer<int>? hrvUploadCompleter;
var hasData = true;
Object? hasDataError;
var hasHealthDataCallCount = 0;
void setPoints(int dataType, List<HealthKitRawDataPoint> points) {
_pointsByType[dataType] = points;
}
void setWorkoutPoints(List<HealthKitRawWorkoutDataPoint> points) {
_workoutPoints
..clear()
..addAll(points);
}
void setSleepPoints(List<HealthKitRawDataPoint> points) {
_sleepPoints
..clear()
..addAll(points);
}
int firstCallStart(int dataType) {
return calls.firstWhere((e) => e.dataType == dataType).startTime;
}
@override
Future<bool> hasHealthData() async {
hasHealthDataCallCount += 1;
final error = hasDataError;
if (error != null) {
throw error;
}
return hasData;
}
@override
Future<List<HealthKitRawDataPoint>> getHealthKitRawData(
int dataType,
int startTime,
int endTime,
) async {
calls.add(_ReadCall(dataType, startTime, endTime));
return (_pointsByType[dataType] ?? const <HealthKitRawDataPoint>[])
.where((e) => e.endTime >= startTime && e.endTime <= endTime)
.toList();
}
@override
Future<int> performHRDataUpload({required String sqliteFilePath}) async {
hrUploadCallCount += 1;
final completer = hrUploadCompleter;
if (completer != null) {
return completer.future;
}
return hrUploadUntil;
}
@override
Future<int> performHRVDataUpload({required String sqliteFilePath}) async {
hrvUploadCallCount += 1;
final completer = hrvUploadCompleter;
if (completer != null) {
return completer.future;
}
return hrvUploadUntil;
}
@override
Future<int> performAvgRealtimeStressDataUpload({
required String sqliteFilePath,
}) async {
operationLog.add('performAvgRealtimeStressDataUpload');
dailyStressUploadCallCount += 1;
return dailyStressUploadUntil;
}
@override
Future<int> performSleepAnalysisDataUpload({
required String sqliteFilePath,
}) async {
sleepUploadCallCount += 1;
return sleepUploadUntil;
}
@override
Future<List<HealthKitRawSleepDataPoint>> getHealthKitRawSleepData(
int startTime,
int endTime,
) async {
sleepCallCount += 1;
final points = _sleepPoints
.where((e) => e.endTime >= startTime && e.startTime <= endTime)
.toList();
if (points.isEmpty) return const <HealthKitRawSleepDataPoint>[];
return [
HealthKitRawSleepDataPoint(
dataType: 3,
sleepDataPoints: points,
),
];
}
@override
Future<List<HealthKitRawWorkoutDataPoint>> getHealthKitRawWorkoutData(
int startTime,
int endTime,
) async {
workoutCallCount += 1;
return _workoutPoints
.where((e) => e.endTime >= startTime && e.startTime <= endTime)
.toList();
}
}
class _FakeHealthKitHostApi extends HealthKitHostApi {
_FakeHealthKitHostApi({this.status = 1});
final int status;
@override
Future<HealthAuthorization> checkHealthAppAuthorization() async {
return HealthAuthorization(status: status, hasData: status == 1);
}
}
class _FakeHealthRawLocalNotificationDispatcher
extends HealthRawLocalNotificationDispatcher {
final sentNotifications = <HealthRawLocalNotification>[];
var record = const HealthRawLocalNotificationRecord();
@override
Future<HealthRawLocalNotificationRecord> readRecord(int userId) async {
return record;
}
@override
Future<bool> sendOne({
required int userId,
required HealthRawLocalNotification notification,
}) async {
sentNotifications.add(notification);
record = record.withNotification(notification);
return true;
}
@override
Future<void> recordRealtimeStressTime({
required int userId,
required int recordTime,
}) async {
record = record.copyWith(lastRealtimeStressTime: recordTime);
}
@override
Future<void> updateProcessedDataTimes({
required int userId,
int? latestHrvDataTime,
int? latestRealtimeStressDataTime,
int? latestSleepDataTime,
}) async {
record = record.copyWith(
latestHrvDataTime: latestHrvDataTime,
latestRealtimeStressDataTime: latestRealtimeStressDataTime,
latestSleepDataTime: latestSleepDataTime,
);
}
}
class _ReadCall {
const _ReadCall(this.dataType, this.startTime, this.endTime);
final int dataType;
final int startTime;
final int endTime;
}
class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
_MemoryHealthRawStressLocalStore({List<String>? operationLog})
: operationLog = operationLog ?? <String>[];
final Map<int, List<HealthRawHrvStressPoint>> _hrv = {};
final Map<int, List<HealthRawRealtimeStressPoint>> _realtime = {};
final Map<int, List<HealthRawDailyStressPoint>> _daily = {};
final Map<int, List<HealthRawSleepResult>> _sleep = {};
final Map<int, Map<int, int>> _hrvUpdateTimes = {};
final Map<int, Map<int, int>> _realtimeUpdateTimes = {};
final Map<int, Map<int, int>> _dailyUpdateTimes = {};
final Map<int, Map<int, int>> _sleepUpdateTimes = {};
final Map<int, Map<int, int>> _hrvPushSendTimes = {};
final Map<int, Map<int, int>> _realtimePushSendTimes = {};
final Map<int, Map<int, int>> _sleepPushSendTimes = {};
final Map<int, Map<int, int>> _hrvUploadTimes = {};
final Map<int, Map<int, int>> _realtimeUploadTimes = {};
final Map<int, Map<int, int>> _dailyUploadTimes = {};
final Map<int, Map<int, int>> _sleepUploadTimes = {};
final List<String> operationLog;
var _updateTimeClock = 1;
var _uploadTimeClock = 1000;
var realtimeNotificationWindowQueryCount = 0;
void insertDailyStress(HealthRawDailyStressPoint point) {
final byDate = <int, HealthRawDailyStressPoint>{
for (final item in _daily[point.userId] ?? <HealthRawDailyStressPoint>[])
item.date: item,
};
byDate[point.date] = point;
_daily[point.userId] = byDate.values.toList()
..sort((a, b) => a.date.compareTo(b.date));
_dailyUpdateTimes.putIfAbsent(
point.userId, () => <int, int>{})[point.date] = _nextUpdateTime();
}
@override
Future<void> ensureReadable(int userId) async {}
@override
Future<void> prepareDatabaseFileForShare(int userId) async {}
@override
Future<String> dbPath(int userId) async => 'memory-$userId.sqlite';
@override
Future<void> upsertResult(HealthRawStressCalculationResult result) async {
operationLog.add('upsertResult');
final updateTime = _nextUpdateTime();
final hrvByTime = <int, HealthRawHrvStressPoint>{
for (final point in _hrv[result.userId] ?? <HealthRawHrvStressPoint>[])
point.rawEndTime: point,
};
for (final point in result.hrvStressPoints) {
final existing = hrvByTime[point.rawEndTime];
final same = existing != null && _sameHrvStressPoint(existing, point);
hrvByTime[point.rawEndTime] = HealthRawHrvStressPoint(
userId: point.userId,
rawEndTime: point.rawEndTime,
rawHrv: point.rawHrv,
result: point.result,
sourceStartTime: point.sourceStartTime,
sourceEndTime: point.sourceEndTime,
state: point.state,
baselineHrv: point.baselineHrv,
baselineAwakeHrv: point.baselineAwakeHrv,
baselineSleepHrv: point.baselineSleepHrv,
baselineRestingHr: point.baselineRestingHr,
flags: point.flags,
uploaded: same ? existing.uploaded : false,
pushSendTime: existing?.pushSendTime,
uploadTime: same ? existing.uploadTime : null,
);
_hrvUpdateTimes.putIfAbsent(
point.userId, () => <int, int>{})[point.rawEndTime] = updateTime;
if (!same) {
_hrvUploadTimes[point.userId]?.remove(point.rawEndTime);
}
}
_hrv[result.userId] = hrvByTime.values.toList()
..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
final realtimeByTime = <int, HealthRawRealtimeStressPoint>{
for (final point
in _realtime[result.userId] ?? <HealthRawRealtimeStressPoint>[])
point.rawEndTime: point,
};
for (final point in result.realtimeStressPoints) {
final existing = realtimeByTime[point.rawEndTime];
final same =
existing != null && _sameRealtimeStressPoint(existing, point);
realtimeByTime[point.rawEndTime] = HealthRawRealtimeStressPoint(
userId: point.userId,
rawEndTime: point.rawEndTime,
rawHr: point.rawHr,
result: point.result,
sourceStartTime: point.sourceStartTime,
sourceEndTime: point.sourceEndTime,
flags: point.flags,
uploaded: same ? existing.uploaded : false,
pushSendTime: existing?.pushSendTime,
uploadTime: same ? existing.uploadTime : null,
);
_realtimeUpdateTimes.putIfAbsent(
point.userId, () => <int, int>{})[point.rawEndTime] = updateTime;
if (!same) {
_realtimeUploadTimes[point.userId]?.remove(point.rawEndTime);
}
}
_realtime[result.userId] = realtimeByTime.values.toList()
..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
}
@override
Future<void> upsertDailyStressPoints({
required int userId,
required Iterable<HealthRawDailyStressPoint> points,
}) async {
operationLog.add('upsertDailyStressPoints');
final updateTime = _nextUpdateTime();
final byDate = <int, HealthRawDailyStressPoint>{
for (final point in _daily[userId] ?? <HealthRawDailyStressPoint>[])
point.date: point,
};
for (final point in points) {
final existing = byDate[point.date];
final same = existing != null &&
existing.userId == point.userId &&
existing.stressValue == point.stressValue &&
existing.stressScore == point.stressScore &&
existing.state == point.state;
final isToday = point.date == _todayDateKey();
byDate[point.date] = HealthRawDailyStressPoint(
userId: point.userId,
date: point.date,
stressValue: point.stressValue,
stressScore: point.stressScore,
state: point.state,
dataTime: point.dataTime,
uploaded: isToday ? false : (same ? existing.uploaded : false),
uploadTime: isToday || !same ? null : existing.uploadTime,
);
_dailyUpdateTimes.putIfAbsent(
point.userId, () => <int, int>{})[point.date] = updateTime;
if (isToday || !same) {
_dailyUploadTimes[point.userId]?.remove(point.date);
}
}
_daily[userId] = byDate.values.toList()
..sort((a, b) => a.date.compareTo(b.date));
}
@override
Future<void> upsertSleepResults({
required int userId,
required Iterable<HealthRawSleepResult> results,
}) async {
final updateTime = _nextUpdateTime();
final byDate = <int, HealthRawSleepResult>{
for (final result in _sleep[userId] ?? <HealthRawSleepResult>[])
result.date: result,
};
for (final result in results) {
final existing = byDate[result.date];
final same = existing != null &&
existing.userId == result.userId &&
existing.startDate == result.startDate &&
existing.sleepScore == result.sleepScore &&
existing.sleepState == result.sleepState &&
existing.inBedMinutes == result.inBedMinutes &&
existing.awakMinutes == result.awakMinutes &&
existing.sleepMinutes == result.sleepMinutes;
byDate[result.date] = HealthRawSleepResult(
userId: result.userId,
date: result.date,
startDate: result.startDate,
sleepScore: result.sleepScore,
sleepState: result.sleepState,
inBedMinutes: result.inBedMinutes,
awakMinutes: result.awakMinutes,
sleepMinutes: result.sleepMinutes,
uploaded: same ? existing.uploaded : result.uploaded,
pushSendTime: existing?.pushSendTime,
uploadTime: same ? existing.uploadTime : result.uploadTime,
);
_sleepUpdateTimes.putIfAbsent(
result.userId, () => <int, int>{})[result.date] = updateTime;
if (!same) {
_sleepUploadTimes[result.userId]?.remove(result.date);
}
}
_sleep[userId] = byDate.values.toList()
..sort((a, b) => a.date.compareTo(b.date));
}
@override
Future<Set<int>> existingDailyStressDates({
required int userId,
required Iterable<int> dates,
}) async {
final dateSet = dates.toSet();
return (_daily[userId] ?? <HealthRawDailyStressPoint>[])
.where((e) => dateSet.contains(e.date))
.map((e) => e.date)
.toSet();
}
@override
Future<void> deleteDailyStressDates({
required int userId,
required Iterable<int> dates,
}) async {
final dateSet = dates.toSet();
_daily[userId] = (_daily[userId] ?? <HealthRawDailyStressPoint>[])
.where((e) => !dateSet.contains(e.date))
.toList();
}
@override
Future<void> clearTables(int userId) async {
_hrv.remove(userId);
_realtime.remove(userId);
_daily.remove(userId);
_sleep.remove(userId);
}
@override
Future<List<HealthRawHrvStressPoint>> queryHrvStressPoints({
required int userId,
required int startTime,
required int endTime,
}) async {
return (_hrv[userId] ?? <HealthRawHrvStressPoint>[])
.where((e) => e.rawEndTime >= startTime && e.rawEndTime <= endTime)
.toList();
}
@override
Future<List<HealthRawRealtimeStressPoint>> queryRealtimeStressPoints({
required int userId,
required int startTime,
required int endTime,
}) async {
operationLog.add('queryRealtimeStressPoints');
if (endTime - startTime <= Duration.secondsPerHour) {
realtimeNotificationWindowQueryCount += 1;
}
return (_realtime[userId] ?? <HealthRawRealtimeStressPoint>[])
.where((e) => e.rawEndTime >= startTime && e.rawEndTime <= endTime)
.toList();
}
@override
Future<List<HealthRawDailyStressPoint>> queryDailyStressPoints({
required int userId,
required int startDate,
required int endDate,
}) async {
return (_daily[userId] ?? <HealthRawDailyStressPoint>[])
.where((e) => e.date >= startDate && e.date <= endDate)
.toList();
}
@override
Future<List<HealthRawSleepResult>> querySleepResults({
required int userId,
required int startTime,
required int endTime,
}) async {
return (_sleep[userId] ?? <HealthRawSleepResult>[])
.where((e) => e.date >= startTime && e.date <= endTime)
.toList();
}
@override
Future<HealthRawStressDbRows> debugQueryAllRows(int userId) async {
return HealthRawStressDbRows(
hrvRows:
(_hrv[userId] ?? <HealthRawHrvStressPoint>[]).map(_hrvRow).toList(),
realtimeRows: (_realtime[userId] ?? <HealthRawRealtimeStressPoint>[])
.map(_realtimeRow)
.toList(),
dailyStressRows: (_daily[userId] ?? <HealthRawDailyStressPoint>[])
.map(_dailyStressRow)
.toList(),
sleepRows:
(_sleep[userId] ?? <HealthRawSleepResult>[]).map(_sleepRow).toList(),
);
}
@override
Future<int?> latestHrvSourceStartTime(int userId) async {
final points = _hrv[userId];
if (points == null || points.isEmpty) return null;
return points.last.sourceStartTime;
}
@override
Future<int?> latestRealtimeSourceStartTime(int userId) async {
final points = _realtime[userId];
if (points == null || points.isEmpty) return null;
return points.last.sourceStartTime;
}
@override
Future<int?> latestHrvRawEndTime(int userId) async {
final points = _hrv[userId];
if (points == null || points.isEmpty) return null;
return points.last.rawEndTime;
}
@override
Future<int?> latestRealtimeRawEndTime(int userId) async {
final points = _realtime[userId];
if (points == null || points.isEmpty) return null;
return points.last.rawEndTime;
}
@override
Future<int?> latestRealtimeStressPushSendTime(int userId) async {
final times = (_realtimePushSendTimes[userId] ?? <int, int>{}).values;
if (times.isEmpty) return null;
return times.reduce((a, b) => a >= b ? a : b);
}
@override
Future<void> markLocalNotificationPushed({
required int userId,
required HealthRawLocalNotificationRecordType recordType,
required int recordTime,
required int pushSendTime,
}) async {
switch (recordType) {
case HealthRawLocalNotificationRecordType.hrv:
_hrvPushSendTimes.putIfAbsent(userId, () => <int, int>{})[recordTime] =
pushSendTime;
_hrv[userId] = (_hrv[userId] ?? <HealthRawHrvStressPoint>[])
.map((point) => point.rawEndTime == recordTime
? HealthRawHrvStressPoint(
userId: point.userId,
rawEndTime: point.rawEndTime,
rawHrv: point.rawHrv,
result: point.result,
sourceStartTime: point.sourceStartTime,
sourceEndTime: point.sourceEndTime,
state: point.state,
baselineHrv: point.baselineHrv,
baselineAwakeHrv: point.baselineAwakeHrv,
baselineSleepHrv: point.baselineSleepHrv,
baselineRestingHr: point.baselineRestingHr,
flags: point.flags,
uploaded: point.uploaded,
pushSendTime: pushSendTime,
uploadTime: point.uploadTime,
)
: point)
.toList();
case HealthRawLocalNotificationRecordType.realtimeStress:
_realtimePushSendTimes.putIfAbsent(
userId, () => <int, int>{})[recordTime] = pushSendTime;
_realtime[userId] =
(_realtime[userId] ?? <HealthRawRealtimeStressPoint>[])
.map((point) => point.rawEndTime == recordTime
? HealthRawRealtimeStressPoint(
userId: point.userId,
rawEndTime: point.rawEndTime,
rawHr: point.rawHr,
result: point.result,
sourceStartTime: point.sourceStartTime,
sourceEndTime: point.sourceEndTime,
flags: point.flags,
uploaded: point.uploaded,
pushSendTime: pushSendTime,
uploadTime: point.uploadTime,
)
: point)
.toList();
case HealthRawLocalNotificationRecordType.sleep:
_sleepPushSendTimes.putIfAbsent(
userId, () => <int, int>{})[recordTime] = pushSendTime;
_sleep[userId] = (_sleep[userId] ?? <HealthRawSleepResult>[])
.map((result) => result.date == recordTime
? HealthRawSleepResult(
userId: result.userId,
date: result.date,
startDate: result.startDate,
sleepScore: result.sleepScore,
sleepState: result.sleepState,
inBedMinutes: result.inBedMinutes,
awakMinutes: result.awakMinutes,
sleepMinutes: result.sleepMinutes,
uploaded: result.uploaded,
pushSendTime: pushSendTime,
uploadTime: result.uploadTime,
)
: result)
.toList();
}
}
@override
Future<int?> earliestRealtimeRawEndTime(int userId) async {
final points = _realtime[userId];
if (points == null || points.isEmpty) return null;
return points.first.rawEndTime;
}
@override
Future<int?> latestSleepResultTime(int userId) async {
final results = _sleep[userId];
if (results == null || results.isEmpty) return null;
return results.last.date;
}
@override
Future<void> markHrvStressUploaded({
required int userId,
required Iterable<int> rawEndTimes,
}) async {
final timeSet = rawEndTimes.toSet();
final uploadTime = _nextUploadTime();
_hrv[userId] = (_hrv[userId] ?? <HealthRawHrvStressPoint>[]).map((point) {
if (!timeSet.contains(point.rawEndTime)) return point;
_hrvUploadTimes.putIfAbsent(
userId, () => <int, int>{})[point.rawEndTime] = uploadTime;
return HealthRawHrvStressPoint(
userId: point.userId,
rawEndTime: point.rawEndTime,
rawHrv: point.rawHrv,
result: point.result,
sourceStartTime: point.sourceStartTime,
sourceEndTime: point.sourceEndTime,
state: point.state,
baselineHrv: point.baselineHrv,
baselineAwakeHrv: point.baselineAwakeHrv,
baselineSleepHrv: point.baselineSleepHrv,
baselineRestingHr: point.baselineRestingHr,
flags: point.flags,
uploaded: true,
pushSendTime: point.pushSendTime,
uploadTime: uploadTime,
);
}).toList();
}
@override
Future<void> markHrvStressUploadedUntil({
required int userId,
required int rawEndTime,
}) async {
final times = (_hrv[userId] ?? <HealthRawHrvStressPoint>[])
.where((point) => point.rawEndTime <= rawEndTime)
.map((point) => point.rawEndTime);
await markHrvStressUploaded(userId: userId, rawEndTimes: times);
}
@override
Future<void> markRealtimeStressUploaded({
required int userId,
required Iterable<int> rawEndTimes,
}) async {
final timeSet = rawEndTimes.toSet();
final uploadTime = _nextUploadTime();
_realtime[userId] =
(_realtime[userId] ?? <HealthRawRealtimeStressPoint>[]).map((point) {
if (!timeSet.contains(point.rawEndTime)) return point;
_realtimeUploadTimes.putIfAbsent(
userId, () => <int, int>{})[point.rawEndTime] = uploadTime;
return HealthRawRealtimeStressPoint(
userId: point.userId,
rawEndTime: point.rawEndTime,
rawHr: point.rawHr,
result: point.result,
sourceStartTime: point.sourceStartTime,
sourceEndTime: point.sourceEndTime,
flags: point.flags,
uploaded: true,
pushSendTime: point.pushSendTime,
uploadTime: uploadTime,
);
}).toList();
}
@override
Future<void> markRealtimeStressUploadedUntil({
required int userId,
required int rawEndTime,
}) async {
final times = (_realtime[userId] ?? <HealthRawRealtimeStressPoint>[])
.where((point) => point.rawEndTime <= rawEndTime)
.map((point) => point.rawEndTime);
await markRealtimeStressUploaded(userId: userId, rawEndTimes: times);
}
@override
Future<void> markDailyStressUploadedUntil({
required int userId,
required int date,
}) async {
final uploadTime = _nextUploadTime();
_daily[userId] =
(_daily[userId] ?? <HealthRawDailyStressPoint>[]).map((point) {
if (point.date > date) return point;
_dailyUploadTimes.putIfAbsent(userId, () => <int, int>{})[point.date] =
uploadTime;
return HealthRawDailyStressPoint(
userId: point.userId,
date: point.date,
stressValue: point.stressValue,
stressScore: point.stressScore,
state: point.state,
dataTime: point.dataTime,
uploaded: true,
uploadTime: uploadTime,
);
}).toList();
}
@override
Future<void> markSleepResultsUploadedUntil({
required int userId,
required int date,
}) async {
final uploadTime = _nextUploadTime();
_sleep[userId] = (_sleep[userId] ?? <HealthRawSleepResult>[]).map((result) {
if (result.date > date) return result;
_sleepUploadTimes.putIfAbsent(userId, () => <int, int>{})[result.date] =
uploadTime;
return HealthRawSleepResult(
userId: result.userId,
date: result.date,
startDate: result.startDate,
sleepScore: result.sleepScore,
sleepState: result.sleepState,
inBedMinutes: result.inBedMinutes,
awakMinutes: result.awakMinutes,
sleepMinutes: result.sleepMinutes,
uploaded: true,
pushSendTime: result.pushSendTime,
uploadTime: uploadTime,
);
}).toList();
}
@override
Future<bool> hasPendingHrvStressUploads({required int userId}) async {
return (_hrv[userId] ?? <HealthRawHrvStressPoint>[])
.any((point) => !point.uploaded);
}
@override
Future<bool> hasPendingRealtimeStressUploads({required int userId}) async {
return (_realtime[userId] ?? <HealthRawRealtimeStressPoint>[])
.any((point) => !point.uploaded);
}
@override
Future<bool> hasPendingDailyStressUploads({required int userId}) async {
return (_daily[userId] ?? <HealthRawDailyStressPoint>[])
.any((point) => !point.uploaded);
}
@override
Future<bool> hasPendingSleepResultUploads({required int userId}) async {
return (_sleep[userId] ?? <HealthRawSleepResult>[])
.any((result) => !result.uploaded);
}
bool _sameHrvStressPoint(
HealthRawHrvStressPoint a,
HealthRawHrvStressPoint b,
) {
return a.userId == b.userId &&
a.rawHrv == b.rawHrv &&
a.result == b.result &&
a.state == b.state &&
a.baselineHrv == b.baselineHrv &&
a.baselineAwakeHrv == b.baselineAwakeHrv &&
a.baselineSleepHrv == b.baselineSleepHrv &&
a.baselineRestingHr == b.baselineRestingHr &&
_sameFlags(a.flags, b.flags);
}
bool _sameRealtimeStressPoint(
HealthRawRealtimeStressPoint a,
HealthRawRealtimeStressPoint b,
) {
return a.userId == b.userId &&
a.rawHr == b.rawHr &&
a.result == b.result &&
_sameFlags(a.flags, b.flags);
}
bool _sameFlags(HealthRawPointFlags a, HealthRawPointFlags b) {
return a.isWorkout == b.isWorkout &&
a.isWorkoutRecovery == b.isWorkoutRecovery &&
a.isSleepLikely == b.isSleepLikely &&
a.isSuspectedActivity == b.isSuspectedActivity;
}
Map<String, Object?> _hrvRow(HealthRawHrvStressPoint point) {
return <String, Object?>{
'raw_end_time': point.rawEndTime,
'user_id': point.userId,
'raw_hrv': point.rawHrv,
'result': point.result,
'source_start_time': point.sourceStartTime,
'source_end_time': point.sourceEndTime,
'state': point.state.value,
'baseline_hrv': point.baselineHrv,
'baseline_awake_hrv': point.baselineAwakeHrv,
'baseline_sleep_hrv': point.baselineSleepHrv,
'baseline_resting_hr': point.baselineRestingHr,
'is_workout': point.flags.isWorkout ? 1 : 0,
'is_workout_recovery': point.flags.isWorkoutRecovery ? 1 : 0,
'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0,
'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
'uploaded': point.uploaded ? 1 : 0,
'update_time': _hrvUpdateTimes[point.userId]?[point.rawEndTime] ?? 0,
'push_send_time': _hrvPushSendTimes[point.userId]?[point.rawEndTime],
'upload_time':
_hrvUploadTimes[point.userId]?[point.rawEndTime] ?? point.uploadTime,
};
}
Map<String, Object?> _realtimeRow(HealthRawRealtimeStressPoint point) {
return <String, Object?>{
'raw_end_time': point.rawEndTime,
'user_id': point.userId,
'raw_hr': point.rawHr,
'result': point.result,
'source_start_time': point.sourceStartTime,
'source_end_time': point.sourceEndTime,
'is_workout': point.flags.isWorkout ? 1 : 0,
'is_workout_recovery': point.flags.isWorkoutRecovery ? 1 : 0,
'is_sleep_likely': point.flags.isSleepLikely ? 1 : 0,
'is_suspected_activity': point.flags.isSuspectedActivity ? 1 : 0,
'uploaded': point.uploaded ? 1 : 0,
'update_time': _realtimeUpdateTimes[point.userId]?[point.rawEndTime] ?? 0,
'push_send_time': _realtimePushSendTimes[point.userId]?[point.rawEndTime],
'upload_time': _realtimeUploadTimes[point.userId]?[point.rawEndTime] ??
point.uploadTime,
};
}
Map<String, Object?> _dailyStressRow(HealthRawDailyStressPoint point) {
return <String, Object?>{
'date': point.date,
'user_id': point.userId,
'stress_value': point.stressValue,
'stress_score': point.stressScore,
'state': point.state.value,
'data_time': point.dataTime,
'uploaded': point.uploaded ? 1 : 0,
'update_time': _dailyUpdateTimes[point.userId]?[point.date] ?? 0,
'upload_time':
_dailyUploadTimes[point.userId]?[point.date] ?? point.uploadTime,
};
}
Map<String, Object?> _sleepRow(HealthRawSleepResult result) {
return <String, Object?>{
'date': result.date,
'user_id': result.userId,
'start_date': result.startDate,
'sleep_score': result.sleepScore,
'sleep_state': result.sleepState,
'in_bed_minutes': result.inBedMinutes,
'awak_minutes': result.awakMinutes,
'sleep_minutes': result.sleepMinutes,
'uploaded': result.uploaded ? 1 : 0,
'update_time': _sleepUpdateTimes[result.userId]?[result.date] ?? 0,
'push_send_time': _sleepPushSendTimes[result.userId]?[result.date],
'upload_time':
_sleepUploadTimes[result.userId]?[result.date] ?? result.uploadTime,
};
}
int _nextUpdateTime() => _updateTimeClock++;
int _nextUploadTime() => _uploadTimeClock++;
int _todayDateKey() {
final now = DateTime.now();
return now.year * 10000 + now.month * 100 + now.day;
}
}
... ...
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_source.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/huawei_health_data_type.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/ohos_health_raw_data_sync_service.dart';
import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('OHOS activity data uses iOS-compatible units', () async {
const time = 1800000000;
final local = _FakeOhosHealthRawDataLocalStore(
itemsByDataType: {
HuaweiHealthDataType.activity.dataType: const [
OhosHealthRawDataItem(
dataType: 4,
dataTime: time,
payload: {'time': time, 'value': 245},
),
],
HuaweiHealthDataType.exerciseDuration.dataType: const [
OhosHealthRawDataItem(
dataType: 5,
dataTime: time,
payload: {'time': time, 'value': 1800},
),
],
HuaweiHealthDataType.standingDuration.dataType: const [
OhosHealthRawDataItem(
dataType: 6,
dataTime: time,
payload: {'time': time, 'value': 2},
),
],
},
);
final syncService = OhosHealthRawDataSyncService(
remoteDataSource: const _FakeOhosHealthRawDataRemoteDataSource(),
localStore: local,
);
final dataSource = OhosHealthRawDataSource(syncService: syncService);
final points = await dataSource.getRawActivityData(time - 1, time + 1);
expect(points, hasLength(1));
expect(points.single.activeEnergyBurned, 245);
expect(points.single.appleExerciseTime, 1800);
expect(points.single.appleStandHours, 2);
});
}
class _FakeOhosHealthRawDataRemoteDataSource
implements OhosHealthRawDataRemoteDataSource {
const _FakeOhosHealthRawDataRemoteDataSource();
@override
Future<OhosHealthRawDataPage> fetchRawDataPage({
required int dataType,
required int startTime,
required int endTime,
String? pageToken,
}) async {
return const OhosHealthRawDataPage(items: []);
}
@override
Future<V2ActivityTarget?> fetchActivityGoal() async {
return null;
}
}
class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
const _FakeOhosHealthRawDataLocalStore({
required this.itemsByDataType,
});
final Map<int, List<OhosHealthRawDataItem>> itemsByDataType;
@override
Future<int?> latestDataTime({required int dataType}) async {
final items = itemsByDataType[dataType] ?? const [];
if (items.isEmpty) return null;
return items.map((item) => item.dataTime).reduce((a, b) => a > b ? a : b);
}
@override
Future<List<OhosHealthRawDataItem>> queryRawData({
required int dataType,
required int startTime,
required int endTime,
}) async {
return (itemsByDataType[dataType] ?? const <OhosHealthRawDataItem>[])
.where((item) => item.dataTime >= startTime && item.dataTime <= endTime)
.toList(growable: false);
}
@override
Future<int> upsertRawDataBatch({
required int dataType,
required List<OhosHealthRawDataItem> items,
}) async {
return items.length;
}
@override
Future<void> upsertActivityGoal(V2ActivityTarget goal) async {}
@override
Future<V2ActivityTarget?> queryActivityGoal() async {
return null;
}
}
... ...