mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 19:33:55 +01:00
Immediate push on log only helps when the device is online. If a meal is logged while offline, the in-process auto-sync fails silently and the meal waited for the next app lifecycle event. Now each log also enqueues a one-off WorkManager task constrained to NetworkType.connected, so the log uploads as soon as connectivity returns, without the app being reopened. - background_sync_service.dart: backgroundSyncPush() loads SyncSettings and runs runSync, returning WorkManager's success/retry flag (true when nothing to push or the push succeeded; false to retry after a transient failure). Unit-testable half; the dispatcher branch and the one-off registration are integration-only (real background isolate) and coverage-ignored, matching the existing periodic check. - log_meal_screen.dart: _enqueueSyncBackstop() registers the task (replace policy coalesces a burst of logs; linear backoff retries) from both log paths. No-op off mobile. - background_check_service.dart: dispatcher routes the new task name. - log_storage_service.dart: writeLog is now atomic (temp file + rename) — the background push's runSync can write the log in a separate isolate while the foreground app also writes, so a torn read must be impossible. Verified on device (BL9000EEA0000102): airplane mode on -> log -> background; airplane mode off -> the phone's remote food_log.json commit advanced within ~15s, app never reopened (also confirms the token loads in the background isolate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FtSmzACzJSGyEatgK9exkd
59 lines
2.6 KiB
Dart
59 lines
2.6 KiB
Dart
/// WorkManager-driven periodic check: re-runs the same due/missing-slot
|
|
/// logic diet_guard's `_gate.py` uses to decide whether to lock the PC, and
|
|
/// syncs notifications to match. Registered as a 15-minute periodic task
|
|
/// (WorkManager's periodic floor) rather than four fixed exact alarms --
|
|
/// more robust against OEM background-kill behavior, at the cost of ±15 min
|
|
/// precision (accepted; see the project plan). Deliberately **not**
|
|
/// requesting `SCHEDULE_EXACT_ALARM` for this reason -- don't reach for it
|
|
/// to "fix" perceived lateness.
|
|
library;
|
|
|
|
import 'package:diet_guard_app/models/slot.dart';
|
|
import 'package:diet_guard_app/services/background_sync_service.dart';
|
|
import 'package:diet_guard_app/services/log_storage_service.dart';
|
|
import 'package:diet_guard_app/services/notification_service.dart';
|
|
import 'package:workmanager/workmanager.dart';
|
|
|
|
/// Unique WorkManager task name for the periodic due-slot check.
|
|
const String backgroundCheckTaskName = 'diet_guard.background_check';
|
|
|
|
/// Reads the local log, computes today's due-but-unlogged slots as of
|
|
/// [now] (defaults to the real clock), and syncs notifications to match.
|
|
///
|
|
/// Extracted from [backgroundCheckCallbackDispatcher] so this logic is
|
|
/// unit-testable without the real WorkManager plugin, which only runs as a
|
|
/// true background isolate on-device. [now] is injectable for the same
|
|
/// reason `slot.dart`'s functions are clock-free: a test should not depend
|
|
/// on the wall-clock hour it happens to run at.
|
|
Future<void> checkAndNotify({DateTime? now}) async {
|
|
await LogStorageService.init();
|
|
await NotificationService.init();
|
|
final logged = await LogStorageService.instance.loggedSlotsToday();
|
|
final due = missingSlots(now ?? DateTime.now(), logged);
|
|
await NotificationService.instance.syncToSlots(due);
|
|
}
|
|
|
|
/// WorkManager entry point invoked by the OS on each periodic tick.
|
|
///
|
|
/// Deliberately thin: all logic lives in [checkAndNotify] so it stays unit
|
|
/// testable. This dispatcher itself is integration-only -- manually
|
|
/// smoke-tested on-device (see the project plan's verification section),
|
|
/// not chased for unit coverage.
|
|
// coverage:ignore-start
|
|
@pragma('vm:entry-point')
|
|
void backgroundCheckCallbackDispatcher() {
|
|
Workmanager().executeTask((taskName, inputData) async {
|
|
if (taskName == syncPushTaskName) {
|
|
// Return the push's own success flag so a still-offline / failed push
|
|
// is retried with backoff rather than silently dropped.
|
|
return backgroundSyncPush();
|
|
}
|
|
if (taskName == backgroundCheckTaskName) {
|
|
await checkAndNotify();
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// coverage:ignore-end
|