|
|
|
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;
|
|
|
|
}
|
|
|
|
} |
...
|
...
|
|