diet-guard/app/lib/services/background_check_service.dart
Krzysztof kuhy Rudnicki 67bc98c7e0
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
app: offline backstop — connectivity-gated push on reconnect
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
2026-07-11 19:11:54 +02:00

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