|
...
|
...
|
@@ -2,6 +2,7 @@ import 'dart:async'; |
|
|
|
|
|
|
|
import 'package:doublefeel_flutter/core/result/app_result.dart';
|
|
|
|
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
|
|
|
|
import 'package:doublefeel_flutter/core/services/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';
|
|
...
|
...
|
@@ -642,6 +643,108 @@ void main() { |
|
|
|
expect(rows.realtimeRows.single['uploaded'], 1);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('hrv notification still sends when realtime window query fails',
|
|
|
|
() async {
|
|
|
|
final base =
|
|
|
|
DateTime.now().millisecondsSinceEpoch ~/ 1000 - Duration.secondsPerDay;
|
|
|
|
final api = _FakeHealthKitRawDataHostApi();
|
|
|
|
api.setPoints(HealthDataUploadType.hrv.type, [
|
|
|
|
_point(base + 60, 30),
|
|
|
|
_point(base + 360, 34),
|
|
|
|
]);
|
|
|
|
api.setPoints(HealthDataUploadType.heartRate.type, [
|
|
|
|
for (var i = 0; i < 12; i++) _point(base + 60 + i * 60, 70),
|
|
|
|
]);
|
|
|
|
api.setPoints(HealthDataUploadType.restingHeartRate.type, [
|
|
|
|
_point(base + 30, 60),
|
|
|
|
]);
|
|
|
|
final store = _MemoryHealthRawStressLocalStore()
|
|
|
|
..throwOnRealtimeNotificationWindowQuery = true;
|
|
|
|
await store.upsertResult(
|
|
|
|
HealthRawStressCalculationResult(
|
|
|
|
userId: 42,
|
|
|
|
hrvStressPoints: [_hrvStressPoint(base + 60)],
|
|
|
|
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
|
|
|
|
dailyStressPoints: const <HealthRawDailyStressPoint>[],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
final notificationDispatcher = _FakeLocalNotificationDispatcher();
|
|
|
|
final service = HealthRawDataCoreService(
|
|
|
|
healthApi: _FakeHealthKitHostApi(),
|
|
|
|
rawDataApi: api,
|
|
|
|
localStore: store,
|
|
|
|
userIdProvider: () => 42,
|
|
|
|
uploadResultsAfterCalculation: false,
|
|
|
|
localNotificationDispatcher: notificationDispatcher,
|
|
|
|
);
|
|
|
|
|
|
|
|
await service.syncAndStore(
|
|
|
|
startTime: base,
|
|
|
|
endTime: base + 900,
|
|
|
|
readChunkDays: 1,
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(
|
|
|
|
notificationDispatcher.sentRecordTypes,
|
|
|
|
contains(HealthRawLocalNotificationRecordType.hrv),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('sleep notification still sends after hrv send fails', () async {
|
|
|
|
final base =
|
|
|
|
DateTime.now().millisecondsSinceEpoch ~/ 1000 - Duration.secondsPerDay;
|
|
|
|
final sleepStart = base + 60;
|
|
|
|
final sleepEnd = sleepStart + const Duration(hours: 7).inSeconds;
|
|
|
|
final api = _FakeHealthKitRawDataHostApi();
|
|
|
|
api.setPoints(HealthDataUploadType.hrv.type, [
|
|
|
|
_point(base + 60, 30),
|
|
|
|
_point(base + 360, 34),
|
|
|
|
]);
|
|
|
|
api.setPoints(HealthDataUploadType.heartRate.type, [
|
|
|
|
for (var i = 0; i < 12; i++) _point(base + 60 + i * 60, 70),
|
|
|
|
]);
|
|
|
|
api.setPoints(HealthDataUploadType.restingHeartRate.type, [
|
|
|
|
_point(base + 30, 60),
|
|
|
|
]);
|
|
|
|
api.setSleepPoints([
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 3,
|
|
|
|
startTime: sleepStart,
|
|
|
|
endTime: sleepEnd,
|
|
|
|
),
|
|
|
|
]);
|
|
|
|
final store = _MemoryHealthRawStressLocalStore();
|
|
|
|
await store.upsertResult(
|
|
|
|
HealthRawStressCalculationResult(
|
|
|
|
userId: 42,
|
|
|
|
hrvStressPoints: [_hrvStressPoint(base + 60)],
|
|
|
|
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
|
|
|
|
dailyStressPoints: const <HealthRawDailyStressPoint>[],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
final notificationDispatcher = _FakeLocalNotificationDispatcher()
|
|
|
|
..throwOnRecordTypes.add(HealthRawLocalNotificationRecordType.hrv);
|
|
|
|
final service = HealthRawDataCoreService(
|
|
|
|
healthApi: _FakeHealthKitHostApi(),
|
|
|
|
rawDataApi: api,
|
|
|
|
localStore: store,
|
|
|
|
userIdProvider: () => 42,
|
|
|
|
uploadResultsAfterCalculation: false,
|
|
|
|
localNotificationDispatcher: notificationDispatcher,
|
|
|
|
);
|
|
|
|
|
|
|
|
await service.syncAndStore(
|
|
|
|
startTime: base,
|
|
|
|
endTime: sleepEnd + 60,
|
|
|
|
readChunkDays: 1,
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(
|
|
|
|
notificationDispatcher.sentRecordTypes,
|
|
|
|
contains(HealthRawLocalNotificationRecordType.sleep),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('local data source returns earliest local hr start time', () async {
|
|
|
|
const base = 1800000000;
|
|
|
|
final api = _FakeHealthKitRawDataHostApi();
|
|
...
|
...
|
@@ -852,6 +955,31 @@ class _FakeHealthKitHostApi extends HealthKitHostApi { |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _FakeLocalNotificationDispatcher
|
|
|
|
extends HealthRawLocalNotificationDispatcher {
|
|
|
|
final sentRecordTypes = <HealthRawLocalNotificationRecordType>[];
|
|
|
|
final throwOnRecordTypes = <HealthRawLocalNotificationRecordType>{};
|
|
|
|
var record = const HealthRawLocalNotificationRecord();
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<HealthRawLocalNotificationRecord> readRecord(int userId) async {
|
|
|
|
return record;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<bool> sendOne({
|
|
|
|
required int userId,
|
|
|
|
required HealthRawLocalNotification notification,
|
|
|
|
}) async {
|
|
|
|
if (throwOnRecordTypes.contains(notification.recordType)) {
|
|
|
|
throw StateError('send ${notification.recordType.name} failed');
|
|
|
|
}
|
|
|
|
sentRecordTypes.add(notification.recordType);
|
|
|
|
record = record.withNotification(notification);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _ReadCall {
|
|
|
|
const _ReadCall(this.dataType, this.startTime, this.endTime);
|
|
|
|
|
|
...
|
...
|
@@ -869,6 +997,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
final Map<int, List<HealthRawDailyStressPoint>> _daily = {};
|
|
|
|
final Map<int, List<HealthRawSleepResult>> _sleep = {};
|
|
|
|
final List<String> operationLog;
|
|
|
|
var throwOnRealtimeNotificationWindowQuery = false;
|
|
|
|
|
|
|
|
void insertDailyStress(HealthRawDailyStressPoint point) {
|
|
|
|
final byDate = <int, HealthRawDailyStressPoint>{
|
|
...
|
...
|
@@ -1039,6 +1168,10 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
required int endTime,
|
|
|
|
}) async {
|
|
|
|
operationLog.add('queryRealtimeStressPoints');
|
|
|
|
if (throwOnRealtimeNotificationWindowQuery &&
|
|
|
|
endTime - startTime <= Duration.secondsPerHour) {
|
|
|
|
throw StateError('realtime notification window query failed');
|
|
|
|
}
|
|
|
|
return (_realtime[userId] ?? <HealthRawRealtimeStressPoint>[])
|
|
|
|
.where((e) => e.rawEndTime >= startTime && e.rawEndTime <= endTime)
|
|
|
|
.toList();
|
...
|
...
|
|