--- phase: 04-notifications plan: 01 type: execute wave: 1 depends_on: [] files_modified: - pubspec.yaml - android/app/build.gradle.kts - android/app/src/main/AndroidManifest.xml - lib/main.dart - lib/core/notifications/notification_service.dart - lib/core/notifications/notification_settings_notifier.dart - lib/core/notifications/notification_settings_notifier.g.dart - lib/features/home/data/daily_plan_dao.dart - lib/features/home/data/daily_plan_dao.g.dart - lib/l10n/app_de.arb - test/core/notifications/notification_service_test.dart - test/core/notifications/notification_settings_notifier_test.dart autonomous: true requirements: - NOTF-01 - NOTF-02 must_haves: truths: - "NotificationService can schedule a daily notification at a given TimeOfDay" - "NotificationService can cancel all scheduled notifications" - "NotificationService can request POST_NOTIFICATIONS permission" - "NotificationSettingsNotifier persists enabled boolean and TimeOfDay to SharedPreferences" - "NotificationSettingsNotifier loads persisted values on build" - "DailyPlanDao can return a one-shot count of overdue + today tasks" - "Timezone is initialized before any notification scheduling" - "Android build compiles with core library desugaring enabled" - "AndroidManifest has POST_NOTIFICATIONS permission, RECEIVE_BOOT_COMPLETED permission, and boot receiver" artifacts: - path: "lib/core/notifications/notification_service.dart" provides: "Singleton wrapper around FlutterLocalNotificationsPlugin" exports: ["NotificationService"] - path: "lib/core/notifications/notification_settings_notifier.dart" provides: "Riverpod notifier for notification enabled + time" exports: ["NotificationSettings", "NotificationSettingsNotifier"] - path: "test/core/notifications/notification_service_test.dart" provides: "Unit tests for scheduling, cancel, permission" - path: "test/core/notifications/notification_settings_notifier_test.dart" provides: "Unit tests for persistence and state management" key_links: - from: "lib/core/notifications/notification_service.dart" to: "flutter_local_notifications" via: "FlutterLocalNotificationsPlugin" pattern: "FlutterLocalNotificationsPlugin" - from: "lib/core/notifications/notification_settings_notifier.dart" to: "shared_preferences" via: "SharedPreferences persistence" pattern: "SharedPreferences\\.getInstance" - from: "lib/main.dart" to: "lib/core/notifications/notification_service.dart" via: "timezone init + service initialize" pattern: "NotificationService.*initialize" --- Install notification packages, configure Android build system, create the NotificationService singleton and NotificationSettingsNotifier provider, add one-shot DAO query for task counts, initialize timezone in main.dart, add ARB strings, and write unit tests. Purpose: Establish the complete notification infrastructure so Plan 02 can wire it into the Settings UI. Output: Working notification service and settings notifier with full test coverage. Android build configuration complete. @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-notifications/04-CONTEXT.md @.planning/phases/04-notifications/04-RESEARCH.md From lib/core/theme/theme_provider.dart (pattern to follow for notifier): ```dart @riverpod class ThemeNotifier extends _$ThemeNotifier { @override ThemeMode build() { _loadPersistedThemeMode(); return ThemeMode.system; // sync default, then async load overrides } Future setThemeMode(ThemeMode mode) async { ... } } ``` From lib/features/home/data/daily_plan_dao.dart (DAO to extend): ```dart @DriftAccessor(tables: [Tasks, Rooms, TaskCompletions]) class DailyPlanDao extends DatabaseAccessor with _$DailyPlanDaoMixin { DailyPlanDao(super.attachedDatabase); Stream> watchAllTasksWithRoomName() { ... } Stream watchCompletionsToday({DateTime? today}) { ... } } ``` From lib/main.dart (entry point to modify): ```dart void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(const ProviderScope(child: App())); } ``` From lib/core/router/router.dart (top-level GoRouter for notification tap): ```dart final router = GoRouter( initialLocation: '/', routes: [ StatefulShellRoute.indexedStack(...) ], ); ``` From lib/l10n/app_de.arb (localization file, 92 existing keys): Last key: "dailyPlanNoTasks": "Noch keine Aufgaben angelegt" From android/app/build.gradle.kts: ```kotlin android { compileSdk = flutter.compileSdkVersion compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } } ``` From android/app/src/main/AndroidManifest.xml: No notification-related entries exist yet. Only standard Flutter activity + meta-data. Task 1: Android config, packages, NotificationService, timezone init, DAO query, ARB strings pubspec.yaml, android/app/build.gradle.kts, android/app/src/main/AndroidManifest.xml, lib/main.dart, lib/core/notifications/notification_service.dart, lib/features/home/data/daily_plan_dao.dart, lib/features/home/data/daily_plan_dao.g.dart, lib/l10n/app_de.arb 1. **Add packages** to pubspec.yaml dependencies: - `flutter_local_notifications: ^21.0.0` - `timezone: ^0.9.4` - `flutter_timezone: ^1.0.8` Run `flutter pub get`. 2. **Configure Android build** in `android/app/build.gradle.kts`: - Set `compileSdk = 35` (explicit, replacing `flutter.compileSdkVersion`) - Add `isCoreLibraryDesugaringEnabled = true` inside `compileOptions` - Add `coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")` in `dependencies` block 3. **Configure AndroidManifest.xml** — add inside `` (outside ``): - `` - `` Add inside ``: - `ScheduledNotificationReceiver` with `android:exported="false"` - `ScheduledNotificationBootReceiver` with `android:exported="true"` and intent-filter for BOOT_COMPLETED, MY_PACKAGE_REPLACED, QUICKBOOT_POWERON, HTC QUICKBOOT_POWERON Use the exact XML from RESEARCH.md Pattern section. 4. **Create NotificationService** at `lib/core/notifications/notification_service.dart`: - Singleton pattern (factory constructor + static `_instance`) - `final _plugin = FlutterLocalNotificationsPlugin();` - `Future initialize()`: AndroidInitializationSettings with `@mipmap/ic_launcher`, call `_plugin.initialize(settings, onDidReceiveNotificationResponse: _onTap)` - `Future requestPermission()`: resolve Android implementation, call `requestNotificationsPermission()`, return `granted ?? false` - `Future scheduleDailyNotification({required TimeOfDay time, required String title, required String body})`: - Call `_plugin.cancelAll()` first - Compute `_nextInstanceOf(time)` as TZDateTime - AndroidNotificationDetails: channelId `'daily_summary'`, channelName `'Tagliche Zusammenfassung'`, channelDescription `'Tagliche Aufgaben-Erinnerung'`, importance default, priority default - Call `_plugin.zonedSchedule(0, title: title, body: body, scheduledDate: scheduledDate, details, androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, matchDateTimeComponents: DateTimeComponents.time)` - `Future cancelAll()`: delegates to `_plugin.cancelAll()` - `tz.TZDateTime _nextInstanceOf(TimeOfDay time)`: compute next occurrence (today if in future, tomorrow otherwise) - `static void _onTap(NotificationResponse response)`: no-op for now (Plan 02 wires navigation) 5. **Add one-shot DAO query** to `lib/features/home/data/daily_plan_dao.dart`: ```dart /// One-shot count of overdue + today tasks (for notification body). Future getOverdueAndTodayTaskCount({DateTime? today}) async { final now = today ?? DateTime.now(); final endOfToday = DateTime(now.year, now.month, now.day + 1); final result = await (selectOnly(tasks) ..addColumns([tasks.id.count()]) ..where(tasks.nextDueDate.isSmallerThanValue(endOfToday))) .getSingle(); return result.read(tasks.id.count()) ?? 0; } /// One-shot count of overdue tasks only (for notification body split). Future getOverdueTaskCount({DateTime? today}) async { final now = today ?? DateTime.now(); final startOfToday = DateTime(now.year, now.month, now.day); final result = await (selectOnly(tasks) ..addColumns([tasks.id.count()]) ..where(tasks.nextDueDate.isSmallerThanValue(startOfToday))) .getSingle(); return result.read(tasks.id.count()) ?? 0; } ``` Run `dart run build_runner build --delete-conflicting-outputs` to regenerate DAO. 6. **Initialize timezone in main.dart** — before `NotificationService().initialize()`: ```dart import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/timezone.dart' as tz; import 'package:flutter_timezone/flutter_timezone.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); tz.initializeTimeZones(); final timeZoneName = await FlutterTimezone.getLocalTimezone(); tz.setLocalLocation(tz.getLocation(timeZoneName)); await NotificationService().initialize(); runApp(const ProviderScope(child: App())); } ``` 7. **Add ARB strings** to `lib/l10n/app_de.arb`: - `settingsSectionNotifications`: "Benachrichtigungen" - `notificationsEnabledLabel`: "Tägliche Erinnerung" - `notificationsTimeLabel`: "Uhrzeit" - `notificationsPermissionDeniedHint`: "Benachrichtigungen sind in den Systemeinstellungen deaktiviert. Tippe hier, um sie zu aktivieren." - `notificationTitle`: "Dein Tagesplan" - `notificationBody`: "{count} Aufgaben fällig" with `@notificationBody` placeholder `count: int` - `notificationBodyWithOverdue`: "{count} Aufgaben fällig ({overdue} überfällig)" with `@notificationBodyWithOverdue` placeholders `count: int, overdue: int` 8. Run `flutter pub get` and `flutter test` to confirm no regressions (expect 72 existing tests to pass). cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test - flutter_local_notifications, timezone, flutter_timezone in pubspec.yaml - build.gradle.kts has compileSdk=35, desugaring enabled, desugar_jdk_libs dependency - AndroidManifest has POST_NOTIFICATIONS, RECEIVE_BOOT_COMPLETED, both receivers - NotificationService exists with initialize, requestPermission, scheduleDailyNotification, cancelAll - DailyPlanDao has getOverdueAndTodayTaskCount and getOverdueTaskCount one-shot queries - main.dart initializes timezone and notification service before runApp - ARB file has 7 new notification-related keys - All 72 existing tests still pass Task 2: NotificationSettingsNotifier and unit tests lib/core/notifications/notification_settings_notifier.dart, lib/core/notifications/notification_settings_notifier.g.dart, test/core/notifications/notification_settings_notifier_test.dart, test/core/notifications/notification_service_test.dart - Test: NotificationSettingsNotifier build() returns default state (enabled=false, time=07:00) - Test: setEnabled(true) updates state.enabled to true and persists to SharedPreferences - Test: setEnabled(false) updates state.enabled to false and persists to SharedPreferences - Test: setTime(TimeOfDay(hour: 9, minute: 30)) updates state.time and persists hour+minute to SharedPreferences - Test: After _load() with existing prefs (enabled=true, hour=8, minute=15), state reflects persisted values - Test: NotificationService._nextInstanceOf returns today if time is in the future - Test: NotificationService._nextInstanceOf returns tomorrow if time has passed 1. **Create NotificationSettingsNotifier** at `lib/core/notifications/notification_settings_notifier.dart`: - Use `@Riverpod(keepAlive: true)` annotation (NOT plain `@riverpod`) to survive tab switches - `class NotificationSettings { final bool enabled; final TimeOfDay time; const NotificationSettings({required this.enabled, required this.time}); }` - `class NotificationSettingsNotifier extends _$NotificationSettingsNotifier` - `build()` returns `NotificationSettings(enabled: false, time: TimeOfDay(hour: 7, minute: 0))` synchronously, then calls `_load()` which async reads SharedPreferences and updates state - `Future setEnabled(bool enabled)`: update state, persist `notifications_enabled` bool - `Future setTime(TimeOfDay time)`: update state, persist `notifications_hour` int and `notifications_minute` int - Follow exact pattern from ThemeNotifier: sync return default, async load overrides state - Add `part 'notification_settings_notifier.g.dart';` 2. Run `dart run build_runner build --delete-conflicting-outputs` to generate `.g.dart`. 3. **Write tests** at `test/core/notifications/notification_settings_notifier_test.dart`: - Use `SharedPreferences.setMockInitialValues({})` for clean state - Use `SharedPreferences.setMockInitialValues({'notifications_enabled': true, 'notifications_hour': 8, 'notifications_minute': 15})` for pre-existing state - Create a `ProviderContainer` with the notifier, verify default state, call `setEnabled`/`setTime`, verify state updates and SharedPreferences values 4. **Write tests** at `test/core/notifications/notification_service_test.dart`: - Test `_nextInstanceOf` logic by extracting it to a `@visibleForTesting` static method or by testing `scheduleDailyNotification` with a mock plugin - Since `FlutterLocalNotificationsPlugin` dispatches to native and cannot be truly unit-tested, focus tests on: a. `_nextInstanceOf` returns correct TZDateTime (make it a package-private or `@visibleForTesting` method) b. Verify the service can be instantiated (singleton pattern) - Initialize timezone in test setUp: `tz.initializeTimeZones(); tz.setLocalLocation(tz.getLocation('Europe/Berlin'));` 5. Run `flutter test test/core/notifications/` to confirm new tests pass. 6. Run `flutter test` to confirm all tests pass (72 existing + new). cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/core/notifications/ - NotificationSettingsNotifier with @Riverpod(keepAlive: true) created and generated - NotificationSettings data class with enabled + time fields - SharedPreferences persistence for enabled, hour, minute - Unit tests for default state, setEnabled, setTime, persistence load - Unit tests for _nextInstanceOf timezone logic - All tests pass including existing 72 - `flutter test` — all tests pass (72 existing + new notification tests) - `dart analyze --fatal-infos` — no warnings or errors - `grep -r "flutter_local_notifications" pubspec.yaml` — package present - `grep -r "isCoreLibraryDesugaringEnabled" android/app/build.gradle.kts` — desugaring enabled - `grep -r "POST_NOTIFICATIONS" android/app/src/main/AndroidManifest.xml` — permission present - `grep -r "RECEIVE_BOOT_COMPLETED" android/app/src/main/AndroidManifest.xml` — permission present - `grep -r "ScheduledNotificationBootReceiver" android/app/src/main/AndroidManifest.xml` — receiver present - NotificationService singleton with initialize, requestPermission, scheduleDailyNotification, cancelAll - NotificationSettingsNotifier persists enabled + time to SharedPreferences - DailyPlanDao has one-shot overdue+today count queries - Android build configured for flutter_local_notifications v21 - Timezone initialized in main.dart - All tests pass, dart analyze clean After completion, create `.planning/phases/04-notifications/04-01-SUMMARY.md`