import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:timezone/timezone.dart' as tz; import 'package:timezone/data/latest.dart' as tz; import '../models/supplement.dart'; import 'database_helper.dart'; // Top-level function to handle notification responses when app is running @pragma('vm:entry-point') void notificationTapBackground(NotificationResponse notificationResponse) { print('SupplementsLog: πŸ“± === BACKGROUND NOTIFICATION RESPONSE ==='); print('SupplementsLog: πŸ“± Action ID: ${notificationResponse.actionId}'); print('SupplementsLog: πŸ“± Payload: ${notificationResponse.payload}'); print('SupplementsLog: πŸ“± Notification ID: ${notificationResponse.id}'); print('SupplementsLog: πŸ“± =========================================='); // For now, just log the action. The main app handler will process it. if (notificationResponse.actionId == 'take_supplement') { print('SupplementsLog: πŸ“± BACKGROUND: Take action detected'); } else if (notificationResponse.actionId == 'snooze_10') { print('SupplementsLog: πŸ“± BACKGROUND: Snooze action detected'); } } class NotificationService { static final NotificationService _instance = NotificationService._internal(); factory NotificationService() => _instance; NotificationService._internal(); final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin(); bool _isInitialized = false; static bool _engineInitialized = false; bool _permissionsRequested = false; // Callback for handling supplement intake from notifications Function(int supplementId, String supplementName, double units, String unitType)? _onTakeSupplementCallback; // Set callback for handling supplement intake from notifications void setTakeSupplementCallback(Function(int supplementId, String supplementName, double units, String unitType) callback) { _onTakeSupplementCallback = callback; } Future initialize() async { print('SupplementsLog: πŸ“± Initializing NotificationService...'); if (_isInitialized) { print('SupplementsLog: πŸ“± Already initialized'); return; } try { print('SupplementsLog: πŸ“± Initializing timezones...'); print('SupplementsLog: πŸ“± Engine initialized flag: $_engineInitialized'); if (!_engineInitialized) { tz.initializeTimeZones(); _engineInitialized = true; print('SupplementsLog: πŸ“± Timezones initialized successfully'); } else { print('SupplementsLog: πŸ“± Timezones already initialized, skipping'); } } catch (e) { print('SupplementsLog: πŸ“± Warning: Timezone initialization issue (may already be initialized): $e'); _engineInitialized = true; // Mark as initialized to prevent retry } // Try to detect and set the local timezone more reliably try { // First try using the system timezone name final String timeZoneName = DateTime.now().timeZoneName; print('SupplementsLog: πŸ“± System timezone name: $timeZoneName'); tz.Location? location; // Try common timezone mappings for your region if (timeZoneName.contains('CET') || timeZoneName.contains('CEST')) { location = tz.getLocation('Europe/Amsterdam'); // Netherlands } else if (timeZoneName.contains('UTC') || timeZoneName.contains('GMT')) { location = tz.getLocation('UTC'); } else { // Fallback: try to use the timezone name directly try { location = tz.getLocation(timeZoneName); } catch (e) { print('SupplementsLog: πŸ“± Could not find timezone $timeZoneName, using Europe/Amsterdam as default'); location = tz.getLocation('Europe/Amsterdam'); } } tz.setLocalLocation(location); print('SupplementsLog: πŸ“± Timezone set to: ${location.name}'); } catch (e) { print('SupplementsLog: πŸ“± Error setting timezone: $e, using default'); // Fallback to a reasonable default for Netherlands tz.setLocalLocation(tz.getLocation('Europe/Amsterdam')); } print('SupplementsLog: πŸ“± Current local time: ${tz.TZDateTime.now(tz.local)}'); print('SupplementsLog: πŸ“± Current system time: ${DateTime.now()}'); const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); const DarwinInitializationSettings iosSettings = DarwinInitializationSettings( requestAlertPermission: false, // We'll request these separately requestBadgePermission: false, requestSoundPermission: false, ); const LinuxInitializationSettings linuxSettings = LinuxInitializationSettings( defaultActionName: 'Open notification', ); const InitializationSettings initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, linux: linuxSettings, ); print('SupplementsLog: πŸ“± Initializing flutter_local_notifications...'); await _notifications.initialize( initSettings, onDidReceiveNotificationResponse: _onNotificationResponse, onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); // Test if notification response callback is working print('SupplementsLog: πŸ“± Callback function is set and ready'); _isInitialized = true; print('SupplementsLog: πŸ“± NotificationService initialization complete'); } // Handle notification responses (when user taps on notification or action) void _onNotificationResponse(NotificationResponse response) { print('SupplementsLog: πŸ“± === NOTIFICATION RESPONSE ==='); print('SupplementsLog: πŸ“± Action ID: ${response.actionId}'); print('SupplementsLog: πŸ“± Payload: ${response.payload}'); print('SupplementsLog: πŸ“± Notification ID: ${response.id}'); print('SupplementsLog: πŸ“± Input: ${response.input}'); print('SupplementsLog: πŸ“± ==============================='); if (response.actionId == 'take_supplement') { print('SupplementsLog: πŸ“± Processing TAKE action...'); _handleTakeAction(response.payload, response.id); } else if (response.actionId == 'snooze_10') { print('SupplementsLog: πŸ“± Processing SNOOZE action...'); _handleSnoozeAction(response.payload, 10, response.id); } else { print('SupplementsLog: πŸ“± Default notification tap (no specific action)'); // Default tap (no actionId) opens the app normally } } Future _handleTakeAction(String? payload, int? notificationId) async { print('SupplementsLog: πŸ“± === HANDLING TAKE ACTION ==='); print('SupplementsLog: πŸ“± Payload received: $payload'); if (payload != null) { try { // Parse the payload to get supplement info final parts = payload.split('|'); print('SupplementsLog: πŸ“± Payload parts: $parts (length: ${parts.length})'); if (parts.length >= 4) { final supplementId = int.parse(parts[0]); final supplementName = parts[1]; final units = double.parse(parts[2]); final unitType = parts[3]; print('SupplementsLog: πŸ“± Parsed data:'); print('SupplementsLog: πŸ“± - ID: $supplementId'); print('SupplementsLog: πŸ“± - Name: $supplementName'); print('SupplementsLog: πŸ“± - Units: $units'); print('SupplementsLog: πŸ“± - Type: $unitType'); // Call the callback to record the intake if (_onTakeSupplementCallback != null) { print('SupplementsLog: πŸ“± Calling supplement callback...'); _onTakeSupplementCallback!( supplementId, supplementName, units, unitType); print('SupplementsLog: πŸ“± Callback completed'); } else { print('SupplementsLog: πŸ“± ERROR: No callback registered!'); } // For retry notifications, the original notification ID is in the payload int originalNotificationId; if (parts.length > 4 && int.tryParse(parts[4]) != null) { originalNotificationId = int.parse(parts[4]); print( 'SupplementsLog: πŸ“± Retry notification detected. Original ID: $originalNotificationId'); } else if (notificationId != null) { originalNotificationId = notificationId; } else { print( 'SupplementsLog: πŸ“± ERROR: Could not determine notification ID to cancel.'); return; } // Mark notification as taken in database (this will cancel any pending retries) print( 'SupplementsLog: πŸ“± Marking notification $originalNotificationId as taken'); await DatabaseHelper.instance .markNotificationTaken(originalNotificationId); // Cancel any pending retry notifications for this notification _cancelRetryNotifications(originalNotificationId); // Show a confirmation notification print('SupplementsLog: πŸ“± Showing confirmation notification...'); showInstantNotification( 'Supplement Taken!', '$supplementName has been recorded at ${DateTime.now().hour.toString().padLeft(2, '0')}:${DateTime.now().minute.toString().padLeft(2, '0')}', ); } else { print( 'SupplementsLog: πŸ“± ERROR: Invalid payload format - not enough parts'); } } catch (e) { print('SupplementsLog: πŸ“± ERROR in _handleTakeAction: $e'); } } else { print('SupplementsLog: πŸ“± ERROR: Payload is null'); } print('SupplementsLog: πŸ“± === TAKE ACTION COMPLETE ==='); } void _cancelRetryNotifications(int notificationId) { // Retry notifications use ID range starting from 200000 for (int i = 0; i < 10; i++) { // Cancel up to 10 potential retries int retryId = 200000 + (notificationId * 10) + i; _notifications.cancel(retryId); print('SupplementsLog: πŸ“± Cancelled retry notification ID: $retryId'); } } void _handleSnoozeAction(String? payload, int minutes, int? notificationId) { print('SupplementsLog: πŸ“± === HANDLING SNOOZE ACTION ==='); print('SupplementsLog: πŸ“± Payload: $payload, Minutes: $minutes'); if (payload != null) { try { final parts = payload.split('|'); if (parts.length >= 2) { final supplementId = int.parse(parts[0]); final supplementName = parts[1]; print('SupplementsLog: πŸ“± Snoozing supplement for $minutes minutes: $supplementName'); // Mark notification as snoozed in database (increment retry count) if (notificationId != null) { print('SupplementsLog: πŸ“± Incrementing retry count for notification $notificationId'); DatabaseHelper.instance.incrementRetryCount(notificationId); } // Schedule a new notification for the snooze time final snoozeTime = tz.TZDateTime.now(tz.local).add(Duration(minutes: minutes)); print('SupplementsLog: πŸ“± Snooze time: $snoozeTime'); _notifications.zonedSchedule( supplementId * 1000 + minutes, // Unique ID for snooze notifications 'Reminder: $supplementName', 'Snoozed reminder - Take your $supplementName now', snoozeTime, NotificationDetails( android: AndroidNotificationDetails( 'supplement_reminders', 'Supplement Reminders', channelDescription: 'Notifications for supplement intake reminders', importance: Importance.high, priority: Priority.high, actions: [ AndroidNotificationAction( 'take_supplement', 'Take', ), AndroidNotificationAction( 'snooze_10', 'Snooze 10min', ), ], ), iOS: const DarwinNotificationDetails(), ), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, payload: payload, ); showInstantNotification( 'Reminder Snoozed', '$supplementName reminder snoozed for $minutes minutes', ); print('SupplementsLog: πŸ“± Snooze scheduled successfully'); } } catch (e) { print('SupplementsLog: πŸ“± Error handling snooze action: $e'); } } print('SupplementsLog: πŸ“± === SNOOZE ACTION COMPLETE ==='); } /// Check for persistent reminders from app context with settings Future checkPersistentReminders( bool persistentReminders, int reminderRetryInterval, int maxRetryAttempts, ) async { await schedulePersistentReminders( persistentReminders: persistentReminders, reminderRetryInterval: reminderRetryInterval, maxRetryAttempts: maxRetryAttempts, ); } /// Check for pending notifications that need retry and schedule them Future schedulePersistentReminders({ required bool persistentReminders, required int reminderRetryInterval, required int maxRetryAttempts, }) async { print('SupplementsLog: πŸ“± Checking for pending notifications to retry...'); try { if (!persistentReminders) { print('SupplementsLog: πŸ“± Persistent reminders disabled'); return; } print('SupplementsLog: πŸ“± Retry settings: interval=$reminderRetryInterval min, max=$maxRetryAttempts attempts'); // Get all pending notifications from database final pendingNotifications = await DatabaseHelper.instance.getPendingNotifications(); print('SupplementsLog: πŸ“± Found ${pendingNotifications.length} pending notifications'); final now = DateTime.now(); for (final notification in pendingNotifications) { final scheduledTime = DateTime.parse(notification['scheduledTime']).toLocal(); final retryCount = notification['retryCount'] as int; final lastRetryTime = notification['lastRetryTime'] != null ? DateTime.parse(notification['lastRetryTime']).toLocal() : null; // Check if notification is overdue final timeSinceScheduled = now.difference(scheduledTime).inMinutes; final shouldRetry = timeSinceScheduled >= reminderRetryInterval; print('SupplementsLog: πŸ“± Checking notification ${notification['notificationId']}:'); print('SupplementsLog: πŸ“± Scheduled: $scheduledTime (local)'); print('SupplementsLog: πŸ“± Now: $now'); print('SupplementsLog: πŸ“± Time since scheduled: $timeSinceScheduled minutes'); print('SupplementsLog: πŸ“± Retry interval: $reminderRetryInterval minutes'); print('SupplementsLog: πŸ“± Should retry: $shouldRetry'); print('SupplementsLog: πŸ“± Retry count: $retryCount / $maxRetryAttempts'); // Check if we haven't exceeded max retry attempts if (retryCount >= maxRetryAttempts) { print('SupplementsLog: πŸ“± Notification ${notification['notificationId']} exceeded max attempts ($maxRetryAttempts)'); continue; } // Check if enough time has passed since last retry if (lastRetryTime != null) { final timeSinceLastRetry = now.difference(lastRetryTime).inMinutes; if (timeSinceLastRetry < reminderRetryInterval) { print('SupplementsLog: πŸ“± Notification ${notification['notificationId']} not ready for retry yet'); continue; } } if (shouldRetry) { print('SupplementsLog: πŸ“± ⚑ SCHEDULING RETRY for notification ${notification['notificationId']}'); await _scheduleRetryNotification(notification, retryCount + 1); } else { print('SupplementsLog: πŸ“± ⏸️ NOT READY FOR RETRY: ${notification['notificationId']}'); } } } catch (e) { print('SupplementsLog: πŸ“± Error scheduling persistent reminders: $e'); } } Future _scheduleRetryNotification(Map notification, int retryAttempt) async { try { final notificationId = notification['notificationId'] as int; final supplementId = notification['supplementId'] as int; // Generate a unique ID for this retry (200000 + original_id * 10 + retry_attempt) final retryNotificationId = 200000 + (notificationId * 10) + retryAttempt; print('SupplementsLog: πŸ“± Scheduling retry notification $retryNotificationId for supplement $supplementId (attempt $retryAttempt)'); // Get supplement details from database final supplements = await DatabaseHelper.instance.getAllSupplements(); final supplement = supplements.firstWhere((s) => s.id == supplementId && s.isActive, orElse: () => throw Exception('Supplement not found')); // Schedule the retry notification immediately await _notifications.show( retryNotificationId, 'Reminder: ${supplement.name}', 'Don\'t forget to take your ${supplement.name}! (Retry #$retryAttempt)', NotificationDetails( android: AndroidNotificationDetails( 'supplement_reminders', 'Supplement Reminders', channelDescription: 'Notifications for supplement intake reminders', importance: Importance.high, priority: Priority.high, actions: [ AndroidNotificationAction( 'take_supplement', 'Take', showsUserInterface: true, ), AndroidNotificationAction( 'snooze_10', 'Snooze 10min', showsUserInterface: true, ), ], ), iOS: const DarwinNotificationDetails(), ), payload: '${supplement.id}|${supplement.name}|${supplement.numberOfUnits}|${supplement.unitType}|$notificationId', ); // Update the retry count in database await DatabaseHelper.instance.incrementRetryCount(notificationId); print('SupplementsLog: πŸ“± Retry notification scheduled successfully'); } catch (e) { print('SupplementsLog: πŸ“± Error scheduling retry notification: $e'); } } Future requestPermissions() async { print('SupplementsLog: πŸ“± Requesting notification permissions...'); if (_permissionsRequested) { print('SupplementsLog: πŸ“± Permissions already requested'); return true; } try { _permissionsRequested = true; final androidPlugin = _notifications.resolvePlatformSpecificImplementation(); if (androidPlugin != null) { print('SupplementsLog: πŸ“± Requesting Android permissions...'); final granted = await androidPlugin.requestNotificationsPermission(); print('SupplementsLog: πŸ“± Android permissions granted: $granted'); if (granted != true) { _permissionsRequested = false; return false; } } final iosPlugin = _notifications.resolvePlatformSpecificImplementation(); if (iosPlugin != null) { print('SupplementsLog: πŸ“± Requesting iOS permissions...'); final granted = await iosPlugin.requestPermissions( alert: true, badge: true, sound: true, ); print('SupplementsLog: πŸ“± iOS permissions granted: $granted'); if (granted != true) { _permissionsRequested = false; return false; } } print('SupplementsLog: πŸ“± All permissions granted successfully'); return true; } catch (e) { _permissionsRequested = false; print('SupplementsLog: πŸ“± Error requesting permissions: $e'); return false; } } Future scheduleSupplementReminders(Supplement supplement) async { print('SupplementsLog: πŸ“± Scheduling reminders for ${supplement.name}'); print('SupplementsLog: πŸ“± Reminder times: ${supplement.reminderTimes}'); // Cancel existing notifications for this supplement await cancelSupplementReminders(supplement.id!); for (int i = 0; i < supplement.reminderTimes.length; i++) { final timeStr = supplement.reminderTimes[i]; final timeParts = timeStr.split(':'); final hour = int.parse(timeParts[0]); final minute = int.parse(timeParts[1]); final notificationId = supplement.id! * 100 + i; // Unique ID for each reminder final scheduledTime = _nextInstanceOfTime(hour, minute); print('SupplementsLog: πŸ“± Scheduling notification ID $notificationId for ${timeStr} -> ${scheduledTime}'); // Track this notification in the database await DatabaseHelper.instance.trackNotification( notificationId: notificationId, supplementId: supplement.id!, scheduledTime: scheduledTime.toLocal(), ); await _notifications.zonedSchedule( notificationId, 'Time for ${supplement.name}', 'Take ${supplement.numberOfUnits} ${supplement.unitType} (${supplement.ingredientsPerUnit})', scheduledTime, NotificationDetails( android: AndroidNotificationDetails( 'supplement_reminders', 'Supplement Reminders', channelDescription: 'Notifications for supplement intake reminders', importance: Importance.high, priority: Priority.high, actions: [ AndroidNotificationAction( 'take_supplement', 'Take', icon: DrawableResourceAndroidBitmap('@android:drawable/ic_menu_save'), showsUserInterface: true, // Changed to true to open app ), AndroidNotificationAction( 'snooze_10', 'Snooze 10min', icon: DrawableResourceAndroidBitmap('@android:drawable/ic_menu_recent_history'), showsUserInterface: true, // Changed to true to open app ), ], ), iOS: const DarwinNotificationDetails(), ), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, matchDateTimeComponents: DateTimeComponents.time, payload: '${supplement.id}|${supplement.name}|${supplement.numberOfUnits}|${supplement.unitType}', ); print('SupplementsLog: πŸ“± Successfully scheduled notification ID $notificationId'); } // Get all pending notifications to verify final pendingNotifications = await _notifications.pendingNotificationRequests(); print('SupplementsLog: πŸ“± Total pending notifications: ${pendingNotifications.length}'); for (final notification in pendingNotifications) { print('SupplementsLog: πŸ“± Pending: ID=${notification.id}, Title=${notification.title}'); } } Future cancelSupplementReminders(int supplementId) async { // Cancel all notifications for this supplement (up to 10 possible reminders) for (int i = 0; i < 10; i++) { final notificationId = supplementId * 100 + i; await _notifications.cancel(notificationId); } // Also clean up database tracking records for this supplement await DatabaseHelper.instance.clearNotificationTracking(supplementId); } Future cancelAllReminders() async { await _notifications.cancelAll(); } tz.TZDateTime _nextInstanceOfTime(int hour, int minute) { final tz.TZDateTime now = tz.TZDateTime.now(tz.local); tz.TZDateTime scheduledDate = tz.TZDateTime(tz.local, now.year, now.month, now.day, hour, minute); print('SupplementsLog: πŸ“± Current time: $now (${now.timeZoneName})'); print('SupplementsLog: πŸ“± Target time: ${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'); print('SupplementsLog: πŸ“± Initial scheduled date: $scheduledDate (${scheduledDate.timeZoneName})'); if (scheduledDate.isBefore(now)) { scheduledDate = scheduledDate.add(const Duration(days: 1)); print('SupplementsLog: πŸ“± Time has passed, scheduling for tomorrow: $scheduledDate (${scheduledDate.timeZoneName})'); } else { print('SupplementsLog: πŸ“± Time is in the future, scheduling for today: $scheduledDate (${scheduledDate.timeZoneName})'); } return scheduledDate; } Future showInstantNotification(String title, String body) async { print('SupplementsLog: πŸ“± Showing instant notification: $title - $body'); const NotificationDetails notificationDetails = NotificationDetails( android: AndroidNotificationDetails( 'instant_notifications', 'Instant Notifications', channelDescription: 'Instant notifications for supplement app', importance: Importance.high, priority: Priority.high, ), iOS: DarwinNotificationDetails(), ); await _notifications.show( DateTime.now().millisecondsSinceEpoch ~/ 1000, title, body, notificationDetails, ); print('SupplementsLog: πŸ“± Instant notification sent'); } // Debug function to test notifications Future testNotification() async { print('SupplementsLog: πŸ“± Testing notification system...'); await showInstantNotification('Test Notification', 'This is a test notification to verify the system is working.'); } // Debug function to schedule a test notification 1 minute from now Future testScheduledNotification() async { print('SupplementsLog: πŸ“± Testing scheduled notification...'); final now = tz.TZDateTime.now(tz.local); final testTime = now.add(const Duration(minutes: 1)); print('SupplementsLog: πŸ“± Scheduling test notification for: $testTime'); await _notifications.zonedSchedule( 99999, // Special ID for test notifications 'Test Scheduled Notification', 'This notification was scheduled 1 minute ago at ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}', testTime, const NotificationDetails( android: AndroidNotificationDetails( 'test_notifications', 'Test Notifications', channelDescription: 'Test notifications for debugging', importance: Importance.high, priority: Priority.high, ), iOS: DarwinNotificationDetails(), ), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, ); print('SupplementsLog: πŸ“± Test notification scheduled successfully'); } // Debug function to get all pending notifications Future> getPendingNotifications() async { return await _notifications.pendingNotificationRequests(); } // Debug function to test notification actions Future testNotificationWithActions() async { print('SupplementsLog: πŸ“± Creating test notification with actions...'); await _notifications.show( 88888, // Special test ID 'Test Action Notification', 'Tap Take or Snooze to test notification actions', NotificationDetails( android: AndroidNotificationDetails( 'test_notifications', 'Test Notifications', channelDescription: 'Test notifications for debugging actions', importance: Importance.high, priority: Priority.high, actions: [ AndroidNotificationAction( 'take_supplement', 'Take', icon: DrawableResourceAndroidBitmap('@android:drawable/ic_menu_save'), showsUserInterface: true, ), AndroidNotificationAction( 'snooze_10', 'Snooze 10min', icon: DrawableResourceAndroidBitmap('@android:drawable/ic_menu_recent_history'), showsUserInterface: true, ), ], ), iOS: const DarwinNotificationDetails(), ), payload: '999|Test Supplement|1.0|capsule', ); print('SupplementsLog: πŸ“± Test notification with actions created'); } // Debug function to test basic notification tap response Future testBasicNotification() async { print('SupplementsLog: πŸ“± Creating basic test notification...'); await _notifications.show( 77777, // Special test ID for basic notification 'Basic Test Notification', 'Tap this notification to test basic callback', NotificationDetails( android: AndroidNotificationDetails( 'test_notifications', 'Test Notifications', channelDescription: 'Test notifications for debugging', importance: Importance.high, priority: Priority.high, ), iOS: const DarwinNotificationDetails(), ), payload: 'basic_test', ); print('SupplementsLog: πŸ“± Basic test notification created'); } }