|
...
|
...
|
@@ -439,6 +439,10 @@ void main() { |
|
|
|
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,
|
|
...
|
...
|
@@ -454,6 +458,7 @@ void main() { |
|
|
|
greaterThan(firstHrvUpdateTime),
|
|
|
|
);
|
|
|
|
expect(secondRows.hrvRows.single['uploaded'], 1);
|
|
|
|
expect(secondRows.hrvRows.single['upload_time'], hrvUploadTime);
|
|
|
|
});
|
|
|
|
|
|
|
|
test(
|
|
...
|
...
|
@@ -567,13 +572,14 @@ void main() { |
|
|
|
);
|
|
|
|
final eventFuture = service.healthDataUpdatedStream.first;
|
|
|
|
|
|
|
|
await service.onHealthDataUpdated(
|
|
|
|
final hasNewResult = await service.onHealthDataUpdated(
|
|
|
|
dataTypes: [
|
|
|
|
HealthDataUploadType.hrv.type,
|
|
|
|
HealthDataUploadType.heartRate.type,
|
|
|
|
],
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(hasNewResult, isTrue);
|
|
|
|
final event = await eventFuture;
|
|
|
|
expect(
|
|
|
|
event.dataTypes,
|
|
...
|
...
|
@@ -581,6 +587,24 @@ void main() { |
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
|
...
|
...
|
@@ -670,7 +694,7 @@ void main() { |
|
|
|
});
|
|
|
|
|
|
|
|
test(
|
|
|
|
'startCoreCaculate sends hrv and sleep notifications for new non-first results',
|
|
|
|
'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);
|
|
...
|
...
|
@@ -727,7 +751,11 @@ void main() { |
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher();
|
|
|
|
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher()
|
|
|
|
..record = HealthRawLocalNotificationRecord(
|
|
|
|
latestHrvDataTime: base + 10,
|
|
|
|
latestSleepDataTime: base - Duration.secondsPerDay,
|
|
|
|
);
|
|
|
|
final service = AppleHealthRawDataCoreService(
|
|
|
|
healthApi: _FakeHealthKitHostApi(),
|
|
|
|
rawDataApi: api,
|
|
...
|
...
|
@@ -742,17 +770,44 @@ void main() { |
|
|
|
readChunkDays: 1,
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(
|
|
|
|
notificationDispatcher.sentNotifications.where(
|
|
|
|
(e) => e.recordType == HealthRawLocalNotificationRecordType.hrv),
|
|
|
|
hasLength(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(
|
|
...
|
...
|
@@ -784,7 +839,11 @@ void main() { |
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher();
|
|
|
|
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher()
|
|
|
|
..record = const HealthRawLocalNotificationRecord(
|
|
|
|
latestHrvDataTime: base + 100,
|
|
|
|
latestSleepDataTime: base + 100,
|
|
|
|
);
|
|
|
|
final service = AppleHealthRawDataCoreService(
|
|
|
|
healthApi: _FakeHealthKitHostApi(),
|
|
|
|
rawDataApi: api,
|
|
...
|
...
|
@@ -807,6 +866,177 @@ void main() { |
|
|
|
),
|
|
|
|
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(
|
|
...
|
...
|
@@ -862,6 +1092,9 @@ void main() { |
|
|
|
),
|
|
|
|
isEmpty,
|
|
|
|
);
|
|
|
|
expect(notificationDispatcher.record.latestHrvDataTime, base + 420);
|
|
|
|
expect(notificationDispatcher.record.latestSleepDataTime,
|
|
|
|
LocalHealthDataConvert.unixSeconds(sleepEnd));
|
|
|
|
});
|
|
|
|
|
|
|
|
test(
|
|
...
|
...
|
@@ -934,6 +1167,10 @@ void main() { |
|
|
|
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 {
|
|
...
|
...
|
@@ -972,6 +1209,8 @@ void main() { |
|
|
|
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);
|
|
...
|
...
|
@@ -980,6 +1219,8 @@ void main() { |
|
|
|
|
|
|
|
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 {
|
|
...
|
...
|
@@ -1033,12 +1274,15 @@ HealthKitRawDataPoint _point( |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
HealthRawRealtimeStressPoint _realtimeStressPoint(int time) {
|
|
|
|
HealthRawRealtimeStressPoint _realtimeStressPoint(
|
|
|
|
int time, {
|
|
|
|
double result = 30,
|
|
|
|
}) {
|
|
|
|
return HealthRawRealtimeStressPoint(
|
|
|
|
userId: 42,
|
|
|
|
rawEndTime: time,
|
|
|
|
rawHr: 70,
|
|
|
|
result: 30,
|
|
|
|
result: result,
|
|
|
|
sourceStartTime: time,
|
|
|
|
sourceEndTime: time,
|
|
|
|
);
|
|
...
|
...
|
@@ -1232,6 +1476,20 @@ class _FakeHealthRawLocalNotificationDispatcher |
|
|
|
}) 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 {
|
|
...
|
...
|
@@ -1254,8 +1512,16 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
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) {
|
|
...
|
...
|
@@ -1282,6 +1548,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
@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,
|
|
...
|
...
|
@@ -1303,10 +1570,14 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
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] =
|
|
|
|
_nextUpdateTime();
|
|
|
|
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));
|
|
...
|
...
|
@@ -1329,10 +1600,14 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
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] =
|
|
|
|
_nextUpdateTime();
|
|
|
|
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));
|
|
...
|
...
|
@@ -1344,6 +1619,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
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,
|
|
...
|
...
|
@@ -1364,9 +1640,13 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
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] = _nextUpdateTime();
|
|
|
|
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));
|
|
...
|
...
|
@@ -1377,14 +1657,39 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
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) {
|
|
|
|
byDate[result.date] = result;
|
|
|
|
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] = _nextUpdateTime();
|
|
|
|
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));
|
|
...
|
...
|
@@ -1514,6 +1819,88 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
}
|
|
|
|
|
|
|
|
@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;
|
|
...
|
...
|
@@ -1533,8 +1920,11 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
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,
|
|
...
|
...
|
@@ -1549,6 +1939,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
baselineRestingHr: point.baselineRestingHr,
|
|
|
|
flags: point.flags,
|
|
|
|
uploaded: true,
|
|
|
|
pushSendTime: point.pushSendTime,
|
|
|
|
uploadTime: uploadTime,
|
|
|
|
);
|
|
|
|
}).toList();
|
|
|
|
}
|
|
...
|
...
|
@@ -1570,9 +1962,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
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,
|
|
...
|
...
|
@@ -1582,6 +1977,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
sourceEndTime: point.sourceEndTime,
|
|
|
|
flags: point.flags,
|
|
|
|
uploaded: true,
|
|
|
|
pushSendTime: point.pushSendTime,
|
|
|
|
uploadTime: uploadTime,
|
|
|
|
);
|
|
|
|
}).toList();
|
|
|
|
}
|
|
...
|
...
|
@@ -1602,9 +1999,12 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
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,
|
|
...
|
...
|
@@ -1613,6 +2013,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
state: point.state,
|
|
|
|
dataTime: point.dataTime,
|
|
|
|
uploaded: true,
|
|
|
|
uploadTime: uploadTime,
|
|
|
|
);
|
|
|
|
}).toList();
|
|
|
|
}
|
|
...
|
...
|
@@ -1622,8 +2023,11 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
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,
|
|
...
|
...
|
@@ -1634,6 +2038,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
awakMinutes: result.awakMinutes,
|
|
|
|
sleepMinutes: result.sleepMinutes,
|
|
|
|
uploaded: true,
|
|
|
|
pushSendTime: result.pushSendTime,
|
|
|
|
uploadTime: uploadTime,
|
|
|
|
);
|
|
|
|
}).toList();
|
|
|
|
}
|
|
...
|
...
|
@@ -1713,6 +2119,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
'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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -1730,6 +2139,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
'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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -1743,6 +2155,8 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
'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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -1758,11 +2172,16 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { |
|
|
|
'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;
|
...
|
...
|
|