diet-guard/app/lib/services/background_check_service.dart
Krzysztof kuhy Rudnicki adbfb20e9a Add OAuth device flow, background notifications, and fix AGP9 release crash (Milestones 3–4)
M3 – GitHub OAuth device flow: replace PAT-paste with a guided "Connect
GitHub" button that runs the device-code flow; tapping with no client id
now opens a setup dialog (instructions + inline paste field) rather than
a buried inline hint. Bakes in the app's own OAuth App client id so fresh
installs work with zero manual config. Auto-syncs immediately after
connect. Verified end-to-end on the real phone: OAuth flow → token saved
→ PC's 48-entry log merged in (confirmed via food-bank vs manual source
labels in History).

M4 – Background meal-slot notifications: WorkManager periodic task (15 min
floor) checks for overdue slots and posts/cancels notifications via
flutter_local_notifications. New permissions: POST_NOTIFICATIONS,
WAKE_LOCK, RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
INTERNET (was missing — latent sync bug). "Disable battery optimization"
button in Settings. Verified on real phone: WorkManager registered, forced
run posted a real notification ("Meal not logged / You haven't logged your
16:00 meal yet."), isolated to background path (only caller is the
WorkManager dispatcher, not any foreground lifecycle hook).

AGP9 release crash fix: AGP 9 defaults isMinifyEnabled/isShrinkResources
to true for release even with no proguard config; R8 stripped
WorkDatabase_Impl's reflection-only constructor, crashing every launch
with NoSuchMethodException. Explicitly disabled both flags in
build.gradle.kts. Verified via dexdump (constructor present) and on-device
launch (no crash). Proper R8 keep rules are the long-term fix; tracked.

177 tests, flutter analyze clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SWPUBzE24Ls9i9GMRwXnnn
2026-06-25 17:29:23 +02:00

53 lines
2.3 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/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 == backgroundCheckTaskName) {
await checkAndNotify();
}
return true;
});
}
// coverage:ignore-end