mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
adds retry functionality
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:supplements/logging.dart';
|
||||
import 'package:supplements/services/notification_debug_store.dart';
|
||||
import 'package:supplements/services/notification_router.dart';
|
||||
import 'package:timezone/data/latest.dart' as tzdata;
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
import 'dart:convert';
|
||||
import 'package:supplements/services/notification_router.dart';
|
||||
import 'package:supplements/services/notification_debug_store.dart';
|
||||
|
||||
import '../models/supplement.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
|
||||
/// A minimal notification scheduler focused purely on:
|
||||
/// - Initialization
|
||||
@@ -131,8 +133,12 @@ class SimpleNotificationService {
|
||||
///
|
||||
/// IDs:
|
||||
/// - Group ID per time slot: 40000 + hour*60 + minute.
|
||||
/// - Retry IDs: 50000 + (hour*60 + minute)*100 + retryIndex.
|
||||
/// - Stable and predictable for cancel/update operations.
|
||||
Future<void> scheduleDailyGroupedReminders(List<Supplement> supplements) async {
|
||||
Future<void> scheduleDailyGroupedReminders(
|
||||
List<Supplement> supplements, {
|
||||
SettingsProvider? settingsProvider,
|
||||
}) async {
|
||||
if (!_initialized) {
|
||||
await initialize();
|
||||
}
|
||||
@@ -281,6 +287,20 @@ class SimpleNotificationService {
|
||||
);
|
||||
|
||||
printLog('✅ Scheduled group $timeKey with ID $id');
|
||||
|
||||
// Schedule retry notifications if enabled
|
||||
if (settingsProvider != null && settingsProvider.notificationRetryEnabled) {
|
||||
await _scheduleRetryNotifications(
|
||||
timeKey: timeKey,
|
||||
supplements: items,
|
||||
isSingle: isSingle,
|
||||
title: title,
|
||||
body: body,
|
||||
payloadStr: payloadStr,
|
||||
retryCount: settingsProvider.notificationRetryCount,
|
||||
retryDelayMinutes: settingsProvider.notificationRetryDelayMinutes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Log what the system reports as pending
|
||||
@@ -297,13 +317,16 @@ class SimpleNotificationService {
|
||||
|
||||
/// Convenience to schedule grouped reminders for today and tomorrow.
|
||||
///
|
||||
/// For iOS’s 64 limit, we stick to one day for recurring (matchDateTimeComponents)
|
||||
/// For iOS's 64 limit, we stick to one day for recurring (matchDateTimeComponents)
|
||||
/// which already repeats every day without needing to schedule future dates.
|
||||
/// If you want an extra safety net, you could schedule tomorrow’s one-offs,
|
||||
/// If you want an extra safety net, you could schedule tomorrow's one-offs,
|
||||
/// but with daily components this is unnecessary and risks hitting iOS limits.
|
||||
Future<void> scheduleDailyGroupedRemindersSafe(List<Supplement> supplements) async {
|
||||
// For now, just schedule today’s recurring groups.
|
||||
await scheduleDailyGroupedReminders(supplements);
|
||||
Future<void> scheduleDailyGroupedRemindersSafe(
|
||||
List<Supplement> supplements, {
|
||||
SettingsProvider? settingsProvider,
|
||||
}) async {
|
||||
// For now, just schedule today's recurring groups.
|
||||
await scheduleDailyGroupedReminders(supplements, settingsProvider: settingsProvider);
|
||||
}
|
||||
|
||||
/// Cancel all scheduled reminders for a given [supplementId].
|
||||
@@ -565,4 +588,176 @@ class SimpleNotificationService {
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Schedule retry notifications for a specific time group.
|
||||
Future<void> _scheduleRetryNotifications({
|
||||
required String timeKey,
|
||||
required List<Supplement> supplements,
|
||||
required bool isSingle,
|
||||
required String title,
|
||||
required String body,
|
||||
required String payloadStr,
|
||||
required int retryCount,
|
||||
required int retryDelayMinutes,
|
||||
}) async {
|
||||
if (retryCount <= 0) return;
|
||||
|
||||
final parts = timeKey.split(':');
|
||||
final hour = int.parse(parts[0]);
|
||||
final minute = int.parse(parts[1]);
|
||||
final baseTime = _nextInstanceOfTime(hour, minute);
|
||||
|
||||
for (int retryIndex = 1; retryIndex <= retryCount; retryIndex++) {
|
||||
final retryDelay = Duration(minutes: retryDelayMinutes * retryIndex);
|
||||
final retryTime = baseTime.add(retryDelay);
|
||||
final retryId = 50000 + ((hour * 60) + minute) * 100 + retryIndex;
|
||||
|
||||
// Parse and modify payload to mark as retry
|
||||
Map<String, dynamic> retryPayload;
|
||||
try {
|
||||
retryPayload = Map<String, dynamic>.from(jsonDecode(payloadStr));
|
||||
retryPayload['meta'] = {
|
||||
'kind': 'retry',
|
||||
'originalTime': timeKey,
|
||||
'retryIndex': retryIndex,
|
||||
'retryOf': 40000 + (hour * 60) + minute,
|
||||
};
|
||||
} catch (e) {
|
||||
retryPayload = {
|
||||
'type': isSingle ? 'single' : 'group',
|
||||
if (isSingle) 'id': supplements.first.id else 'time': timeKey,
|
||||
'meta': {
|
||||
'kind': 'retry',
|
||||
'originalTime': timeKey,
|
||||
'retryIndex': retryIndex,
|
||||
'retryOf': 40000 + (hour * 60) + minute,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
final retryPayloadStr = jsonEncode(retryPayload);
|
||||
final retryTitle = 'Reminder: $title';
|
||||
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
_channelDailyId,
|
||||
_channelDailyName,
|
||||
channelDescription: _channelDailyDescription,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
styleInformation: BigTextStyleInformation(
|
||||
body,
|
||||
contentTitle: retryTitle,
|
||||
htmlFormatContentTitle: false,
|
||||
),
|
||||
actions: [
|
||||
if (isSingle)
|
||||
AndroidNotificationAction(
|
||||
'take_single',
|
||||
'Take',
|
||||
showsUserInterface: true,
|
||||
cancelNotification: true,
|
||||
)
|
||||
else
|
||||
AndroidNotificationAction(
|
||||
'take_group',
|
||||
'Take All',
|
||||
showsUserInterface: true,
|
||||
cancelNotification: true,
|
||||
),
|
||||
if (isSingle)
|
||||
AndroidNotificationAction(
|
||||
'snooze_single',
|
||||
'Snooze',
|
||||
showsUserInterface: false,
|
||||
)
|
||||
else
|
||||
AndroidNotificationAction(
|
||||
'snooze_group',
|
||||
'Snooze',
|
||||
showsUserInterface: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final iosDetails = DarwinNotificationDetails(
|
||||
categoryIdentifier: isSingle ? 'single' : 'group',
|
||||
);
|
||||
|
||||
await _plugin.zonedSchedule(
|
||||
retryId,
|
||||
retryTitle,
|
||||
isSingle ? body : 'Tap to see details',
|
||||
retryTime,
|
||||
NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
),
|
||||
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
|
||||
payload: retryPayloadStr,
|
||||
);
|
||||
|
||||
// Log to debug store
|
||||
final createdAtMs = DateTime.now().millisecondsSinceEpoch;
|
||||
await NotificationDebugStore.instance.add(
|
||||
NotificationLogEntry(
|
||||
id: retryId,
|
||||
kind: 'retry',
|
||||
type: isSingle ? 'single' : 'group',
|
||||
whenEpochMs: retryTime.millisecondsSinceEpoch,
|
||||
createdAtEpochMs: createdAtMs,
|
||||
title: retryTitle,
|
||||
payload: retryPayloadStr,
|
||||
singleId: isSingle ? supplements.first.id : null,
|
||||
timeKey: isSingle ? null : timeKey,
|
||||
),
|
||||
);
|
||||
|
||||
printLog('🔄 Scheduled retry $retryIndex/$retryCount for $timeKey (ID: $retryId) at $retryTime');
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel retry notifications for a specific time slot.
|
||||
/// This should be called when a user responds to a notification.
|
||||
Future<void> cancelRetryNotificationsForTimeSlot(String timeKey) async {
|
||||
if (!_initialized) {
|
||||
await initialize();
|
||||
}
|
||||
|
||||
final parts = timeKey.split(':');
|
||||
if (parts.length != 2) return;
|
||||
|
||||
final hour = int.tryParse(parts[0]);
|
||||
final minute = int.tryParse(parts[1]);
|
||||
if (hour == null || minute == null) return;
|
||||
|
||||
// Calculate base retry ID range for this time slot
|
||||
final baseRetryId = 50000 + ((hour * 60) + minute) * 100;
|
||||
|
||||
// Cancel up to 10 possible retries (generous upper bound)
|
||||
for (int retryIndex = 1; retryIndex <= 10; retryIndex++) {
|
||||
final retryId = baseRetryId + retryIndex;
|
||||
await _plugin.cancel(retryId);
|
||||
printLog('🚫 Cancelled retry notification ID: $retryId for time slot $timeKey');
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel retry notifications for a specific supplement ID.
|
||||
/// This iterates through all possible time slots.
|
||||
Future<void> cancelRetryNotificationsForSupplement(int supplementId) async {
|
||||
if (!_initialized) {
|
||||
await initialize();
|
||||
}
|
||||
|
||||
// Cancel retries for all possible time slots (24 hours * 60 minutes)
|
||||
for (int hour = 0; hour < 24; hour++) {
|
||||
for (int minute = 0; minute < 60; minute += 5) { // Assume 5-minute intervals
|
||||
final baseRetryId = 50000 + ((hour * 60) + minute) * 100;
|
||||
for (int retryIndex = 1; retryIndex <= 10; retryIndex++) {
|
||||
final retryId = baseRetryId + retryIndex;
|
||||
await _plugin.cancel(retryId);
|
||||
}
|
||||
}
|
||||
}
|
||||
printLog('🚫 Cancelled all retry notifications for supplement ID: $supplementId');
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user