Commit 8d7571c7b83eadcdc9a87ce9664c8c113777a24a

Authored by 权海
1 parent 5d3e13ed

feat(ui):本地推送条件后置单独记录

... ... @@ -449,9 +449,30 @@ class AppleHealthRawDataCoreService {
'latest_sleep': _latestSleepResultPayload(result.sleepResults),
},
);
if (result.hrvStressPoints.isEmpty &&
result.realtimeStressPoints.isEmpty &&
result.sleepResults.isEmpty) {
final record = await _readLocalNotificationRecordSafely(result.userId);
await _saveLocalNotificationDebugEvent(
<String, Object?>{
'event': 'flutterNotificationRecordRead',
'user_id': result.userId,
'record': _localNotificationRecordPayload(record),
},
);
final candidateData = await _resolveLocalNotificationCandidates(
result: result,
record: record,
);
await _saveLocalNotificationDebugEvent(
<String, Object?>{
'event': 'flutterNotificationLatestDataTimeChecked',
'user_id': result.userId,
'record': _localNotificationRecordPayload(record),
'latest_data_time': candidateData.latestDataTimePayload,
},
);
final candidateResult = candidateData.result;
if (candidateResult.hrvStressPoints.isEmpty &&
candidateResult.realtimeStressPoints.isEmpty &&
candidateResult.sleepResults.isEmpty) {
await _saveLocalNotificationDebugEvent(
<String, Object?>{
'event': 'flutterNotificationNotBuilt',
... ... @@ -462,12 +483,13 @@ class AppleHealthRawDataCoreService {
return;
}
final latestRealtimeStressPoint = result.realtimeStressPoints.isEmpty
? null
: result.realtimeStressPoints.reduce(
(a, b) => a.rawEndTime >= b.rawEndTime ? a : b,
);
var notificationResult = result;
final latestRealtimeStressPoint =
candidateResult.realtimeStressPoints.isEmpty
? null
: candidateResult.realtimeStressPoints.reduce(
(a, b) => a.rawEndTime >= b.rawEndTime ? a : b,
);
var notificationResult = candidateResult;
var realtimeWindow = const <HealthRawRealtimeStressPoint>[];
if (latestRealtimeStressPoint != null) {
final shouldResetRealtimeStressPushTime =
... ... @@ -484,36 +506,28 @@ class AppleHealthRawDataCoreService {
},
);
await _recordRealtimeStressTimeSafely(
userId: result.userId,
userId: candidateResult.userId,
recordTime: latestRealtimeStressPoint.rawEndTime,
);
notificationResult = result.copyWith(
notificationResult = candidateResult.copyWith(
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
);
} else {
realtimeWindow = await _queryRealtimeStressNotificationWindowSafely(
userId: result.userId,
userId: candidateResult.userId,
latestRawEndTime: latestRealtimeStressPoint.rawEndTime,
);
}
}
final record = await _readLocalNotificationRecordSafely(result.userId);
await _saveLocalNotificationDebugEvent(
<String, Object?>{
'event': 'flutterNotificationRecordRead',
'user_id': result.userId,
'record': _localNotificationRecordPayload(record),
},
);
await _saveLocalNotificationDebugEvent(
<String, Object?>{
'event': 'flutterNotificationBuildDecisions',
'user_id': result.userId,
'user_id': candidateResult.userId,
'decisions': _localNotificationDecisionPayloads(
result: notificationResult,
hasExistingHrv: hasExistingHrv,
hasExistingSleep: hasExistingSleep,
hasExistingHrv: candidateData.hasExistingHrvDataTime,
hasExistingSleep: candidateData.hasExistingSleepDataTime,
realtimeWindow: realtimeWindow,
record: record,
),
... ... @@ -521,15 +535,15 @@ class AppleHealthRawDataCoreService {
);
final notifications = HealthRawLocalNotificationBuilder(l10n).build(
result: notificationResult,
hasExistingHrv: hasExistingHrv,
hasExistingSleep: hasExistingSleep,
hasExistingHrv: candidateData.hasExistingHrvDataTime,
hasExistingSleep: candidateData.hasExistingSleepDataTime,
realtimeWindow: realtimeWindow,
record: record,
);
await _saveLocalNotificationDebugEvent(
<String, Object?>{
'event': 'flutterNotificationBuildFinished',
'user_id': result.userId,
'user_id': candidateResult.userId,
'notification_count': notifications.length,
'notifications': notifications.map(_localNotificationPayload).toList(),
},
... ... @@ -539,7 +553,7 @@ class AppleHealthRawDataCoreService {
<String, Object?>{
'event': 'flutterNotificationNotSent',
'reason': 'no_built_notifications',
'user_id': result.userId,
'user_id': candidateResult.userId,
},
);
return;
... ... @@ -550,18 +564,18 @@ class AppleHealthRawDataCoreService {
await _saveLocalNotificationDebugEvent(
<String, Object?>{
'event': 'flutterNotificationSendStart',
'user_id': result.userId,
'user_id': candidateResult.userId,
'notification': _localNotificationPayload(notification),
},
);
final sent = await _sendLocalNotificationSafely(
userId: result.userId,
userId: candidateResult.userId,
notification: notification,
);
await _saveLocalNotificationDebugEvent(
<String, Object?>{
'event': 'flutterNotificationSendFinished',
'user_id': result.userId,
'user_id': candidateResult.userId,
'sent': sent,
'reason': sent ? null : 'native_or_dispatcher_returned_false',
'notification': _localNotificationPayload(notification),
... ... @@ -574,6 +588,93 @@ class AppleHealthRawDataCoreService {
_scheduleRealtimeStressServerPush(sentNotifications);
}
Future<_HealthRawLocalNotificationCandidateData>
_resolveLocalNotificationCandidates({
required HealthRawStressCalculationResult result,
required HealthRawLocalNotificationRecord record,
}) async {
final userId = result.userId;
final latestHrvRawEndTime = await _localStore.latestHrvRawEndTime(userId);
final latestRealtimeRawEndTime =
await _localStore.latestRealtimeRawEndTime(userId);
final latestSleepDataTime = await _localStore.latestSleepResultTime(userId);
var hrvPoints = const <HealthRawHrvStressPoint>[];
var realtimePoints = const <HealthRawRealtimeStressPoint>[];
var sleepResults = const <HealthRawSleepResult>[];
int? nextHrvDataTime;
int? nextRealtimeStressDataTime;
int? nextSleepDataTime;
final hrvDataTime = record.latestHrvDataTime;
if (latestHrvRawEndTime != null) {
if (hrvDataTime == null) {
nextHrvDataTime = latestHrvRawEndTime;
} else if (hrvDataTime < latestHrvRawEndTime) {
hrvPoints = await _localStore.queryHrvStressPoints(
userId: userId,
startTime: hrvDataTime + 1,
endTime: latestHrvRawEndTime,
);
nextHrvDataTime = latestHrvRawEndTime;
}
}
final realtimeDataTime = record.latestRealtimeStressDataTime;
if (latestRealtimeRawEndTime != null) {
if (realtimeDataTime == null) {
nextRealtimeStressDataTime = latestRealtimeRawEndTime;
} else if (realtimeDataTime < latestRealtimeRawEndTime) {
realtimePoints = await _localStore.queryRealtimeStressPoints(
userId: userId,
startTime: realtimeDataTime + 1,
endTime: latestRealtimeRawEndTime,
);
nextRealtimeStressDataTime = latestRealtimeRawEndTime;
}
}
final sleepDataTime = record.latestSleepDataTime;
if (latestSleepDataTime != null) {
if (sleepDataTime == null) {
nextSleepDataTime = latestSleepDataTime;
} else if (sleepDataTime < latestSleepDataTime) {
sleepResults = await _localStore.querySleepResults(
userId: userId,
startTime: sleepDataTime + 1,
endTime: latestSleepDataTime,
);
nextSleepDataTime = latestSleepDataTime;
}
}
await _updateProcessedDataTimesSafely(
userId: userId,
latestHrvDataTime: nextHrvDataTime,
latestRealtimeStressDataTime: nextRealtimeStressDataTime,
latestSleepDataTime: nextSleepDataTime,
);
return _HealthRawLocalNotificationCandidateData(
result: HealthRawStressCalculationResult(
userId: userId,
hrvStressPoints: hrvPoints,
realtimeStressPoints: realtimePoints,
dailyStressPoints: result.dailyStressPoints,
sleepResults: sleepResults,
),
hasExistingHrvDataTime: hrvDataTime != null,
hasExistingRealtimeStressDataTime: realtimeDataTime != null,
hasExistingSleepDataTime: sleepDataTime != null,
currentHrvDataTime: latestHrvRawEndTime,
currentRealtimeStressDataTime: latestRealtimeRawEndTime,
currentSleepDataTime: latestSleepDataTime,
nextHrvDataTime: nextHrvDataTime,
nextRealtimeStressDataTime: nextRealtimeStressDataTime,
nextSleepDataTime: nextSleepDataTime,
);
}
Future<HealthRawLocalNotificationRecord> _readLocalNotificationRecordSafely(
int userId,
) async {
... ... @@ -585,6 +686,28 @@ class AppleHealthRawDataCoreService {
}
}
Future<void> _updateProcessedDataTimesSafely({
required int userId,
int? latestHrvDataTime,
int? latestRealtimeStressDataTime,
int? latestSleepDataTime,
}) async {
try {
await _localNotificationDispatcher.updateProcessedDataTimes(
userId: userId,
latestHrvDataTime: latestHrvDataTime,
latestRealtimeStressDataTime: latestRealtimeStressDataTime,
latestSleepDataTime: latestSleepDataTime,
);
} catch (error, stackTrace) {
_logError(
'update local notification processed data time failed',
error,
stackTrace,
);
}
}
Future<void> _recordRealtimeStressTimeSafely({
required int userId,
required int recordTime,
... ... @@ -869,6 +992,9 @@ class AppleHealthRawDataCoreService {
'last_sleep_time': record.lastSleepTime,
'last_hrv_time': record.lastHrvTime,
'last_realtime_stress_time': record.lastRealtimeStressTime,
'latest_hrv_data_time': record.latestHrvDataTime,
'latest_realtime_stress_data_time': record.latestRealtimeStressDataTime,
'latest_sleep_data_time': record.latestSleepDataTime,
};
}
... ... @@ -1680,6 +1806,50 @@ class HealthRawStressDbRows {
final List<Map<String, Object?>> sleepRows;
}
class _HealthRawLocalNotificationCandidateData {
const _HealthRawLocalNotificationCandidateData({
required this.result,
required this.hasExistingHrvDataTime,
required this.hasExistingRealtimeStressDataTime,
required this.hasExistingSleepDataTime,
required this.currentHrvDataTime,
required this.currentRealtimeStressDataTime,
required this.currentSleepDataTime,
required this.nextHrvDataTime,
required this.nextRealtimeStressDataTime,
required this.nextSleepDataTime,
});
final HealthRawStressCalculationResult result;
final bool hasExistingHrvDataTime;
final bool hasExistingRealtimeStressDataTime;
final bool hasExistingSleepDataTime;
final int? currentHrvDataTime;
final int? currentRealtimeStressDataTime;
final int? currentSleepDataTime;
final int? nextHrvDataTime;
final int? nextRealtimeStressDataTime;
final int? nextSleepDataTime;
Map<String, Object?> get latestDataTimePayload {
return <String, Object?>{
'has_existing_hrv_data_time': hasExistingHrvDataTime,
'has_existing_realtime_stress_data_time':
hasExistingRealtimeStressDataTime,
'has_existing_sleep_data_time': hasExistingSleepDataTime,
'current_hrv_data_time': currentHrvDataTime,
'current_realtime_stress_data_time': currentRealtimeStressDataTime,
'current_sleep_data_time': currentSleepDataTime,
'next_hrv_data_time': nextHrvDataTime,
'next_realtime_stress_data_time': nextRealtimeStressDataTime,
'next_sleep_data_time': nextSleepDataTime,
'candidate_hrv_count': result.hrvStressPoints.length,
'candidate_realtime_count': result.realtimeStressPoints.length,
'candidate_sleep_count': result.sleepResults.length,
};
}
}
double _integerDouble(num value) => value.toInt().toDouble();
class HealthRawStressLocalStore {
... ...
... ... @@ -289,6 +289,28 @@ class HealthRawLocalNotificationDispatcher {
record.copyWith(lastRealtimeStressTime: recordTime),
);
}
Future<void> updateProcessedDataTimes({
required int userId,
int? latestHrvDataTime,
int? latestRealtimeStressDataTime,
int? latestSleepDataTime,
}) async {
if (latestHrvDataTime == null &&
latestRealtimeStressDataTime == null &&
latestSleepDataTime == null) {
return;
}
final record = await _recordStore.read(userId);
await _recordStore.write(
userId,
record.copyWith(
latestHrvDataTime: latestHrvDataTime,
latestRealtimeStressDataTime: latestRealtimeStressDataTime,
latestSleepDataTime: latestSleepDataTime,
),
);
}
}
class HealthRawLocalNotificationRecord {
... ... @@ -296,11 +318,17 @@ class HealthRawLocalNotificationRecord {
this.lastSleepTime,
this.lastHrvTime,
this.lastRealtimeStressTime,
this.latestHrvDataTime,
this.latestRealtimeStressDataTime,
this.latestSleepDataTime,
});
final int? lastSleepTime;
final int? lastHrvTime;
final int? lastRealtimeStressTime;
final int? latestHrvDataTime;
final int? latestRealtimeStressDataTime;
final int? latestSleepDataTime;
factory HealthRawLocalNotificationRecord.fromJson(Map<String, Object?> json) {
return HealthRawLocalNotificationRecord(
... ... @@ -308,6 +336,10 @@ class HealthRawLocalNotificationRecord {
lastHrvTime: (json['last_hrv_time'] as num?)?.toInt(),
lastRealtimeStressTime:
(json['last_realtime_stress_time'] as num?)?.toInt(),
latestHrvDataTime: (json['latest_hrv_data_time'] as num?)?.toInt(),
latestRealtimeStressDataTime:
(json['latest_realtime_stress_data_time'] as num?)?.toInt(),
latestSleepDataTime: (json['latest_sleep_data_time'] as num?)?.toInt(),
);
}
... ... @@ -317,6 +349,11 @@ class HealthRawLocalNotificationRecord {
if (lastHrvTime != null) 'last_hrv_time': lastHrvTime,
if (lastRealtimeStressTime != null)
'last_realtime_stress_time': lastRealtimeStressTime,
if (latestHrvDataTime != null) 'latest_hrv_data_time': latestHrvDataTime,
if (latestRealtimeStressDataTime != null)
'latest_realtime_stress_data_time': latestRealtimeStressDataTime,
if (latestSleepDataTime != null)
'latest_sleep_data_time': latestSleepDataTime,
};
}
... ... @@ -340,12 +377,19 @@ class HealthRawLocalNotificationRecord {
int? lastSleepTime,
int? lastHrvTime,
int? lastRealtimeStressTime,
int? latestHrvDataTime,
int? latestRealtimeStressDataTime,
int? latestSleepDataTime,
}) {
return HealthRawLocalNotificationRecord(
lastSleepTime: lastSleepTime ?? this.lastSleepTime,
lastHrvTime: lastHrvTime ?? this.lastHrvTime,
lastRealtimeStressTime:
lastRealtimeStressTime ?? this.lastRealtimeStressTime,
latestHrvDataTime: latestHrvDataTime ?? this.latestHrvDataTime,
latestRealtimeStressDataTime:
latestRealtimeStressDataTime ?? this.latestRealtimeStressDataTime,
latestSleepDataTime: latestSleepDataTime ?? this.latestSleepDataTime,
);
}
}
... ...
... ... @@ -662,7 +662,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,
... ... @@ -688,6 +692,9 @@ void main() {
),
hasLength(1),
);
expect(notificationDispatcher.record.latestHrvDataTime, base + 720);
expect(notificationDispatcher.record.latestSleepDataTime,
LocalHealthDataConvert.unixSeconds(sleepEnd));
});
test(
... ... @@ -719,7 +726,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,
... ... @@ -742,6 +753,56 @@ 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,
);
});
test(
... ... @@ -797,6 +858,9 @@ void main() {
),
isEmpty,
);
expect(notificationDispatcher.record.latestHrvDataTime, base + 420);
expect(notificationDispatcher.record.latestSleepDataTime,
LocalHealthDataConvert.unixSeconds(sleepEnd));
});
test(
... ... @@ -968,12 +1032,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,
);
... ... @@ -1167,6 +1234,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 {
... ...
... ... @@ -292,6 +292,49 @@ void main() {
final record = await dispatcher.readRecord(1);
expect(record.lastRealtimeStressTime, 1234);
});
test('reads old notification record json without latest data times', () {
final record = HealthRawLocalNotificationRecord.fromJson(
const <String, Object?>{
'last_sleep_time': 100,
'last_hrv_time': 200,
'last_realtime_stress_time': 300,
},
);
expect(record.lastSleepTime, 100);
expect(record.lastHrvTime, 200);
expect(record.lastRealtimeStressTime, 300);
expect(record.latestHrvDataTime, isNull);
expect(record.latestRealtimeStressDataTime, isNull);
expect(record.latestSleepDataTime, isNull);
});
test('stores processed latest data times independently', () async {
final dir = await Directory.systemTemp.createTemp(
'health_raw_notification_test_',
);
addTearDown(() => dir.delete(recursive: true));
final dispatcher = HealthRawLocalNotificationDispatcher(
recordStore: HealthRawLocalNotificationRecordStore(rootDirectory: dir),
);
await dispatcher.updateProcessedDataTimes(
userId: 1,
latestHrvDataTime: 100,
latestRealtimeStressDataTime: 200,
latestSleepDataTime: 300,
);
await dispatcher.updateProcessedDataTimes(
userId: 1,
latestRealtimeStressDataTime: 250,
);
final record = await dispatcher.readRecord(1);
expect(record.latestHrvDataTime, 100);
expect(record.latestRealtimeStressDataTime, 250);
expect(record.latestSleepDataTime, 300);
});
}
int _seconds(DateTime time) => time.millisecondsSinceEpoch ~/ 1000;
... ...