Commit fe916dc3111475ec0fb81d13d50b028c9e830fda

Authored by 权海
1 parent b6275e80

feat(ui):优化计算数据和推送发送

... ... @@ -160,6 +160,8 @@ class HealthRawHrvStressPoint {
required this.baselineRestingHr,
this.flags = const HealthRawPointFlags.none(),
this.uploaded = false,
this.pushSendTime,
this.uploadTime,
});
final int userId;
... ... @@ -175,6 +177,8 @@ class HealthRawHrvStressPoint {
final double baselineRestingHr;
final HealthRawPointFlags flags;
final bool uploaded;
final int? pushSendTime;
final int? uploadTime;
factory HealthRawHrvStressPoint.fromDb(Map<String, Object?> row) {
final result = (row['result'] as num).toDouble();
... ... @@ -196,6 +200,8 @@ class HealthRawHrvStressPoint {
baselineRestingHr: _positiveDouble(row['baseline_resting_hr']) ?? 65,
flags: _flagsFromDb(row),
uploaded: (row['uploaded'] as int) == 1,
pushSendTime: (row['push_send_time'] as num?)?.toInt(),
uploadTime: (row['upload_time'] as num?)?.toInt(),
);
}
}
... ... @@ -210,6 +216,8 @@ class HealthRawRealtimeStressPoint {
required this.sourceEndTime,
this.flags = const HealthRawPointFlags.none(),
this.uploaded = false,
this.pushSendTime,
this.uploadTime,
});
final int userId;
... ... @@ -220,6 +228,8 @@ class HealthRawRealtimeStressPoint {
final int sourceEndTime;
final HealthRawPointFlags flags;
final bool uploaded;
final int? pushSendTime;
final int? uploadTime;
HealthRawStressState get state => healthRawRealtimeStressState(result);
bool get isWorkout => flags.isWorkout;
... ... @@ -237,6 +247,8 @@ class HealthRawRealtimeStressPoint {
sourceEndTime: row['source_end_time'] as int,
flags: _flagsFromDb(row),
uploaded: (row['uploaded'] as int) == 1,
pushSendTime: (row['push_send_time'] as num?)?.toInt(),
uploadTime: (row['upload_time'] as num?)?.toInt(),
);
}
}
... ... @@ -250,6 +262,7 @@ class HealthRawDailyStressPoint {
required this.state,
required this.dataTime,
this.uploaded = false,
this.uploadTime,
});
final int userId;
... ... @@ -259,6 +272,7 @@ class HealthRawDailyStressPoint {
final HealthRawStressState state;
final int dataTime;
final bool uploaded;
final int? uploadTime;
factory HealthRawDailyStressPoint.fromDb(Map<String, Object?> row) {
final stressScore = row['stress_score'] as int;
... ... @@ -271,6 +285,7 @@ class HealthRawDailyStressPoint {
healthRawRealtimeStressState(stressScore),
dataTime: row['data_time'] as int,
uploaded: (row['uploaded'] as int) == 1,
uploadTime: (row['upload_time'] as num?)?.toInt(),
);
}
}
... ... @@ -286,6 +301,8 @@ class HealthRawSleepResult {
required this.awakMinutes,
required this.sleepMinutes,
this.uploaded = false,
this.pushSendTime,
this.uploadTime,
});
final int userId;
... ... @@ -297,6 +314,8 @@ class HealthRawSleepResult {
final int awakMinutes;
final int sleepMinutes;
final bool uploaded;
final int? pushSendTime;
final int? uploadTime;
factory HealthRawSleepResult.fromDb(Map<String, Object?> row) {
return HealthRawSleepResult(
... ... @@ -309,6 +328,8 @@ class HealthRawSleepResult {
awakMinutes: row['awak_minutes'] as int,
sleepMinutes: row['sleep_minutes'] as int,
uploaded: (row['uploaded'] as int? ?? 0) == 1,
pushSendTime: (row['push_send_time'] as num?)?.toInt(),
uploadTime: (row['upload_time'] as num?)?.toInt(),
);
}
... ...
... ... @@ -16,7 +16,6 @@ import '../../../logging/app_logger.dart';
import '../../../network/api/health_api.dart';
import '../../../result/app_result.dart';
import '../health_raw_models.dart';
import 'apple_health_raw_local_notification_debug_store.dart';
import 'apple_health_raw_local_notification.dart';
import '../health_raw_stress_calculator.dart';
import '../health_sleep_calculator.dart';
... ... @@ -38,7 +37,6 @@ class AppleHealthRawDataCoreService {
int Function()? userIdProvider,
bool uploadResultsAfterCalculation = true,
HealthRawLocalNotificationDispatcher? localNotificationDispatcher,
HealthRawLocalNotificationDebugStore? localNotificationDebugStore,
HealthApi? serverHealthApi,
}) : _healthApi = AppHealthKitHostApi(api: healthApi),
_rawDataApi = AppHealthKitRawDataHostApi(api: rawDataApi),
... ... @@ -48,8 +46,6 @@ class AppleHealthRawDataCoreService {
_uploadResultsAfterCalculation = uploadResultsAfterCalculation,
_localNotificationDispatcher = localNotificationDispatcher ??
HealthRawLocalNotificationDispatcher(),
_localNotificationDebugStore = localNotificationDebugStore ??
HealthRawLocalNotificationDebugStore(),
_serverHealthApi = serverHealthApi;
final AppHealthKitHostApi _healthApi;
... ... @@ -59,7 +55,6 @@ class AppleHealthRawDataCoreService {
final int Function()? _userIdProvider;
final bool _uploadResultsAfterCalculation;
final HealthRawLocalNotificationDispatcher _localNotificationDispatcher;
final HealthRawLocalNotificationDebugStore _localNotificationDebugStore;
final HealthApi? _serverHealthApi;
final StreamController<HealthRawDataUpdatedEvent>
_healthDataUpdatedController =
... ... @@ -218,7 +213,7 @@ class AppleHealthRawDataCoreService {
}
Future<String> readLocalNotificationDebugLogText() {
return _localNotificationDebugStore.readText();
return Future.value('');
}
Future<bool> shareLocalNotificationDebugRecord() async {
... ... @@ -536,6 +531,12 @@ class AppleHealthRawDataCoreService {
);
var notificationResult = candidateResult;
var realtimeWindow = const <HealthRawRealtimeStressPoint>[];
final lastRealtimeStressPushSendTime =
await _localStore.latestRealtimeStressPushSendTime(
candidateResult.userId,
);
final notificationBuildTime = _unixSecondsNow();
var realtimeSleepIntervals = const <HealthRawSleepInterval>[];
if (latestRealtimeStressPoint != null) {
final shouldResetRealtimeStressPushTime =
latestRealtimeStressPoint.isWorkout ||
... ... @@ -550,10 +551,6 @@ class AppleHealthRawDataCoreService {
_realtimeStressPointPayload(latestRealtimeStressPoint),
},
);
await _recordRealtimeStressTimeSafely(
userId: candidateResult.userId,
recordTime: latestRealtimeStressPoint.rawEndTime,
);
notificationResult = candidateResult.copyWith(
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
);
... ... @@ -562,6 +559,11 @@ class AppleHealthRawDataCoreService {
userId: candidateResult.userId,
latestRawEndTime: latestRealtimeStressPoint.rawEndTime,
);
realtimeSleepIntervals =
await _queryRealtimeStressSleepIntervalsForNotificationSafely(
userId: candidateResult.userId,
latestRawEndTime: latestRealtimeStressPoint.rawEndTime,
);
}
}
... ... @@ -575,7 +577,10 @@ class AppleHealthRawDataCoreService {
hasExistingHrv: candidateData.hasExistingHrvDataTime,
hasExistingSleep: candidateData.hasExistingSleepDataTime,
realtimeWindow: realtimeWindow,
realtimeSleepIntervals: realtimeSleepIntervals,
record: record,
lastRealtimeStressPushSendTime: lastRealtimeStressPushSendTime,
notificationBuildTime: notificationBuildTime,
),
},
);
... ... @@ -584,7 +589,10 @@ class AppleHealthRawDataCoreService {
hasExistingHrv: candidateData.hasExistingHrvDataTime,
hasExistingSleep: candidateData.hasExistingSleepDataTime,
realtimeWindow: realtimeWindow,
realtimeSleepIntervals: realtimeSleepIntervals,
record: record,
lastRealtimeStressPushSendTime: lastRealtimeStressPushSendTime,
notificationBuildTime: notificationBuildTime,
);
await _saveLocalNotificationDebugEvent(
<String, Object?>{
... ... @@ -633,6 +641,10 @@ class AppleHealthRawDataCoreService {
);
if (sent) {
sentNotifications.add(notification);
await _recordLocalNotificationPushSendTimeSafely(
userId: candidateResult.userId,
notification: notification,
);
}
}
_scheduleRealtimeStressServerPush(sentNotifications);
... ... @@ -758,17 +770,20 @@ class AppleHealthRawDataCoreService {
}
}
Future<void> _recordRealtimeStressTimeSafely({
Future<void> _recordLocalNotificationPushSendTimeSafely({
required int userId,
required int recordTime,
required HealthRawLocalNotification notification,
}) async {
try {
await _localNotificationDispatcher.recordRealtimeStressTime(
await _localStore.markLocalNotificationPushed(
userId: userId,
recordTime: recordTime,
recordType: notification.recordType,
recordTime: notification.recordTime,
pushSendTime: _unixSecondsNow(),
);
} catch (error, stackTrace) {
_logError('record realtime stress push time failed', error, stackTrace);
_logError(
'record local notification push send time failed', error, stackTrace);
}
}
... ... @@ -814,6 +829,73 @@ class AppleHealthRawDataCoreService {
}
}
Future<List<HealthRawSleepInterval>>
_queryRealtimeStressSleepIntervalsForNotificationSafely({
required int userId,
required int latestRawEndTime,
}) async {
try {
final intervals = await _queryRealtimeStressSleepIntervalsForNotification(
userId: userId,
latestRawEndTime: latestRawEndTime,
);
await _saveLocalNotificationDebugEvent(
<String, Object?>{
'event': 'flutterRealtimeStressSleepIntervalsQueried',
'user_id': userId,
'latest_raw_end_time': latestRawEndTime,
'sleep_intervals': intervals.map(_sleepIntervalPayload).toList(),
},
);
return intervals;
} catch (error, stackTrace) {
_logError(
'query realtime stress sleep intervals failed',
error,
stackTrace,
);
final fallback = [_defaultSleepIntervalForUnixSeconds(latestRawEndTime)];
await _saveLocalNotificationDebugEvent(
<String, Object?>{
'event': 'flutterRealtimeStressSleepIntervalsQueryFailed',
'user_id': userId,
'latest_raw_end_time': latestRawEndTime,
'sleep_intervals': fallback.map(_sleepIntervalPayload).toList(),
'error': error.toString(),
'stack_trace': stackTrace.toString(),
},
);
return fallback;
}
}
Future<List<HealthRawSleepInterval>>
_queryRealtimeStressSleepIntervalsForNotification({
required int userId,
required int latestRawEndTime,
}) async {
final dayRange = _dayRangeFromUnixSeconds(latestRawEndTime);
final sleepResults = await _localStore.querySleepResults(
userId: userId,
startTime: dayRange.$1,
endTime: dayRange.$2,
);
final intervals = sleepResults
.where((result) => result.startDate <= result.date)
.map(
(result) => (
startTime: result.startDate,
endTime: result.date,
),
)
.toList()
..sort((a, b) => a.startTime.compareTo(b.startTime));
if (intervals.isNotEmpty) {
return intervals;
}
return [_defaultSleepIntervalForUnixSeconds(latestRawEndTime)];
}
Future<bool> _sendLocalNotificationSafely({
required int userId,
required HealthRawLocalNotification notification,
... ... @@ -843,7 +925,10 @@ class AppleHealthRawDataCoreService {
required bool hasExistingHrv,
required bool hasExistingSleep,
required List<HealthRawRealtimeStressPoint> realtimeWindow,
required List<HealthRawSleepInterval> realtimeSleepIntervals,
required HealthRawLocalNotificationRecord record,
required int? lastRealtimeStressPushSendTime,
required int notificationBuildTime,
}) {
return <Map<String, Object?>>[
_sleepNotificationDecisionPayload(
... ... @@ -858,7 +943,10 @@ class AppleHealthRawDataCoreService {
_realtimeStressNotificationDecisionPayload(
result.realtimeStressPoints,
realtimeWindow: realtimeWindow,
realtimeSleepIntervals: realtimeSleepIntervals,
record: record,
lastPushSendTime: lastRealtimeStressPushSendTime,
notificationBuildTime: notificationBuildTime,
),
];
}
... ... @@ -936,20 +1024,23 @@ class AppleHealthRawDataCoreService {
}
final sorted = [...hrvPoints]
..sort((a, b) => a.rawEndTime.compareTo(b.rawEndTime));
final latest = sorted.last;
return <String, Object?>{
'type': HealthRawLocalNotificationRecordType.hrv.name,
'will_build': true,
'reason': 'candidate',
'last_hrv_time': record.lastHrvTime,
'latest_hrv': _hrvStressPointPayload(latest),
'candidate_count': sorted.length,
'latest_hrv': _hrvStressPointPayload(sorted.last),
};
}
Map<String, Object?> _realtimeStressNotificationDecisionPayload(
List<HealthRawRealtimeStressPoint> realtimePoints, {
required List<HealthRawRealtimeStressPoint> realtimeWindow,
required List<HealthRawSleepInterval> realtimeSleepIntervals,
required HealthRawLocalNotificationRecord record,
required int? lastPushSendTime,
required int notificationBuildTime,
}) {
if (realtimePoints.isEmpty) {
return <String, Object?>{
... ... @@ -969,6 +1060,19 @@ class AppleHealthRawDataCoreService {
'latest_realtime': _realtimeStressPointPayload(latest),
};
}
final matchedSleepInterval =
_sleepIntervalContaining(latest.rawEndTime, realtimeSleepIntervals);
if (matchedSleepInterval != null) {
return <String, Object?>{
'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
'will_build': false,
'reason': 'sleep_interval',
'matched_sleep_interval': _sleepIntervalPayload(matchedSleepInterval),
'sleep_intervals':
realtimeSleepIntervals.map(_sleepIntervalPayload).toList(),
'latest_realtime': _realtimeStressPointPayload(latest),
};
}
final valid = realtimeWindow
.where((e) => e.result >= 1 && e.result <= 100)
.toList()
... ... @@ -990,14 +1094,15 @@ class AppleHealthRawDataCoreService {
'latest_realtime': _realtimeStressPointPayload(latest),
};
}
final lastPushTime = record.lastRealtimeStressTime;
final lastPushTime = lastPushSendTime;
if (lastPushTime != null &&
latest.rawEndTime - lastPushTime < Duration.secondsPerHour) {
notificationBuildTime - lastPushTime < Duration.secondsPerHour) {
return <String, Object?>{
'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
'will_build': false,
'reason': 'within_realtime_interval',
'last_realtime_stress_time': lastPushTime,
'last_realtime_stress_push_send_time': lastPushTime,
'notification_build_time': notificationBuildTime,
'latest_realtime': _realtimeStressPointPayload(latest),
};
}
... ... @@ -1005,12 +1110,36 @@ class AppleHealthRawDataCoreService {
'type': HealthRawLocalNotificationRecordType.realtimeStress.name,
'will_build': true,
'reason': 'candidate',
'last_realtime_stress_time': lastPushTime,
'last_realtime_stress_push_send_time': lastPushTime,
'notification_build_time': notificationBuildTime,
'valid_window_count': valid.length,
'sleep_intervals':
realtimeSleepIntervals.map(_sleepIntervalPayload).toList(),
'latest_realtime': _realtimeStressPointPayload(latest),
};
}
HealthRawSleepInterval? _sleepIntervalContaining(
int time,
List<HealthRawSleepInterval> intervals,
) {
for (final interval in intervals) {
if (time >= interval.startTime && time <= interval.endTime) {
return interval;
}
}
return null;
}
Map<String, Object?> _sleepIntervalPayload(
HealthRawSleepInterval interval,
) {
return <String, Object?>{
'start_time': interval.startTime,
'end_time': interval.endTime,
};
}
Map<String, Object?> _localNotificationPayload(
HealthRawLocalNotification notification,
) {
... ... @@ -1805,6 +1934,10 @@ class AppleHealthRawDataCoreService {
return DateTime(date.year, date.month, date.day);
}
static (int startTime, int endTime) _dayRangeFromUnixSeconds(int seconds) {
return _dayRangeFromDateKey(_dateKeyFromUnixSeconds(seconds));
}
static (int startTime, int endTime) _dayRangeFromDateKey(int dateKey) {
final year = dateKey ~/ 10000;
final month = (dateKey ~/ 100) % 100;
... ... @@ -1816,6 +1949,16 @@ class AppleHealthRawDataCoreService {
);
}
static HealthRawSleepInterval _defaultSleepIntervalForUnixSeconds(
int seconds,
) {
final dayRange = _dayRangeFromUnixSeconds(seconds);
return (
startTime: dayRange.$1,
endTime: dayRange.$1 + 6 * 60 * 60,
);
}
static String _formatDateTimeMilliseconds(DateTime dateTime) {
String two(int value) => value.toString().padLeft(2, '0');
String three(int value) => value.toString().padLeft(3, '0');
... ... @@ -1914,6 +2057,8 @@ class _HealthRawLocalNotificationCandidateData {
double _integerDouble(num value) => value.toInt().toDouble();
int _unixSecondsNow() => DateTime.now().millisecondsSinceEpoch ~/ 1000;
class HealthRawStressLocalStore {
HealthRawStressLocalStore({
Directory? rootDirectory,
... ... @@ -1935,19 +2080,20 @@ class HealthRawStressLocalStore {
Future<void> upsertResult(HealthRawStressCalculationResult result) async {
final db = await _database(result.userId);
final updateTime = _currentUnixSeconds();
await db.transaction((txn) async {
for (final point in result.hrvStressPoints) {
await _upsertResettingUploaded(
txn,
hrvResultsTable,
_hrvRow(point),
_hrvRow(point, updateTime),
);
}
for (final point in result.realtimeStressPoints) {
await _upsertResettingUploaded(
txn,
realtimeStressResultsTable,
_realtimeRow(point),
_realtimeRow(point, updateTime),
);
}
});
... ... @@ -1958,9 +2104,13 @@ class HealthRawStressLocalStore {
required Iterable<HealthRawDailyStressPoint> points,
}) async {
final db = await _database(userId);
final updateTime = _currentUnixSeconds();
await db.transaction((txn) async {
for (final point in points) {
await _upsertDailyStressResettingUploaded(txn, _dailyStressRow(point));
await _upsertDailyStressResettingUploaded(
txn,
_dailyStressRow(point, updateTime),
);
}
});
}
... ... @@ -1970,9 +2120,13 @@ class HealthRawStressLocalStore {
required Iterable<HealthRawSleepResult> results,
}) async {
final db = await _database(userId);
final updateTime = _currentUnixSeconds();
await db.transaction((txn) async {
for (final result in results) {
await _upsertSleepResultResettingUploaded(txn, _sleepRow(result));
await _upsertSleepResultResettingUploaded(
txn,
_sleepRow(result, updateTime),
);
}
});
}
... ... @@ -2204,10 +2358,43 @@ class HealthRawStressLocalStore {
return _latestRawEndTime(userId, realtimeStressResultsTable);
}
Future<int?> latestRealtimeStressPushSendTime(int userId) {
return _latestPushSendTime(userId, realtimeStressResultsTable);
}
Future<int?> earliestRealtimeRawEndTime(int userId) {
return _earliestRawEndTime(userId, realtimeStressResultsTable);
}
Future<void> markLocalNotificationPushed({
required int userId,
required HealthRawLocalNotificationRecordType recordType,
required int recordTime,
required int pushSendTime,
}) async {
final (table, timeColumn) = switch (recordType) {
HealthRawLocalNotificationRecordType.hrv => (
hrvResultsTable,
'raw_end_time',
),
HealthRawLocalNotificationRecordType.realtimeStress => (
realtimeStressResultsTable,
'raw_end_time',
),
HealthRawLocalNotificationRecordType.sleep => (
sleepResultsTable,
'date',
),
};
final db = await _database(userId);
await db.update(
table,
<String, Object?>{'push_send_time': pushSendTime},
where: '$timeColumn = ?',
whereArgs: [recordTime],
);
}
Future<int?> latestSleepResultTime(int userId) async {
final db = await _database(userId);
final rows = await db.query(
... ... @@ -2347,7 +2534,7 @@ class HealthRawStressLocalStore {
final db = await factory.openDatabase(
path,
options: OpenDatabaseOptions(
version: 9,
version: 10,
onCreate: (db, version) async {
await _createTables(db);
},
... ... @@ -2377,6 +2564,9 @@ class HealthRawStressLocalStore {
if (oldVersion < 9) {
await _addDateKeyColumns(db);
}
if (oldVersion < 10) {
await _addUpdateTimeColumns(db);
}
},
),
);
... ... @@ -2404,7 +2594,9 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable (
is_sleep_likely INTEGER NOT NULL DEFAULT 0,
is_suspected_activity INTEGER NOT NULL DEFAULT 0,
uploaded INTEGER NOT NULL DEFAULT 0,
update_time INTEGER NOT NULL DEFAULT 0
update_time INTEGER NOT NULL DEFAULT 0,
push_send_time INTEGER,
upload_time INTEGER
)
''');
await db.execute('''
... ... @@ -2421,7 +2613,9 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable (
is_sleep_likely INTEGER NOT NULL DEFAULT 0,
is_suspected_activity INTEGER NOT NULL DEFAULT 0,
uploaded INTEGER NOT NULL DEFAULT 0,
update_time INTEGER NOT NULL DEFAULT 0
update_time INTEGER NOT NULL DEFAULT 0,
push_send_time INTEGER,
upload_time INTEGER
)
''');
await _createDailyStressTable(db);
... ... @@ -2439,7 +2633,8 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable (
state INTEGER NOT NULL,
data_time INTEGER NOT NULL,
uploaded INTEGER NOT NULL DEFAULT 0,
update_time INTEGER NOT NULL DEFAULT 0
update_time INTEGER NOT NULL DEFAULT 0,
upload_time INTEGER
)
''');
}
... ... @@ -2457,7 +2652,9 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
awak_minutes INTEGER NOT NULL,
sleep_minutes INTEGER NOT NULL,
uploaded INTEGER NOT NULL DEFAULT 0,
update_time INTEGER NOT NULL DEFAULT 0
update_time INTEGER NOT NULL DEFAULT 0,
push_send_time INTEGER,
upload_time INTEGER
)
''');
}
... ... @@ -2517,6 +2714,21 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
}
}
Future<void> _addPushSendTimeColumns(DatabaseExecutor db) async {
for (final table in const [
hrvResultsTable,
realtimeStressResultsTable,
dailyStressResultsTable,
sleepResultsTable,
]) {
try {
await db.execute('ALTER TABLE $table ADD COLUMN upload_time INTEGER');
} on DatabaseException catch (error) {
if (!error.isDuplicateColumnError()) rethrow;
}
}
}
Future<void> _addFlagColumns(DatabaseExecutor db, String table) async {
for (final column in const [
'is_workout',
... ... @@ -2600,6 +2812,19 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
return rows.first['raw_end_time'] as int;
}
Future<int?> _latestPushSendTime(int userId, String table) async {
final db = await _database(userId);
final rows = await db.query(
table,
columns: ['push_send_time'],
where: 'push_send_time IS NOT NULL',
orderBy: 'push_send_time DESC',
limit: 1,
);
if (rows.isEmpty) return null;
return rows.first['push_send_time'] as int?;
}
Future<int?> _earliestRawEndTime(int userId, String table) async {
final db = await _database(userId);
final rows = await db.query(
... ... @@ -2620,9 +2845,10 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
final times = rawEndTimes.toList();
if (times.isEmpty) return;
final db = await _database(userId);
final uploadTime = _currentUnixSeconds();
await db.update(
table,
{'uploaded': 1},
{'uploaded': 1, 'upload_time': uploadTime},
where: 'raw_end_time IN (${List.filled(times.length, '?').join(',')})',
whereArgs: times,
);
... ... @@ -2636,9 +2862,10 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
}) async {
if (time <= 0) return;
final db = await _database(userId);
final uploadTime = _currentUnixSeconds();
await db.update(
table,
{'uploaded': 1},
{'uploaded': 1, 'upload_time': uploadTime},
where: '$timeColumn <= ? AND uploaded != 1',
whereArgs: [time],
);
... ... @@ -2668,7 +2895,10 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
return date.year * 10000 + date.month * 100 + date.day;
}
Map<String, Object?> _hrvRow(HealthRawHrvStressPoint point) {
Map<String, Object?> _hrvRow(
HealthRawHrvStressPoint point,
int updateTime,
) {
return <String, Object?>{
'raw_end_time': point.rawEndTime,
'date_key': _dateKeyFromUnixSeconds(point.rawEndTime),
... ... @@ -2684,11 +2914,15 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'baseline_resting_hr': point.baselineRestingHr,
..._flagsRow(point.flags),
'uploaded': point.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
'update_time': updateTime,
'upload_time': point.uploadTime,
};
}
Map<String, Object?> _realtimeRow(HealthRawRealtimeStressPoint point) {
Map<String, Object?> _realtimeRow(
HealthRawRealtimeStressPoint point,
int updateTime,
) {
return <String, Object?>{
'raw_end_time': point.rawEndTime,
'date_key': _dateKeyFromUnixSeconds(point.rawEndTime),
... ... @@ -2699,11 +2933,15 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'source_end_time': point.sourceEndTime,
..._flagsRow(point.flags),
'uploaded': point.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
'update_time': updateTime,
'upload_time': point.uploadTime,
};
}
Map<String, Object?> _dailyStressRow(HealthRawDailyStressPoint point) {
Map<String, Object?> _dailyStressRow(
HealthRawDailyStressPoint point,
int updateTime,
) {
return <String, Object?>{
'date': point.date,
'date_key': point.date,
... ... @@ -2713,11 +2951,12 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'state': point.state.value,
'data_time': point.dataTime,
'uploaded': point.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
'update_time': updateTime,
'upload_time': point.uploadTime,
};
}
Map<String, Object?> _sleepRow(HealthRawSleepResult result) {
Map<String, Object?> _sleepRow(HealthRawSleepResult result, int updateTime) {
return <String, Object?>{
'date': result.date,
'date_key': _dateKeyFromUnixSeconds(result.date),
... ... @@ -2729,7 +2968,8 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'awak_minutes': result.awakMinutes,
'sleep_minutes': result.sleepMinutes,
'uploaded': result.uploaded ? 1 : 0,
'update_time': _currentUnixSeconds(),
'update_time': updateTime,
'upload_time': result.uploadTime,
};
}
... ... @@ -2798,6 +3038,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'is_sleep_likely': row['is_sleep_likely'],
'is_suspected_activity': row['is_suspected_activity'],
'uploaded': uploadPayloadChanged ? 0 : existingRow['uploaded'],
'upload_time': uploadPayloadChanged ? null : existingRow['upload_time'],
'update_time': row['update_time'],
},
where: 'raw_end_time = ?',
... ... @@ -2832,6 +3073,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
final isToday = row['date'] == _todayDateKey();
if (isToday) {
row['uploaded'] = 0;
row['upload_time'] = null;
}
final existing = await db.query(
dailyStressResultsTable,
... ... @@ -2884,6 +3126,8 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'state': row['state'],
'data_time': row['data_time'],
'uploaded': isToday ? 0 : (valueChanged ? 0 : existingRow['uploaded']),
'upload_time':
isToday || valueChanged ? null : existingRow['upload_time'],
'update_time': row['update_time'],
},
where: 'date = ?',
... ... @@ -2945,6 +3189,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
'awak_minutes': row['awak_minutes'],
'sleep_minutes': row['sleep_minutes'],
'uploaded': valueChanged ? 0 : existingRow['uploaded'],
'upload_time': valueChanged ? null : existingRow['upload_time'],
'update_time': row['update_time'],
},
where: 'date = ?',
... ...
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class HealthRawLocalNotificationDebugStore {
HealthRawLocalNotificationDebugStore({Directory? rootDirectory})
: _rootDirectory = rootDirectory;
static const _fileName = 'health_raw_local_notification_debug_events.json';
static const _maxEventCount = 500;
final Directory? _rootDirectory;
Future<void> append(Map<String, Object?> event) async {
final file = await _file();
await file.parent.create(recursive: true);
final eventsByMinute = await _readEventsByMinute(file);
final now = DateTime.now();
final minuteKey = _minuteKey(now);
final enrichedEvent = <String, Object?>{
..._sanitizeMap(event),
'saved_at': now.toIso8601String(),
'saved_at_readable': _readableTime(now),
'saved_at_minute': minuteKey,
'saved_at_unix': now.millisecondsSinceEpoch / 1000,
};
eventsByMinute.putIfAbsent(minuteKey, () => <Map<String, Object?>>[]);
eventsByMinute[minuteKey]!.add(enrichedEvent);
final limitedEventsByMinute = _limitedEventsByMinute(eventsByMinute);
const encoder = JsonEncoder.withIndent(' ');
await file.writeAsString(
encoder.convert(limitedEventsByMinute),
flush: true,
);
}
Future<String> readText() async {
final file = await _file();
if (!await file.exists()) {
return '暂无本地推送诊断日志';
}
final text = await file.readAsString();
if (text.trim().isEmpty) {
return '暂无本地推送诊断日志';
}
return text;
}
Future<File> _file() async {
final rootDirectory =
_rootDirectory ?? await getApplicationDocumentsDirectory();
return File('${rootDirectory.path}/$_fileName');
}
Future<Map<String, List<Map<String, Object?>>>> _readEventsByMinute(
File file,
) async {
if (!await file.exists()) {
return <String, List<Map<String, Object?>>>{};
}
final text = await file.readAsString();
if (text.trim().isEmpty) {
return <String, List<Map<String, Object?>>>{};
}
final json = jsonDecode(text);
if (json is Map) {
return json.map((key, value) {
final events = value is List
? value
.whereType<Map>()
.map((e) => _sanitizeMap(e.cast<String, Object?>()))
.toList()
: <Map<String, Object?>>[];
return MapEntry(key.toString(), events);
});
}
if (json is List) {
final events = json
.whereType<Map>()
.map((e) => _sanitizeMap(e.cast<String, Object?>()))
.toList();
final grouped = <String, List<Map<String, Object?>>>{};
for (final event in events) {
final key =
(event['saved_at_minute'] ?? event['saved_at_readable'] ?? 'legacy')
.toString();
grouped.putIfAbsent(key, () => <Map<String, Object?>>[]);
grouped[key]!.add(event);
}
return grouped;
}
return <String, List<Map<String, Object?>>>{};
}
Map<String, List<Map<String, Object?>>> _limitedEventsByMinute(
Map<String, List<Map<String, Object?>>> eventsByMinute,
) {
final events = eventsByMinute.values.expand((e) => e).toList()
..sort((a, b) {
final aTime = (a['saved_at_unix'] as num?)?.toDouble() ?? 0;
final bTime = (b['saved_at_unix'] as num?)?.toDouble() ?? 0;
return aTime.compareTo(bTime);
});
final limitedEvents = events.length > _maxEventCount
? events.sublist(events.length - _maxEventCount)
: events;
final grouped = <String, List<Map<String, Object?>>>{};
for (final event in limitedEvents) {
final key =
(event['saved_at_minute'] ?? event['saved_at_readable'] ?? 'unknown')
.toString();
grouped.putIfAbsent(key, () => <Map<String, Object?>>[]);
grouped[key]!.add(event);
}
return grouped;
}
static Map<String, Object?> _sanitizeMap(Map<String, Object?> map) {
return map.map((key, value) => MapEntry(key, _sanitizeValue(value)));
}
static Object? _sanitizeValue(Object? value) {
return switch (value) {
null => null,
String() => value,
num() => value,
bool() => value,
DateTime() => value.toIso8601String(),
List() => value.map(_sanitizeValue).toList(),
Map() => value.map(
(key, value) => MapEntry(key.toString(), _sanitizeValue(value)),
),
_ => value.toString(),
};
}
static String _minuteKey(DateTime time) {
return '${time.year.toString().padLeft(4, '0')}-'
'${time.month.toString().padLeft(2, '0')}-'
'${time.day.toString().padLeft(2, '0')} '
'${time.hour.toString().padLeft(2, '0')}:'
'${time.minute.toString().padLeft(2, '0')}';
}
static String _readableTime(DateTime time) {
return '${time.year.toString().padLeft(4, '0')}-'
'${time.month.toString().padLeft(2, '0')}-'
'${time.day.toString().padLeft(2, '0')} '
'${time.hour.toString().padLeft(2, '0')}:'
'${time.minute.toString().padLeft(2, '0')}:'
'${time.second.toString().padLeft(2, '0')}';
}
}