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.
374 lines
12 KiB
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;
|
|
}
|
|
}
|