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:
@@ -7,10 +7,11 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.jlmak.household_keeper"
|
namespace = "com.jlmak.household_keeper"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = 35
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
@@ -42,3 +43,7 @@ android {
|
|||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="household_keeper"
|
android:label="household_keeper"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
@@ -30,6 +33,19 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
<receiver
|
||||||
|
android:exported="false"
|
||||||
|
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
|
||||||
|
<receiver
|
||||||
|
android:exported="true"
|
||||||
|
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
||||||
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,28 @@ class DailyPlanDao extends DatabaseAccessor<AppDatabase>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One-shot count of overdue + today tasks (for notification body).
|
||||||
|
Future<int> 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<int> 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;
|
||||||
|
}
|
||||||
|
|
||||||
/// Count task completions recorded today.
|
/// Count task completions recorded today.
|
||||||
/// Uses customSelect with readsFrom for proper stream invalidation.
|
/// Uses customSelect with readsFrom for proper stream invalidation.
|
||||||
Stream<int> watchCompletionsToday({DateTime? today}) {
|
Stream<int> watchCompletionsToday({DateTime? today}) {
|
||||||
|
|||||||
@@ -88,5 +88,23 @@
|
|||||||
"dailyPlanAllClearTitle": "Alles erledigt! \ud83c\udf1f",
|
"dailyPlanAllClearTitle": "Alles erledigt! \ud83c\udf1f",
|
||||||
"dailyPlanAllClearMessage": "Keine Aufgaben f\u00fcr heute. Genie\u00dfe den Moment!",
|
"dailyPlanAllClearMessage": "Keine Aufgaben f\u00fcr heute. Genie\u00dfe den Moment!",
|
||||||
"dailyPlanNoOverdue": "Keine \u00fcberf\u00e4lligen Aufgaben",
|
"dailyPlanNoOverdue": "Keine \u00fcberf\u00e4lligen Aufgaben",
|
||||||
"dailyPlanNoTasks": "Noch keine Aufgaben angelegt"
|
"dailyPlanNoTasks": "Noch keine Aufgaben angelegt",
|
||||||
|
"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",
|
||||||
|
"@notificationBody": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notificationBodyWithOverdue": "{count} Aufgaben fällig ({overdue} überfällig)",
|
||||||
|
"@notificationBodyWithOverdue": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" },
|
||||||
|
"overdue": { "type": "int" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_timezone/flutter_timezone.dart';
|
||||||
|
import 'package:timezone/data/latest_all.dart' as tz;
|
||||||
|
import 'package:timezone/timezone.dart' as tz;
|
||||||
|
|
||||||
import 'package:household_keeper/app.dart';
|
import 'package:household_keeper/app.dart';
|
||||||
|
import 'package:household_keeper/core/notifications/notification_service.dart';
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
tz.initializeTimeZones();
|
||||||
|
final timeZoneName = await FlutterTimezone.getLocalTimezone();
|
||||||
|
tz.setLocalLocation(tz.getLocation(timeZoneName));
|
||||||
|
await NotificationService().initialize();
|
||||||
runApp(const ProviderScope(child: App()));
|
runApp(const ProviderScope(child: App()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ dependencies:
|
|||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
shared_preferences: ^2.5.4
|
shared_preferences: ^2.5.4
|
||||||
flutter_reorderable_grid_view: ^5.6.0
|
flutter_reorderable_grid_view: ^5.6.0
|
||||||
|
flutter_local_notifications: ^21.0.0
|
||||||
|
timezone: ^0.11.0
|
||||||
|
flutter_timezone: ^1.0.8
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user