|
@@ -2,6 +2,7 @@ import 'dart:async'; |
|
@@ -2,6 +2,7 @@ import 'dart:async'; |
|
2
|
|
2
|
|
|
3
|
import 'package:doublefeel_flutter/core/result/app_result.dart';
|
3
|
import 'package:doublefeel_flutter/core/result/app_result.dart';
|
|
4
|
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
|
4
|
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
|
|
|
|
5
|
+import 'package:doublefeel_flutter/core/services/health_raw_local_notification.dart';
|
|
5
|
import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart';
|
6
|
import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart';
|
|
6
|
import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
|
7
|
import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
|
|
7
|
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
|
8
|
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
|
|
@@ -642,6 +643,108 @@ void main() { |
|
@@ -642,6 +643,108 @@ void main() { |
|
642
|
expect(rows.realtimeRows.single['uploaded'], 1);
|
643
|
expect(rows.realtimeRows.single['uploaded'], 1);
|
|
643
|
});
|
644
|
});
|
|
644
|
|
645
|
|
|
|
|
646
|
+ test('hrv notification still sends when realtime window query fails',
|
|
|
|
647
|
+ () async {
|
|
|
|
648
|
+ final base =
|
|
|
|
649
|
+ DateTime.now().millisecondsSinceEpoch ~/ 1000 - Duration.secondsPerDay;
|
|
|
|
650
|
+ final api = _FakeHealthKitRawDataHostApi();
|
|
|
|
651
|
+ api.setPoints(HealthDataUploadType.hrv.type, [
|
|
|
|
652
|
+ _point(base + 60, 30),
|
|
|
|
653
|
+ _point(base + 360, 34),
|
|
|
|
654
|
+ ]);
|
|
|
|
655
|
+ api.setPoints(HealthDataUploadType.heartRate.type, [
|
|
|
|
656
|
+ for (var i = 0; i < 12; i++) _point(base + 60 + i * 60, 70),
|
|
|
|
657
|
+ ]);
|
|
|
|
658
|
+ api.setPoints(HealthDataUploadType.restingHeartRate.type, [
|
|
|
|
659
|
+ _point(base + 30, 60),
|
|
|
|
660
|
+ ]);
|
|
|
|
661
|
+ final store = _MemoryHealthRawStressLocalStore()
|
|
|
|
662
|
+ ..throwOnRealtimeNotificationWindowQuery = true;
|
|
|
|
663
|
+ await store.upsertResult(
|
|
|
|
664
|
+ HealthRawStressCalculationResult(
|
|
|
|
665
|
+ userId: 42,
|
|
|
|
666
|
+ hrvStressPoints: [_hrvStressPoint(base + 60)],
|
|
|
|
667
|
+ realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
|
|
|
|
668
|
+ dailyStressPoints: const <HealthRawDailyStressPoint>[],
|
|
|
|
669
|
+ ),
|
|
|
|
670
|
+ );
|
|
|
|
671
|
+ final notificationDispatcher = _FakeLocalNotificationDispatcher();
|
|
|
|
672
|
+ final service = HealthRawDataCoreService(
|
|
|
|
673
|
+ healthApi: _FakeHealthKitHostApi(),
|
|
|
|
674
|
+ rawDataApi: api,
|
|
|
|
675
|
+ localStore: store,
|
|
|
|
676
|
+ userIdProvider: () => 42,
|
|
|
|
677
|
+ uploadResultsAfterCalculation: false,
|
|
|
|
678
|
+ localNotificationDispatcher: notificationDispatcher,
|
|
|
|
679
|
+ );
|
|
|
|
680
|
+
|
|
|
|
681
|
+ await service.syncAndStore(
|
|
|
|
682
|
+ startTime: base,
|
|
|
|
683
|
+ endTime: base + 900,
|
|
|
|
684
|
+ readChunkDays: 1,
|
|
|
|
685
|
+ );
|
|
|
|
686
|
+
|
|
|
|
687
|
+ expect(
|
|
|
|
688
|
+ notificationDispatcher.sentRecordTypes,
|
|
|
|
689
|
+ contains(HealthRawLocalNotificationRecordType.hrv),
|
|
|
|
690
|
+ );
|
|
|
|
691
|
+ });
|
|
|
|
692
|
+
|
|
|
|
693
|
+ test('sleep notification still sends after hrv send fails', () async {
|
|
|
|
694
|
+ final base =
|
|
|
|
695
|
+ DateTime.now().millisecondsSinceEpoch ~/ 1000 - Duration.secondsPerDay;
|
|
|
|
696
|
+ final sleepStart = base + 60;
|
|
|
|
697
|
+ final sleepEnd = sleepStart + const Duration(hours: 7).inSeconds;
|
|
|
|
698
|
+ final api = _FakeHealthKitRawDataHostApi();
|
|
|
|
699
|
+ api.setPoints(HealthDataUploadType.hrv.type, [
|
|
|
|
700
|
+ _point(base + 60, 30),
|
|
|
|
701
|
+ _point(base + 360, 34),
|
|
|
|
702
|
+ ]);
|
|
|
|
703
|
+ api.setPoints(HealthDataUploadType.heartRate.type, [
|
|
|
|
704
|
+ for (var i = 0; i < 12; i++) _point(base + 60 + i * 60, 70),
|
|
|
|
705
|
+ ]);
|
|
|
|
706
|
+ api.setPoints(HealthDataUploadType.restingHeartRate.type, [
|
|
|
|
707
|
+ _point(base + 30, 60),
|
|
|
|
708
|
+ ]);
|
|
|
|
709
|
+ api.setSleepPoints([
|
|
|
|
710
|
+ HealthKitRawDataPoint(
|
|
|
|
711
|
+ dataType: 3,
|
|
|
|
712
|
+ startTime: sleepStart,
|
|
|
|
713
|
+ endTime: sleepEnd,
|
|
|
|
714
|
+ ),
|
|
|
|
715
|
+ ]);
|
|
|
|
716
|
+ final store = _MemoryHealthRawStressLocalStore();
|
|
|
|
717
|
+ await store.upsertResult(
|
|
|
|
718
|
+ HealthRawStressCalculationResult(
|
|
|
|
719
|
+ userId: 42,
|
|
|
|
720
|
+ hrvStressPoints: [_hrvStressPoint(base + 60)],
|
|
|
|
721
|
+ realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
|
|
|
|
722
|
+ dailyStressPoints: const <HealthRawDailyStressPoint>[],
|
|
|
|
723
|
+ ),
|
|
|
|
724
|
+ );
|
|
|
|
725
|
+ final notificationDispatcher = _FakeLocalNotificationDispatcher()
|
|
|
|
726
|
+ ..throwOnRecordTypes.add(HealthRawLocalNotificationRecordType.hrv);
|
|
|
|
727
|
+ final service = HealthRawDataCoreService(
|
|
|
|
728
|
+ healthApi: _FakeHealthKitHostApi(),
|
|
|
|
729
|
+ rawDataApi: api,
|
|
|
|
730
|
+ localStore: store,
|
|
|
|
731
|
+ userIdProvider: () => 42,
|
|
|
|
732
|
+ uploadResultsAfterCalculation: false,
|
|
|
|
733
|
+ localNotificationDispatcher: notificationDispatcher,
|
|
|
|
734
|
+ );
|
|
|
|
735
|
+
|
|
|
|
736
|
+ await service.syncAndStore(
|
|
|
|
737
|
+ startTime: base,
|
|
|
|
738
|
+ endTime: sleepEnd + 60,
|
|
|
|
739
|
+ readChunkDays: 1,
|
|
|
|
740
|
+ );
|
|
|
|
741
|
+
|
|
|
|
742
|
+ expect(
|
|
|
|
743
|
+ notificationDispatcher.sentRecordTypes,
|
|
|
|
744
|
+ contains(HealthRawLocalNotificationRecordType.sleep),
|
|
|
|
745
|
+ );
|
|
|
|
746
|
+ });
|
|
|
|
747
|
+
|
|
645
|
test('local data source returns earliest local hr start time', () async {
|
748
|
test('local data source returns earliest local hr start time', () async {
|
|
646
|
const base = 1800000000;
|
749
|
const base = 1800000000;
|
|
647
|
final api = _FakeHealthKitRawDataHostApi();
|
750
|
final api = _FakeHealthKitRawDataHostApi();
|
|
@@ -852,6 +955,31 @@ class _FakeHealthKitHostApi extends HealthKitHostApi { |
|
@@ -852,6 +955,31 @@ class _FakeHealthKitHostApi extends HealthKitHostApi { |
|
852
|
}
|
955
|
}
|
|
853
|
}
|
956
|
}
|
|
854
|
|
957
|
|
|
|
|
958
|
+class _FakeLocalNotificationDispatcher
|
|
|
|
959
|
+ extends HealthRawLocalNotificationDispatcher {
|
|
|
|
960
|
+ final sentRecordTypes = <HealthRawLocalNotificationRecordType>[];
|
|
|
|
961
|
+ final throwOnRecordTypes = <HealthRawLocalNotificationRecordType>{};
|
|
|
|
962
|
+ var record = const HealthRawLocalNotificationRecord();
|
|
|
|
963
|
+
|
|
|
|
964
|
+ @override
|
|
|
|
965
|
+ Future<HealthRawLocalNotificationRecord> readRecord(int userId) async {
|
|
|
|
966
|
+ return record;
|
|
|
|
967
|
+ }
|
|
|
|
968
|
+
|
|
|
|
969
|
+ @override
|
|
|
|
970
|
+ Future<bool> sendOne({
|
|
|
|
971
|
+ required int userId,
|
|
|
|
972
|
+ required HealthRawLocalNotification notification,
|
|
|
|
973
|
+ }) async {
|
|
|
|
974
|
+ if (throwOnRecordTypes.contains(notification.recordType)) {
|
|
|
|
975
|
+ throw StateError('send ${notification.recordType.name} failed');
|
|
|
|
976
|
+ }
|
|
|
|
977
|
+ sentRecordTypes.add(notification.recordType);
|
|
|
|
978
|
+ record = record.withNotification(notification);
|
|
|
|
979
|
+ return true;
|
|
|
|
980
|
+ }
|
|
|
|
981
|
+}
|
|
|
|
982
|
+
|
|
855
|
class _ReadCall {
|
983
|
class _ReadCall {
|
|
856
|
const _ReadCall(this.dataType, this.startTime, this.endTime);
|
984
|
const _ReadCall(this.dataType, this.startTime, this.endTime);
|
|
857
|
|
985
|
|
|
@@ -869,6 +997,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
@@ -869,6 +997,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
869
|
final Map<int, List<HealthRawDailyStressPoint>> _daily = {};
|
997
|
final Map<int, List<HealthRawDailyStressPoint>> _daily = {};
|
|
870
|
final Map<int, List<HealthRawSleepResult>> _sleep = {};
|
998
|
final Map<int, List<HealthRawSleepResult>> _sleep = {};
|
|
871
|
final List<String> operationLog;
|
999
|
final List<String> operationLog;
|
|
|
|
1000
|
+ var throwOnRealtimeNotificationWindowQuery = false;
|
|
872
|
|
1001
|
|
|
873
|
void insertDailyStress(HealthRawDailyStressPoint point) {
|
1002
|
void insertDailyStress(HealthRawDailyStressPoint point) {
|
|
874
|
final byDate = <int, HealthRawDailyStressPoint>{
|
1003
|
final byDate = <int, HealthRawDailyStressPoint>{
|
|
@@ -1039,6 +1168,10 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
@@ -1039,6 +1168,10 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
1039
|
required int endTime,
|
1168
|
required int endTime,
|
|
1040
|
}) async {
|
1169
|
}) async {
|
|
1041
|
operationLog.add('queryRealtimeStressPoints');
|
1170
|
operationLog.add('queryRealtimeStressPoints');
|
|
|
|
1171
|
+ if (throwOnRealtimeNotificationWindowQuery &&
|
|
|
|
1172
|
+ endTime - startTime <= Duration.secondsPerHour) {
|
|
|
|
1173
|
+ throw StateError('realtime notification window query failed');
|
|
|
|
1174
|
+ }
|
|
1042
|
return (_realtime[userId] ?? <HealthRawRealtimeStressPoint>[])
|
1175
|
return (_realtime[userId] ?? <HealthRawRealtimeStressPoint>[])
|
|
1043
|
.where((e) => e.rawEndTime >= startTime && e.rawEndTime <= endTime)
|
1176
|
.where((e) => e.rawEndTime >= startTime && e.rawEndTime <= endTime)
|
|
1044
|
.toList();
|
1177
|
.toList();
|