|
...
|
...
|
@@ -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 = ?',
|
...
|
...
|
|