feat(04-01): Android config, NotificationService, DAO queries, timezone init, ARB strings
- Add flutter_local_notifications ^21.0.0, timezone ^0.11.0, flutter_timezone ^1.0.8 to pubspec.yaml - Set compileSdk=35, enable core library desugaring in build.gradle.kts - Add POST_NOTIFICATIONS, RECEIVE_BOOT_COMPLETED permissions and boot receivers to AndroidManifest.xml - Create NotificationService singleton with initialize, requestPermission, scheduleDailyNotification, cancelAll - Add getOverdueAndTodayTaskCount and getOverdueTaskCount one-shot queries to DailyPlanDao - Initialize timezone and NotificationService in main.dart before runApp - Add 7 notification-related ARB strings to app_de.arb - All 72 existing tests pass
This commit is contained in:
80
lib/core/notifications/notification_service.dart
Normal file
80
lib/core/notifications/notification_service.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' show TimeOfDay;
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
class NotificationService {
|
||||
static final NotificationService _instance = NotificationService._internal();
|
||||
factory NotificationService() => _instance;
|
||||
NotificationService._internal();
|
||||
|
||||
final _plugin = FlutterLocalNotificationsPlugin();
|
||||
|
||||
Future<void> initialize() async {
|
||||
const android = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const settings = InitializationSettings(android: android);
|
||||
await _plugin.initialize(
|
||||
settings,
|
||||
onDidReceiveNotificationResponse: _onTap,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> requestPermission() async {
|
||||
final android = _plugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
return await android?.requestNotificationsPermission() ?? false;
|
||||
}
|
||||
|
||||
Future<void> scheduleDailyNotification({
|
||||
required TimeOfDay time,
|
||||
required String title,
|
||||
required String body,
|
||||
}) async {
|
||||
await _plugin.cancelAll();
|
||||
final scheduledDate = _nextInstanceOf(time);
|
||||
const details = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'daily_summary',
|
||||
'Tägliche Zusammenfassung',
|
||||
channelDescription: 'Tägliche Aufgaben-Erinnerung',
|
||||
importance: Importance.defaultImportance,
|
||||
priority: Priority.defaultPriority,
|
||||
),
|
||||
);
|
||||
await _plugin.zonedSchedule(
|
||||
0,
|
||||
title: title,
|
||||
body: body,
|
||||
scheduledDate: scheduledDate,
|
||||
details,
|
||||
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
|
||||
matchDateTimeComponents: DateTimeComponents.time,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> cancelAll() => _plugin.cancelAll();
|
||||
|
||||
/// Computes the next occurrence of [time] as a [tz.TZDateTime].
|
||||
/// Returns today if [time] is still in the future, tomorrow otherwise.
|
||||
@visibleForTesting
|
||||
tz.TZDateTime nextInstanceOf(TimeOfDay time) {
|
||||
final now = tz.TZDateTime.now(tz.local);
|
||||
var scheduled = tz.TZDateTime(
|
||||
tz.local,
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
);
|
||||
if (scheduled.isBefore(now)) {
|
||||
scheduled = scheduled.add(const Duration(days: 1));
|
||||
}
|
||||
return scheduled;
|
||||
}
|
||||
|
||||
static void _onTap(NotificationResponse response) {
|
||||
// Navigation to Home tab — wired in Plan 02 via global navigator key
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user