You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
HMG_Patient_App_New/lib/services/notification_service.dart

374 lines
12 KiB
Dart

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<void> initialize({Function(String payload)? onNotificationClick});
/// Request notification permissions (mainly for iOS)
Future<bool> requestPermissions();
/// Show an immediate notification
Future<void> showNotification({
required String title,
required String body,
String? payload,
});
/// Schedule a notification at a specific date and time
Future<void> scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledDate,
String? payload,
});
/// Schedule daily notifications at specific times
Future<void> scheduleDailyNotifications({
required List<DateTime> times,
required String title,
required String body,
String? payload,
});
/// Schedule water reminder notifications
Future<void> scheduleWaterReminders({
required List<DateTime> reminderTimes,
required String title,
required String body,
});
/// Cancel a specific notification by id
Future<void> cancelNotification(int id);
/// Cancel all scheduled notifications
Future<void> cancelAllNotifications();
/// Get list of pending notifications
Future<List<PendingNotificationRequest>> 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<void> 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<bool> requestPermissions() async {
try {
// Request permissions for iOS
final result =
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
// For Android 13+, permissions are requested at runtime
final androidResult = await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.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<void> 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<void> 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<void> scheduleDailyNotifications({
required List<DateTime> 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<void> scheduleWaterReminders({
required List<DateTime> 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<void> _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<void> 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<void> cancelAllNotifications() async {
try {
await flutterLocalNotificationsPlugin.cancelAll();
loggerService.logInfo('Cancelled all notifications');
} catch (ex) {
loggerService.logError('Error cancelling all notifications: $ex');
}
}
@override
Future<List<PendingNotificationRequest>> 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;
}
}