import 'dart:typed_data'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/timezone.dart' as tz show TZDateTime, local, setLocalLocation, getLocation; /// Abstract class defining the notification service interface abstract class NotificationService { /// Initialize the notification service Future initialize({Function(String payload)? onNotificationClick}); /// Request notification permissions (mainly for iOS) Future requestPermissions(); /// Show an immediate notification Future showNotification({ required String title, required String body, String? payload, }); /// Schedule a notification at a specific date and time Future scheduleNotification({ required int id, required String title, required String body, required DateTime scheduledDate, String? payload, }); /// Schedule daily notifications at specific times Future scheduleDailyNotifications({ required List times, required String title, required String body, String? payload, }); /// Schedule water reminder notifications Future scheduleWaterReminders({ required List reminderTimes, required String title, required String body, }); /// Cancel a specific notification by id Future cancelNotification(int id); /// Cancel all scheduled notifications Future cancelAllNotifications(); /// Get list of pending notifications Future> getPendingNotifications(); } /// Implementation of NotificationService following the project architecture class NotificationServiceImp implements NotificationService { final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; final LoggerService loggerService; NotificationServiceImp({required this.flutterLocalNotificationsPlugin, required this.loggerService}); // Channel IDs for different notification types static const String _waterReminderChannelId = 'water_reminder_channel'; static const String _waterReminderChannelName = 'Water Reminders'; static const String _waterReminderChannelDescription = 'Daily water intake reminders'; static const String _generalChannelId = 'hmg_general_channel'; static const String _generalChannelName = 'HMG Notifications'; static const String _generalChannelDescription = 'General notifications from HMG'; Function(String payload)? _onNotificationClick; @override Future initialize({Function(String payload)? onNotificationClick}) async { try { // Initialize timezone database tz.initializeTimeZones(); // Set local timezone (you can also use a specific timezone if needed) // For example: tz.setLocalLocation(tz.getLocation('Asia/Riyadh')); final locationName = DateTime.now().timeZoneName; try { tz.setLocalLocation(tz.getLocation(locationName)); } catch (e) { // Fallback to UTC if specific timezone not found loggerService.logInfo('Could not set timezone $locationName, using UTC'); tz.setLocalLocation(tz.getLocation('UTC')); } _onNotificationClick = onNotificationClick; const androidSettings = AndroidInitializationSettings('app_icon'); const iosSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); const initializationSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, ); await flutterLocalNotificationsPlugin.initialize( initializationSettings, onDidReceiveNotificationResponse: _handleNotificationResponse, ); loggerService.logInfo('NotificationService initialized successfully'); } catch (ex) { loggerService.logError('Failed to initialize NotificationService: $ex'); } } /// Handle notification tap void _handleNotificationResponse(NotificationResponse response) { try { if (response.payload != null && _onNotificationClick != null) { _onNotificationClick!(response.payload!); } loggerService.logInfo('Notification tapped: ${response.payload}'); } catch (ex) { loggerService.logError('Error handling notification response: $ex'); } } @override Future requestPermissions() async { try { // Request permissions for iOS final result = await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation()?.requestPermissions( alert: true, badge: true, sound: true, ); // For Android 13+, permissions are requested at runtime final androidResult = await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation() ?.requestNotificationsPermission(); loggerService.logInfo('Notification permissions: iOS=${result ?? true}, Android=${androidResult ?? true}'); return result ?? androidResult ?? true; } catch (ex) { loggerService.logError('Error requesting notification permissions: $ex'); return false; } } @override Future showNotification({ required String title, required String body, String? payload, }) async { try { final androidDetails = AndroidNotificationDetails( _generalChannelId, _generalChannelName, channelDescription: _generalChannelDescription, importance: Importance.high, priority: Priority.high, vibrationPattern: _getVibrationPattern(), ); const iosDetails = DarwinNotificationDetails(); final notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await flutterLocalNotificationsPlugin.show( DateTime.now().millisecondsSinceEpoch ~/ 1000, title, body, notificationDetails, payload: payload, ); loggerService.logInfo('Notification shown: $title'); } catch (ex) { loggerService.logError('Error showing notification: $ex'); } } @override Future scheduleNotification({ required int id, required String title, required String body, required DateTime scheduledDate, String? payload, }) async { try { final androidDetails = AndroidNotificationDetails( _generalChannelId, _generalChannelName, channelDescription: _generalChannelDescription, importance: Importance.high, priority: Priority.high, vibrationPattern: _getVibrationPattern(), ); const iosDetails = DarwinNotificationDetails(); final notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await flutterLocalNotificationsPlugin.zonedSchedule( id, title, body, tz.TZDateTime.from(scheduledDate, tz.local), notificationDetails, androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, payload: payload, ); loggerService.logInfo('Notification scheduled for: $scheduledDate'); } catch (ex) { loggerService.logError('Error scheduling notification: $ex'); } } @override Future scheduleDailyNotifications({ required List times, required String title, required String body, String? payload, }) async { try { for (int i = 0; i < times.length; i++) { final time = times[i]; await scheduleNotification( id: i + 1000, // Offset ID to avoid conflicts title: title, body: body, scheduledDate: time, payload: payload, ); } loggerService.logInfo('Scheduled ${times.length} daily notifications'); } catch (ex) { loggerService.logError('Error scheduling daily notifications: $ex'); } } @override Future scheduleWaterReminders({ required List reminderTimes, required String title, required String body, }) async { try { // Cancel existing water reminders first await _cancelWaterReminders(); final androidDetails = AndroidNotificationDetails( _waterReminderChannelId, _waterReminderChannelName, channelDescription: _waterReminderChannelDescription, importance: Importance.high, priority: Priority.high, vibrationPattern: _getVibrationPattern(), icon: 'app_icon', styleInformation: const BigTextStyleInformation(''), ); const iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); final notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); for (int i = 0; i < reminderTimes.length; i++) { final reminderTime = reminderTimes[i]; final notificationId = 5000 + i; // Use 5000+ range for water reminders // Schedule for today if time hasn't passed, otherwise schedule for tomorrow DateTime scheduledDate = reminderTime; if (scheduledDate.isBefore(DateTime.now())) { scheduledDate = scheduledDate.add(const Duration(days: 1)); } await flutterLocalNotificationsPlugin.zonedSchedule( notificationId, title, body, tz.TZDateTime.from(scheduledDate, tz.local), notificationDetails, androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, matchDateTimeComponents: DateTimeComponents.time, // Repeat daily at the same time payload: 'water_reminder_$i', ); } loggerService.logInfo('Scheduled ${reminderTimes.length} water reminders'); } catch (ex) { loggerService.logError('Error scheduling water reminders: $ex'); } } /// Cancel all water reminders (IDs 5000-5999) Future _cancelWaterReminders() async { try { final pendingNotifications = await getPendingNotifications(); for (final notification in pendingNotifications) { if (notification.id >= 5000 && notification.id < 6000) { await cancelNotification(notification.id); } } loggerService.logInfo('Cancelled all water reminders'); } catch (ex) { loggerService.logError('Error cancelling water reminders: $ex'); } } @override Future cancelNotification(int id) async { try { await flutterLocalNotificationsPlugin.cancel(id); loggerService.logInfo('Cancelled notification with ID: $id'); } catch (ex) { loggerService.logError('Error cancelling notification: $ex'); } } @override Future cancelAllNotifications() async { try { await flutterLocalNotificationsPlugin.cancelAll(); loggerService.logInfo('Cancelled all notifications'); } catch (ex) { loggerService.logError('Error cancelling all notifications: $ex'); } } @override Future> getPendingNotifications() async { try { final pending = await flutterLocalNotificationsPlugin.pendingNotificationRequests(); loggerService.logInfo('Found ${pending.length} pending notifications'); return pending; } catch (ex) { loggerService.logError('Error getting pending notifications: $ex'); return []; } } /// Get vibration pattern for notifications Int64List _getVibrationPattern() { final vibrationPattern = Int64List(4); vibrationPattern[0] = 0; vibrationPattern[1] = 500; vibrationPattern[2] = 500; vibrationPattern[3] = 500; return vibrationPattern; } }