241 lines
7.2 KiB
Dart
241 lines
7.2 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
import 'package:relationship_saver/features/local/local_models.dart';
|
|
import 'package:relationship_saver/features/reminders/scheduling/reminder_notification_intent_bus.dart';
|
|
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler.dart';
|
|
import 'package:timezone/timezone.dart' as tz;
|
|
|
|
const String _channelId = 'relationship_saver_reminders';
|
|
const String _channelName = 'Relationship Reminders';
|
|
const String _channelDescription =
|
|
'Reminders for relationship plans and follow-ups';
|
|
|
|
/// Schedules reminders with local notifications where platform support exists.
|
|
class LocalNotificationsReminderScheduler implements ReminderScheduler {
|
|
LocalNotificationsReminderScheduler({
|
|
ReminderNotificationsDriver? driver,
|
|
DateTime Function()? now,
|
|
ReminderNotificationIntentBus? intentBus,
|
|
}) : _driver =
|
|
driver ??
|
|
FlutterReminderNotificationsDriver(onTapPayload: intentBus?.emitTap),
|
|
_now = now ?? DateTime.now;
|
|
|
|
final ReminderNotificationsDriver _driver;
|
|
final DateTime Function() _now;
|
|
|
|
@override
|
|
Future<void> clearAll() async {
|
|
if (!_supportsSchedulingPlatform()) {
|
|
return;
|
|
}
|
|
await _driver.ensureInitialized();
|
|
await _driver.cancelAll();
|
|
}
|
|
|
|
@override
|
|
Future<void> reconcile(List<ReminderRule> reminders) async {
|
|
if (!_supportsSchedulingPlatform()) {
|
|
return;
|
|
}
|
|
await _driver.ensureInitialized();
|
|
await _driver.cancelAll();
|
|
|
|
final DateTime now = _now();
|
|
final List<ReminderRule> enabled = reminders
|
|
.where((ReminderRule reminder) => reminder.enabled)
|
|
.toList(growable: false);
|
|
|
|
for (final ReminderRule reminder in enabled) {
|
|
final DateTimeComponents? repeating = _repeatComponents(reminder.cadence);
|
|
if (repeating == null && reminder.nextAt.isBefore(now)) {
|
|
continue;
|
|
}
|
|
|
|
await _driver.schedule(
|
|
id: _notificationId(reminder.id),
|
|
title: reminder.title,
|
|
body: _buildBody(reminder),
|
|
scheduledAt: reminder.nextAt,
|
|
repeatComponents: repeating,
|
|
payload: reminder.id,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> requestPermissions() async {
|
|
if (!_supportsSchedulingPlatform()) {
|
|
return false;
|
|
}
|
|
await _driver.ensureInitialized();
|
|
return _driver.requestPermissions();
|
|
}
|
|
|
|
DateTimeComponents? _repeatComponents(ReminderCadence cadence) {
|
|
return switch (cadence) {
|
|
ReminderCadence.daily => DateTimeComponents.time,
|
|
ReminderCadence.weekly => DateTimeComponents.dayOfWeekAndTime,
|
|
ReminderCadence.monthly => DateTimeComponents.dayOfMonthAndTime,
|
|
};
|
|
}
|
|
|
|
String _buildBody(ReminderRule reminder) {
|
|
final String frequency = switch (reminder.cadence) {
|
|
ReminderCadence.daily => 'daily',
|
|
ReminderCadence.weekly => 'weekly',
|
|
ReminderCadence.monthly => 'monthly',
|
|
};
|
|
return 'Your $frequency reminder is due now.';
|
|
}
|
|
|
|
bool _supportsSchedulingPlatform() {
|
|
if (kIsWeb) {
|
|
return false;
|
|
}
|
|
|
|
return defaultTargetPlatform != TargetPlatform.linux;
|
|
}
|
|
|
|
int _notificationId(String raw) {
|
|
int hash = 0;
|
|
for (final int unit in raw.codeUnits) {
|
|
hash = ((hash * 31) + unit) & 0x7fffffff;
|
|
}
|
|
return hash;
|
|
}
|
|
}
|
|
|
|
/// Driver wrapper around the notification plugin for easier testing.
|
|
abstract class ReminderNotificationsDriver {
|
|
Future<void> ensureInitialized();
|
|
|
|
Future<void> cancelAll();
|
|
|
|
Future<void> schedule({
|
|
required int id,
|
|
required String title,
|
|
required String body,
|
|
required DateTime scheduledAt,
|
|
required DateTimeComponents? repeatComponents,
|
|
required String payload,
|
|
});
|
|
|
|
Future<bool> requestPermissions();
|
|
}
|
|
|
|
class FlutterReminderNotificationsDriver
|
|
implements ReminderNotificationsDriver {
|
|
FlutterReminderNotificationsDriver({
|
|
FlutterLocalNotificationsPlugin? plugin,
|
|
void Function(String payload)? onTapPayload,
|
|
}) : _plugin = plugin ?? FlutterLocalNotificationsPlugin(),
|
|
_onTapPayload = onTapPayload;
|
|
|
|
final FlutterLocalNotificationsPlugin _plugin;
|
|
final void Function(String payload)? _onTapPayload;
|
|
Future<void>? _initializeFuture;
|
|
|
|
@override
|
|
Future<void> ensureInitialized() {
|
|
return _initializeFuture ??= _initialize();
|
|
}
|
|
|
|
@override
|
|
Future<void> cancelAll() async {
|
|
await _plugin.cancelAll();
|
|
}
|
|
|
|
@override
|
|
Future<void> schedule({
|
|
required int id,
|
|
required String title,
|
|
required String body,
|
|
required DateTime scheduledAt,
|
|
required DateTimeComponents? repeatComponents,
|
|
required String payload,
|
|
}) async {
|
|
await _plugin.zonedSchedule(
|
|
id: id,
|
|
title: title,
|
|
body: body,
|
|
scheduledDate: tz.TZDateTime.from(scheduledAt.toUtc(), tz.UTC),
|
|
notificationDetails: _notificationDetails,
|
|
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
|
|
matchDateTimeComponents: repeatComponents,
|
|
payload: payload,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<bool> requestPermissions() async {
|
|
if (defaultTargetPlatform == TargetPlatform.android) {
|
|
return await _plugin
|
|
.resolvePlatformSpecificImplementation<
|
|
AndroidFlutterLocalNotificationsPlugin
|
|
>()
|
|
?.requestNotificationsPermission() ??
|
|
false;
|
|
}
|
|
|
|
if (defaultTargetPlatform == TargetPlatform.iOS) {
|
|
return await _plugin
|
|
.resolvePlatformSpecificImplementation<
|
|
IOSFlutterLocalNotificationsPlugin
|
|
>()
|
|
?.requestPermissions(alert: true, badge: true, sound: true) ??
|
|
false;
|
|
}
|
|
|
|
if (defaultTargetPlatform == TargetPlatform.macOS) {
|
|
return await _plugin
|
|
.resolvePlatformSpecificImplementation<
|
|
MacOSFlutterLocalNotificationsPlugin
|
|
>()
|
|
?.requestPermissions(alert: true, badge: true, sound: true) ??
|
|
false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Future<void> _initialize() async {
|
|
await _plugin.initialize(
|
|
settings: const InitializationSettings(
|
|
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
|
|
iOS: DarwinInitializationSettings(),
|
|
macOS: DarwinInitializationSettings(),
|
|
linux: LinuxInitializationSettings(defaultActionName: 'Open reminders'),
|
|
windows: WindowsInitializationSettings(
|
|
appName: 'Relationship Saver',
|
|
appUserModelId: 'com.relationshipsaver.app',
|
|
guid: 'ee23c0af-5898-4d96-b599-cf2d7d0d37d7',
|
|
),
|
|
),
|
|
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
|
final String? payload = response.payload;
|
|
if (payload == null || payload.isEmpty) {
|
|
return;
|
|
}
|
|
_onTapPayload?.call(payload);
|
|
},
|
|
);
|
|
}
|
|
|
|
NotificationDetails get _notificationDetails {
|
|
return const NotificationDetails(
|
|
android: AndroidNotificationDetails(
|
|
_channelId,
|
|
_channelName,
|
|
channelDescription: _channelDescription,
|
|
importance: Importance.high,
|
|
priority: Priority.high,
|
|
),
|
|
iOS: DarwinNotificationDetails(),
|
|
macOS: DarwinNotificationDetails(),
|
|
linux: LinuxNotificationDetails(),
|
|
windows: WindowsNotificationDetails(),
|
|
);
|
|
}
|
|
}
|