Commit 412d71340899e3c34deef5e4149da70906505425

Authored by 权海
1 parent 3a10895f

feat(ui):增加上传日志

@@ -21,6 +21,8 @@ class DeveloperOptionsItem { @@ -21,6 +21,8 @@ class DeveloperOptionsItem {
21 21
22 enum DeveloperOptionsAction { 22 enum DeveloperOptionsAction {
23 shareHealthRawDataDatabase, 23 shareHealthRawDataDatabase,
  24 + shareHealthRawUploadLog,
  25 + clearHealthRawDataDatabaseAndUploadLog,
24 } 26 }
25 27
26 class DeveloperOptionsController extends GetxController { 28 class DeveloperOptionsController extends GetxController {
@@ -51,6 +53,14 @@ class DeveloperOptionsController extends GetxController { @@ -51,6 +53,14 @@ class DeveloperOptionsController extends GetxController {
51 title: 'Share HealthRawData Database', 53 title: 'Share HealthRawData Database',
52 action: DeveloperOptionsAction.shareHealthRawDataDatabase, 54 action: DeveloperOptionsAction.shareHealthRawDataDatabase,
53 ), 55 ),
  56 + DeveloperOptionsItem(
  57 + title: '本地上传日志',
  58 + action: DeveloperOptionsAction.shareHealthRawUploadLog,
  59 + ),
  60 + DeveloperOptionsItem(
  61 + title: '清除本地数据库、上传日志',
  62 + action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
  63 + ),
54 ]; 64 ];
55 65
56 @override 66 @override
@@ -70,6 +80,12 @@ class DeveloperOptionsController extends GetxController { @@ -70,6 +80,12 @@ class DeveloperOptionsController extends GetxController {
70 case DeveloperOptionsAction.shareHealthRawDataDatabase: 80 case DeveloperOptionsAction.shareHealthRawDataDatabase:
71 await shareHealthRawDataDatabase(); 81 await shareHealthRawDataDatabase();
72 break; 82 break;
  83 + case DeveloperOptionsAction.shareHealthRawUploadLog:
  84 + await shareHealthRawUploadLog();
  85 + break;
  86 + case DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog:
  87 + await clearHealthRawDataDatabaseAndUploadLog();
  88 + break;
73 case null: 89 case null:
74 break; 90 break;
75 } 91 }
@@ -85,6 +101,30 @@ class DeveloperOptionsController extends GetxController { @@ -85,6 +101,30 @@ class DeveloperOptionsController extends GetxController {
85 } 101 }
86 } 102 }
87 103
  104 + Future<void> shareHealthRawUploadLog() async {
  105 + try {
  106 + final text = await _healthRawDataCoreService.readUploadApiLogText();
  107 + await _platformHostApi.shareText(text);
  108 + } catch (error, stackTrace) {
  109 + AppLogger.e('Share HealthRawData upload log failed', error, stackTrace);
  110 + Get.snackbar('Share failed', error.toString());
  111 + }
  112 + }
  113 +
  114 + Future<void> clearHealthRawDataDatabaseAndUploadLog() async {
  115 + try {
  116 + await _healthRawDataCoreService.clearLocalDatabaseAndUploadLog();
  117 + Get.snackbar('已清除', '本地数据库和上传日志已清除');
  118 + } catch (error, stackTrace) {
  119 + AppLogger.e(
  120 + 'Clear HealthRawData database and upload log failed',
  121 + error,
  122 + stackTrace,
  123 + );
  124 + Get.snackbar('Clear failed', error.toString());
  125 + }
  126 + }
  127 +
88 Future<void> saveAccessToken() async { 128 Future<void> saveAccessToken() async {
89 final token = tokenController.text.trim(); 129 final token = tokenController.text.trim();
90 await _userPreferencesStorage.updateAccessToken(token); 130 await _userPreferencesStorage.updateAccessToken(token);
@@ -51,6 +51,8 @@ class HealthRawDataCoreService { @@ -51,6 +51,8 @@ class HealthRawDataCoreService {
51 bool _isUploadingDailyStressResults = false; 51 bool _isUploadingDailyStressResults = false;
52 bool _isUploadingSleepResults = false; 52 bool _isUploadingSleepResults = false;
53 53
  54 + bool get _isDebug => _environmentConfig?.isDebug ?? false;
  55 +
54 int get _userId { 56 int get _userId {
55 final userId = _userIdProvider?.call() ?? 0; 57 final userId = _userIdProvider?.call() ?? 0;
56 if (userId <= 0) { 58 if (userId <= 0) {
@@ -132,6 +134,30 @@ class HealthRawDataCoreService { @@ -132,6 +134,30 @@ class HealthRawDataCoreService {
132 return bytes; 134 return bytes;
133 } 135 }
134 136
  137 + Future<String> readUploadApiLogText() async {
  138 + final file = await _uploadApiLogFile();
  139 + if (!await file.exists()) {
  140 + return _emptyUploadApiLogText;
  141 + }
  142 + final text = await file.readAsString();
  143 + if (text.trim().isEmpty) {
  144 + return _emptyUploadApiLogText;
  145 + }
  146 + return text;
  147 + }
  148 +
  149 + Future<void> clearLocalDatabaseAndUploadLog() async {
  150 + await _localStore.clearTables(_userId);
  151 + await clearUploadApiLog();
  152 + }
  153 +
  154 + Future<void> clearUploadApiLog() async {
  155 + final file = await _uploadApiLogFile();
  156 + if (await file.exists()) {
  157 + await file.delete();
  158 + }
  159 + }
  160 +
135 Future<HealthRawStressCalculationResult> startCoreCaculate({ 161 Future<HealthRawStressCalculationResult> startCoreCaculate({
136 int? endTime, 162 int? endTime,
137 int readChunkDays = defaultReadChunkDays, 163 int readChunkDays = defaultReadChunkDays,
@@ -533,8 +559,11 @@ class HealthRawDataCoreService { @@ -533,8 +559,11 @@ class HealthRawDataCoreService {
533 } 559 }
534 _isUploadingHrvResults = true; 560 _isUploadingHrvResults = true;
535 try { 561 try {
536 - final uploadedUntil = await _rawDataApi.performHRVDataUpload(  
537 - sqliteFilePath: await _localStore.dbPath(_userId), 562 + final uploadedUntil = await _callUploadApi<int>(
  563 + apiName: 'performHRVDataUpload',
  564 + body: () async => _rawDataApi.performHRVDataUpload(
  565 + sqliteFilePath: await _localStore.dbPath(_userId),
  566 + ),
538 ); 567 );
539 if (uploadedUntil > 0) { 568 if (uploadedUntil > 0) {
540 await _localStore.markHrvStressUploadedUntil( 569 await _localStore.markHrvStressUploadedUntil(
@@ -558,8 +587,11 @@ class HealthRawDataCoreService { @@ -558,8 +587,11 @@ class HealthRawDataCoreService {
558 } 587 }
559 _isUploadingRealtimeStressResults = true; 588 _isUploadingRealtimeStressResults = true;
560 try { 589 try {
561 - final uploadedUntil = await _rawDataApi.performHRDataUpload(  
562 - sqliteFilePath: await _localStore.dbPath(_userId), 590 + final uploadedUntil = await _callUploadApi<int>(
  591 + apiName: 'performHRDataUpload',
  592 + body: () async => _rawDataApi.performHRDataUpload(
  593 + sqliteFilePath: await _localStore.dbPath(_userId),
  594 + ),
563 ); 595 );
564 if (uploadedUntil > 0) { 596 if (uploadedUntil > 0) {
565 await _localStore.markRealtimeStressUploadedUntil( 597 await _localStore.markRealtimeStressUploadedUntil(
@@ -583,9 +615,11 @@ class HealthRawDataCoreService { @@ -583,9 +615,11 @@ class HealthRawDataCoreService {
583 } 615 }
584 _isUploadingDailyStressResults = true; 616 _isUploadingDailyStressResults = true;
585 try { 617 try {
586 - final uploadedUntil =  
587 - await _rawDataApi.performAvgRealtimeStressDataUpload(  
588 - sqliteFilePath: await _localStore.dbPath(_userId), 618 + final uploadedUntil = await _callUploadApi<int>(
  619 + apiName: 'performAvgRealtimeStressDataUpload',
  620 + body: () async => _rawDataApi.performAvgRealtimeStressDataUpload(
  621 + sqliteFilePath: await _localStore.dbPath(_userId),
  622 + ),
589 ); 623 );
590 if (uploadedUntil > 0) { 624 if (uploadedUntil > 0) {
591 await _localStore.markDailyStressUploadedUntil( 625 await _localStore.markDailyStressUploadedUntil(
@@ -609,8 +643,11 @@ class HealthRawDataCoreService { @@ -609,8 +643,11 @@ class HealthRawDataCoreService {
609 } 643 }
610 _isUploadingSleepResults = true; 644 _isUploadingSleepResults = true;
611 try { 645 try {
612 - final uploadedUntil = await _rawDataApi.performSleepAnalysisDataUpload(  
613 - sqliteFilePath: await _localStore.dbPath(_userId), 646 + final uploadedUntil = await _callUploadApi<int>(
  647 + apiName: 'performSleepAnalysisDataUpload',
  648 + body: () async => _rawDataApi.performSleepAnalysisDataUpload(
  649 + sqliteFilePath: await _localStore.dbPath(_userId),
  650 + ),
614 ); 651 );
615 if (uploadedUntil > 0) { 652 if (uploadedUntil > 0) {
616 await _localStore.markSleepResultsUploadedUntil( 653 await _localStore.markSleepResultsUploadedUntil(
@@ -625,6 +662,54 @@ class HealthRawDataCoreService { @@ -625,6 +662,54 @@ class HealthRawDataCoreService {
625 } 662 }
626 } 663 }
627 664
  665 + Future<T> _callUploadApi<T>({
  666 + required String apiName,
  667 + required Future<T> Function() body,
  668 + }) async {
  669 + final calledAt = DateTime.now();
  670 + try {
  671 + final result = await body();
  672 + await _appendUploadApiLog(
  673 + calledAt: calledAt,
  674 + apiName: apiName,
  675 + result: result,
  676 + );
  677 + return result;
  678 + } catch (error) {
  679 + await _appendUploadApiLog(
  680 + calledAt: calledAt,
  681 + apiName: apiName,
  682 + result: 'ERROR: $error',
  683 + );
  684 + rethrow;
  685 + }
  686 + }
  687 +
  688 + Future<void> _appendUploadApiLog({
  689 + required DateTime calledAt,
  690 + required String apiName,
  691 + required Object? result,
  692 + }) async {
  693 + if (!_isDebug) return;
  694 + final file = await _uploadApiLogFile();
  695 + await file.parent.create(recursive: true);
  696 + final line =
  697 + '${_formatDateTimeMilliseconds(calledAt)} $apiName result=$result\n';
  698 + await file.writeAsString(line, mode: FileMode.append, flush: true);
  699 + }
  700 +
  701 + Future<File> _uploadApiLogFile() async {
  702 + final dir = await getApplicationDocumentsDirectory();
  703 + return File('${dir.path}/health_raw_upload_debug.log');
  704 + }
  705 +
  706 + String get _emptyUploadApiLogText {
  707 + if (_isDebug) {
  708 + return '暂无本地上传日志';
  709 + }
  710 + return '当前非 Debug 模式,未记录本地上传日志';
  711 + }
  712 +
628 HealthRawStressCalculationResult calculate({ 713 HealthRawStressCalculationResult calculate({
629 required List<HealthKitRawDataPoint> hrvPoints, 714 required List<HealthKitRawDataPoint> hrvPoints,
630 required List<HealthKitRawDataPoint> heartRatePoints, 715 required List<HealthKitRawDataPoint> heartRatePoints,
@@ -1432,6 +1517,20 @@ class HealthRawStressLocalStore { @@ -1432,6 +1517,20 @@ class HealthRawStressLocalStore {
1432 ); 1517 );
1433 } 1518 }
1434 1519
  1520 + Future<void> clearTables(int userId) async {
  1521 + final db = await _database(userId);
  1522 + await db.transaction((txn) async {
  1523 + for (final table in const [
  1524 + hrvResultsTable,
  1525 + realtimeStressResultsTable,
  1526 + dailyStressResultsTable,
  1527 + sleepResultsTable,
  1528 + ]) {
  1529 + await txn.delete(table);
  1530 + }
  1531 + });
  1532 + }
  1533 +
1435 Future<void> ensureReadable(int userId) async { 1534 Future<void> ensureReadable(int userId) async {
1436 final db = await _database(userId); 1535 final db = await _database(userId);
1437 await db.query( 1536 await db.query(
@@ -1944,6 +2043,11 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -1944,6 +2043,11 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
1944 return rows.isNotEmpty; 2043 return rows.isNotEmpty;
1945 } 2044 }
1946 2045
  2046 + int _todayDateKey() {
  2047 + final now = DateTime.now();
  2048 + return now.year * 10000 + now.month * 100 + now.day;
  2049 + }
  2050 +
1947 Map<String, Object?> _hrvRow(HealthRawHrvStressPoint point) { 2051 Map<String, Object?> _hrvRow(HealthRawHrvStressPoint point) {
1948 return <String, Object?>{ 2052 return <String, Object?>{
1949 'raw_end_time': point.rawEndTime, 2053 'raw_end_time': point.rawEndTime,
@@ -2068,6 +2172,10 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2068,6 +2172,10 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2068 DatabaseExecutor db, 2172 DatabaseExecutor db,
2069 Map<String, Object?> row, 2173 Map<String, Object?> row,
2070 ) async { 2174 ) async {
  2175 + final isToday = row['date'] == _todayDateKey();
  2176 + if (isToday) {
  2177 + row['uploaded'] = 0;
  2178 + }
2071 final existing = await db.query( 2179 final existing = await db.query(
2072 dailyStressResultsTable, 2180 dailyStressResultsTable,
2073 where: 'date = ?', 2181 where: 'date = ?',
@@ -2092,7 +2200,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2092,7 +2200,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2092 'stress_score': row['stress_score'], 2200 'stress_score': row['stress_score'],
2093 'state': row['state'], 2201 'state': row['state'],
2094 'data_time': row['data_time'], 2202 'data_time': row['data_time'],
2095 - 'uploaded': valueChanged ? 0 : existingRow['uploaded'], 2203 + 'uploaded': isToday ? 0 : (valueChanged ? 0 : existingRow['uploaded']),
2096 }, 2204 },
2097 where: 'date = ?', 2205 where: 'date = ?',
2098 whereArgs: [row['date']], 2206 whereArgs: [row['date']],
@@ -240,6 +240,106 @@ void main() { @@ -240,6 +240,106 @@ void main() {
240 expect(daily.single.uploaded, isFalse); 240 expect(daily.single.uploaded, isFalse);
241 }); 241 });
242 242
  243 + test('daily stress marks uploaded after upload and resets today on calculate',
  244 + () async {
  245 + final now = DateTime.now();
  246 + final todayDate = now.year * 10000 + now.month * 100 + now.day;
  247 + final nowSeconds = now.millisecondsSinceEpoch ~/ 1000;
  248 + final store = _MemoryHealthRawStressLocalStore();
  249 + store.insertDailyStress(
  250 + HealthRawDailyStressPoint(
  251 + userId: 42,
  252 + date: todayDate,
  253 + stressValue: 40,
  254 + stressScore: 40,
  255 + state: HealthRawStressState.normal,
  256 + dataTime: nowSeconds,
  257 + ),
  258 + );
  259 +
  260 + await store.markDailyStressUploadedUntil(userId: 42, date: todayDate);
  261 + var daily = await store.queryDailyStressPoints(
  262 + userId: 42,
  263 + startDate: todayDate,
  264 + endDate: todayDate,
  265 + );
  266 + expect(daily.single.uploaded, isTrue);
  267 +
  268 + await store.upsertDailyStressPoints(
  269 + userId: 42,
  270 + points: [
  271 + HealthRawDailyStressPoint(
  272 + userId: 42,
  273 + date: todayDate,
  274 + stressValue: 40,
  275 + stressScore: 40,
  276 + state: HealthRawStressState.normal,
  277 + dataTime: nowSeconds + 1,
  278 + ),
  279 + ],
  280 + );
  281 + daily = await store.queryDailyStressPoints(
  282 + userId: 42,
  283 + startDate: todayDate,
  284 + endDate: todayDate,
  285 + );
  286 + expect(daily.single.uploaded, isFalse);
  287 + });
  288 +
  289 + test('daily stress recalculates today before native upload', () async {
  290 + final operationLog = <String>[];
  291 + final now = DateTime.now();
  292 + final todayStart = DateTime(now.year, now.month, now.day);
  293 + final todayDate =
  294 + todayStart.year * 10000 + todayStart.month * 100 + todayStart.day;
  295 + final startSeconds = todayStart.millisecondsSinceEpoch ~/ 1000;
  296 + final api = _FakeHealthKitRawDataHostApi(operationLog: operationLog)
  297 + ..dailyStressUploadUntil = todayDate;
  298 + final store = _MemoryHealthRawStressLocalStore(operationLog: operationLog);
  299 + await store.upsertResult(
  300 + HealthRawStressCalculationResult(
  301 + userId: 42,
  302 + hrvStressPoints: const <HealthRawHrvStressPoint>[],
  303 + realtimeStressPoints: [
  304 + _realtimeStressPoint(startSeconds + 60),
  305 + _realtimeStressPoint(startSeconds + 120),
  306 + _realtimeStressPoint(startSeconds + 180),
  307 + ],
  308 + dailyStressPoints: const <HealthRawDailyStressPoint>[],
  309 + ),
  310 + );
  311 + store.insertDailyStress(
  312 + HealthRawDailyStressPoint(
  313 + userId: 42,
  314 + date: todayDate,
  315 + stressValue: 30,
  316 + stressScore: 30,
  317 + state: HealthRawStressState.normal,
  318 + dataTime: startSeconds,
  319 + uploaded: true,
  320 + ),
  321 + );
  322 + final service = HealthRawDataCoreService(
  323 + healthApi: _FakeHealthKitHostApi(),
  324 + rawDataApi: api,
  325 + localStore: store,
  326 + userIdProvider: () => 42,
  327 + );
  328 +
  329 + await service.startCoreCaculate(
  330 + endTime: startSeconds + 240,
  331 + readChunkDays: 1,
  332 + );
  333 +
  334 + expect(
  335 + operationLog.indexOf('upsertDailyStressPoints'),
  336 + lessThan(operationLog.indexOf('performAvgRealtimeStressDataUpload')),
  337 + );
  338 + expect(api.dailyStressUploadCallCount, 1);
  339 + final rows = await store.debugQueryAllRows(42);
  340 + expect(rows.dailyStressRows.single['uploaded'], 1);
  341 + });
  342 +
243 test('same calculated rows keep uploaded state', () async { 343 test('same calculated rows keep uploaded state', () async {
244 final store = _MemoryHealthRawStressLocalStore(); 344 final store = _MemoryHealthRawStressLocalStore();
245 const uploadedDaily = HealthRawDailyStressPoint( 345 const uploadedDaily = HealthRawDailyStressPoint(
@@ -414,6 +514,77 @@ void main() { @@ -414,6 +514,77 @@ void main() {
414 expect(rows.sleepRows.single['uploaded'], 1); 514 expect(rows.sleepRows.single['uploaded'], 1);
415 }); 515 });
416 516
  517 + test(
  518 + 'startCoreCaculate uploads pending rows even when no new data calculates',
  519 + () async {
  520 + const base = 1800000000;
  521 + final oldDate = DateTime.fromMillisecondsSinceEpoch(base * 1000);
  522 + final oldDateKey = oldDate.year * 10000 + oldDate.month * 100 + oldDate.day;
  523 + final api = _FakeHealthKitRawDataHostApi()
  524 + ..hrvUploadUntil = base + 10
  525 + ..hrUploadUntil = base + 20
  526 + ..dailyStressUploadUntil = oldDateKey
  527 + ..sleepUploadUntil = base + 30;
  528 + final store = _MemoryHealthRawStressLocalStore();
  529 + await store.upsertResult(
  530 + HealthRawStressCalculationResult(
  531 + userId: 42,
  532 + hrvStressPoints: [_hrvStressPoint(base + 10)],
  533 + realtimeStressPoints: [_realtimeStressPoint(base + 20)],
  534 + dailyStressPoints: const <HealthRawDailyStressPoint>[],
  535 + ),
  536 + );
  537 + store.insertDailyStress(
  538 + HealthRawDailyStressPoint(
  539 + userId: 42,
  540 + date: oldDateKey,
  541 + stressValue: 40,
  542 + stressScore: 40,
  543 + state: HealthRawStressState.normal,
  544 + dataTime: base + 20,
  545 + ),
  546 + );
  547 + await store.upsertSleepResults(
  548 + userId: 42,
  549 + results: const [
  550 + HealthRawSleepResult(
  551 + userId: 42,
  552 + date: base + 30,
  553 + startDate: base,
  554 + sleepScore: 80,
  555 + sleepState: 2,
  556 + inBedMinutes: 500,
  557 + awakMinutes: 20,
  558 + sleepMinutes: 480,
  559 + ),
  560 + ],
  561 + );
  562 + final service = HealthRawDataCoreService(
  563 + healthApi: _FakeHealthKitHostApi(),
  564 + rawDataApi: api,
  565 + localStore: store,
  566 + userIdProvider: () => 42,
  567 + );
  568 +
  569 + final result = await service.startCoreCaculate(
  570 + endTime: base + 40,
  571 + readChunkDays: 1,
  572 + );
  573 + final rows = await store.debugQueryAllRows(42);
  574 +
  575 + expect(result.hrvStressPoints, isEmpty);
  576 + expect(result.realtimeStressPoints, isEmpty);
  577 + expect(result.sleepResults, isEmpty);
  578 + expect(api.hrvUploadCallCount, 1);
  579 + expect(api.hrUploadCallCount, 1);
  580 + expect(api.dailyStressUploadCallCount, 1);
  581 + expect(api.sleepUploadCallCount, 1);
  582 + expect(rows.hrvRows.single['uploaded'], 1);
  583 + expect(rows.realtimeRows.single['uploaded'], 1);
  584 + expect(rows.dailyStressRows.single['uploaded'], 1);
  585 + expect(rows.sleepRows.single['uploaded'], 1);
  586 + });
  587 +
417 test('local data source returns earliest local hr start time', () async { 588 test('local data source returns earliest local hr start time', () async {
418 const base = 1800000000; 589 const base = 1800000000;
419 final api = _FakeHealthKitRawDataHostApi(); 590 final api = _FakeHealthKitRawDataHostApi();
@@ -472,14 +643,40 @@ HealthRawRealtimeStressPoint _realtimeStressPoint(int time) { @@ -472,14 +643,40 @@ HealthRawRealtimeStressPoint _realtimeStressPoint(int time) {
472 ); 643 );
473 } 644 }
474 645
  646 +HealthRawHrvStressPoint _hrvStressPoint(int time) {
  647 + return HealthRawHrvStressPoint(
  648 + userId: 42,
  649 + rawEndTime: time,
  650 + rawHrv: 30,
  651 + result: 30,
  652 + sourceStartTime: time,
  653 + sourceEndTime: time,
  654 + state: HealthRawStressState.normal,
  655 + baselineHrv: 30,
  656 + baselineAwakeHrv: 30,
  657 + baselineSleepHrv: null,
  658 + baselineRestingHr: 60,
  659 + );
  660 +}
  661 +
475 class _FakeHealthKitRawDataHostApi extends HealthKitRawDataHostApi { 662 class _FakeHealthKitRawDataHostApi extends HealthKitRawDataHostApi {
  663 + _FakeHealthKitRawDataHostApi({List<String>? operationLog})
  664 + : operationLog = operationLog ?? <String>[];
  665 +
476 final Map<int, List<HealthKitRawDataPoint>> _pointsByType = {}; 666 final Map<int, List<HealthKitRawDataPoint>> _pointsByType = {};
477 final List<HealthKitRawWorkoutDataPoint> _workoutPoints = []; 667 final List<HealthKitRawWorkoutDataPoint> _workoutPoints = [];
478 final List<HealthKitRawDataPoint> _sleepPoints = []; 668 final List<HealthKitRawDataPoint> _sleepPoints = [];
479 final List<_ReadCall> calls = []; 669 final List<_ReadCall> calls = [];
  670 + final List<String> operationLog;
480 var sleepCallCount = 0; 671 var sleepCallCount = 0;
481 var workoutCallCount = 0; 672 var workoutCallCount = 0;
  673 + var hrUploadCallCount = 0;
  674 + var hrvUploadCallCount = 0;
  675 + var dailyStressUploadCallCount = 0;
482 var sleepUploadCallCount = 0; 676 var sleepUploadCallCount = 0;
  677 + var hrUploadUntil = 0;
  678 + var hrvUploadUntil = 0;
  679 + var dailyStressUploadUntil = 0;
483 var sleepUploadUntil = 0; 680 var sleepUploadUntil = 0;
484 var hasData = true; 681 var hasData = true;
485 682
@@ -519,16 +716,25 @@ class _FakeHealthKitRawDataHostApi extends HealthKitRawDataHostApi { @@ -519,16 +716,25 @@ class _FakeHealthKitRawDataHostApi extends HealthKitRawDataHostApi {
519 } 716 }
520 717
521 @override 718 @override
522 - Future<int> performHRDataUpload({required String sqliteFilePath}) async => 0; 719 + Future<int> performHRDataUpload({required String sqliteFilePath}) async {
  720 + hrUploadCallCount += 1;
  721 + return hrUploadUntil;
  722 + }
523 723
524 @override 724 @override
525 - Future<int> performHRVDataUpload({required String sqliteFilePath}) async => 0; 725 + Future<int> performHRVDataUpload({required String sqliteFilePath}) async {
  726 + hrvUploadCallCount += 1;
  727 + return hrvUploadUntil;
  728 + }
526 729
527 @override 730 @override
528 Future<int> performAvgRealtimeStressDataUpload({ 731 Future<int> performAvgRealtimeStressDataUpload({
529 required String sqliteFilePath, 732 required String sqliteFilePath,
530 - }) async =>  
531 - 0; 733 + }) async {
  734 + operationLog.add('performAvgRealtimeStressDataUpload');
  735 + dailyStressUploadCallCount += 1;
  736 + return dailyStressUploadUntil;
  737 + }
532 738
533 @override 739 @override
534 Future<int> performSleepAnalysisDataUpload({ 740 Future<int> performSleepAnalysisDataUpload({
@@ -588,11 +794,14 @@ class _ReadCall { @@ -588,11 +794,14 @@ class _ReadCall {
588 } 794 }
589 795
590 class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { 796 class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
  797 + _MemoryHealthRawStressLocalStore({List<String>? operationLog})
  798 + : operationLog = operationLog ?? <String>[];
  799 +
591 final Map<int, List<HealthRawHrvStressPoint>> _hrv = {}; 800 final Map<int, List<HealthRawHrvStressPoint>> _hrv = {};
592 final Map<int, List<HealthRawRealtimeStressPoint>> _realtime = {}; 801 final Map<int, List<HealthRawRealtimeStressPoint>> _realtime = {};
593 final Map<int, List<HealthRawDailyStressPoint>> _daily = {}; 802 final Map<int, List<HealthRawDailyStressPoint>> _daily = {};
594 final Map<int, List<HealthRawSleepResult>> _sleep = {}; 803 final Map<int, List<HealthRawSleepResult>> _sleep = {};
595 - final List<String> operationLog = []; 804 + final List<String> operationLog;
596 805
597 void insertDailyStress(HealthRawDailyStressPoint point) { 806 void insertDailyStress(HealthRawDailyStressPoint point) {
598 final byDate = <int, HealthRawDailyStressPoint>{ 807 final byDate = <int, HealthRawDailyStressPoint>{
@@ -683,6 +892,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -683,6 +892,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
683 existing.stressValue == point.stressValue && 892 existing.stressValue == point.stressValue &&
684 existing.stressScore == point.stressScore && 893 existing.stressScore == point.stressScore &&
685 existing.state == point.state; 894 existing.state == point.state;
  895 + final isToday = point.date == _todayDateKey();
686 byDate[point.date] = HealthRawDailyStressPoint( 896 byDate[point.date] = HealthRawDailyStressPoint(
687 userId: point.userId, 897 userId: point.userId,
688 date: point.date, 898 date: point.date,
@@ -690,7 +900,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -690,7 +900,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
690 stressScore: point.stressScore, 900 stressScore: point.stressScore,
691 state: point.state, 901 state: point.state,
692 dataTime: point.dataTime, 902 dataTime: point.dataTime,
693 - uploaded: same ? existing.uploaded : false, 903 + uploaded: isToday ? false : (same ? existing.uploaded : false),
694 ); 904 );
695 } 905 }
696 _daily[userId] = byDate.values.toList() 906 _daily[userId] = byDate.values.toList()
@@ -737,6 +947,14 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -737,6 +947,14 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
737 } 947 }
738 948
739 @override 949 @override
  950 + Future<void> clearTables(int userId) async {
  951 + _hrv.remove(userId);
  952 + _realtime.remove(userId);
  953 + _daily.remove(userId);
  954 + _sleep.remove(userId);
  955 + }
  956 +
  957 + @override
740 Future<List<HealthRawHrvStressPoint>> queryHrvStressPoints({ 958 Future<List<HealthRawHrvStressPoint>> queryHrvStressPoints({
741 required int userId, 959 required int userId,
742 required int startTime, 960 required int startTime,
@@ -1068,4 +1286,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { @@ -1068,4 +1286,9 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore {
1068 'uploaded': result.uploaded ? 1 : 0, 1286 'uploaded': result.uploaded ? 1 : 0,
1069 }; 1287 };
1070 } 1288 }
  1289 +
  1290 + int _todayDateKey() {
  1291 + final now = DateTime.now();
  1292 + return now.year * 10000 + now.month * 100 + now.day;
  1293 + }
1071 } 1294 }