import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:timezone/timezone.dart' as tz; class NotificationManger { // private constructor to avoid create object NotificationManger._(); static late FlutterLocalNotificationsPlugin localNotificationsPlugin; // Track notification groups for Android grouping static final Map>> _notificationGroups = {}; static final Map _groupSummaryIds = {}; // Store initial notification details when app launched by tapping notification static NotificationResponse? _initialNotificationResponse; /// initialisation setting for all platform /// onNotificationPressed action when notification pressed to open tap /// onIOSNotificationPressed action when notification pressed /// to open tap in iOS versions older than 10 static Future initialisation(Function(NotificationResponse) onNotificationPressed, DidReceiveLocalNotificationCallback onIOSNotificationPressed) async { final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); // initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@drawable/ic_stat_name'); final DarwinInitializationSettings initializationSettingsDarwin = DarwinInitializationSettings(onDidReceiveLocalNotification: onIOSNotificationPressed); final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsDarwin, macOS: initializationSettingsDarwin); localNotificationsPlugin = flutterLocalNotificationsPlugin; if (Platform.isIOS) { await localNotificationsPlugin.resolvePlatformSpecificImplementation()?.requestPermissions(alert: true, badge: true, sound: true); } else if (Platform.isAndroid) { final AndroidFlutterLocalNotificationsPlugin? androidImplementation = localNotificationsPlugin.resolvePlatformSpecificImplementation(); // Create notification channel explicitly for Android const AndroidNotificationChannel channel = AndroidNotificationChannel( 'com.hmg.atoms', // id 'ATOMS', // title description: 'Push notification service for ATOMS', importance: Importance.max, playSound: true, enableVibration: true, showBadge: true, ); await androidImplementation?.createNotificationChannel(channel); final bool granted = await androidImplementation?.requestNotificationsPermission() ?? false; if (!granted) { if (kDebugMode) { print("-------------------- Permission Granted ------------------------"); print(granted); } await Permission.notification.request(); } } // CRITICAL: Get the notification that launched the app (if any) // This must be done BEFORE setting up the callback final notificationAppLaunchDetails = await localNotificationsPlugin.getNotificationAppLaunchDetails(); if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { _initialNotificationResponse = notificationAppLaunchDetails!.notificationResponse; if (kDebugMode) { print('[NOTIFICATION_MANAGER] App launched from notification tap!'); print('[NOTIFICATION_MANAGER] Initial notification payload: ${_initialNotificationResponse?.payload}'); } } await localNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: onNotificationPressed); } // push new notification /// Get the initial notification response if app was launched by tapping a notification /// Returns null if app was opened normally static NotificationResponse? getInitialNotificationResponse() { final response = _initialNotificationResponse; _initialNotificationResponse = null; // Clear after retrieving if (kDebugMode) { print('[NOTIFICATION_MANAGER] getInitialNotificationResponse() returning: ${response?.payload}'); } return response; } static const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( 'com.hmg.atoms', 'ATOMS', channelDescription: 'Push notification service for ATOMS', importance: Importance.max, priority: Priority.max, icon: "@drawable/ic_stat_name", playSound: true, channelShowBadge: true, visibility: NotificationVisibility.public, enableVibration: true, groupKey: 'com.hmg.atoms', ); static const DarwinNotificationDetails iosNotificationDetails = DarwinNotificationDetails( categoryIdentifier: "atoms", ); static Future showNotification({ required BuildContext context, required String title, required String subtext, required int hashcode, String? payload, String? groupId, // New parameter for grouping }) async { if (groupId != null && groupId.isNotEmpty) { // Show grouped notification await _showGroupedNotification( context: context, title: title, subtext: subtext, hashcode: hashcode, payload: payload, groupId: groupId, ); } else { // Show regular notification without grouping const NotificationDetails platformChannel = NotificationDetails( android: androidNotificationDetails, iOS: iosNotificationDetails, macOS: iosNotificationDetails, ); await localNotificationsPlugin.show(hashcode, title, subtext, platformChannel, payload: payload); } } /// Show grouped notification with Android grouping and iOS thread support static Future _showGroupedNotification({ required BuildContext context, required String title, required String subtext, required int hashcode, String? payload, required String groupId, }) async { // Store notification in group _notificationGroups.putIfAbsent(groupId, () => []); _notificationGroups[groupId]!.add({ 'title': title, 'body': subtext, 'id': hashcode, 'payload': payload, 'timestamp': DateTime.now().millisecondsSinceEpoch, }); if (Platform.isAndroid) { // Android: Show individual notification + summary notification await _showAndroidGroupedNotification( title: title, subtext: subtext, hashcode: hashcode, payload: payload, groupId: groupId, ); } else if (Platform.isIOS) { // iOS: Use thread identifier for grouping await _showIOSGroupedNotification( title: title, subtext: subtext, hashcode: hashcode, payload: payload, groupId: groupId, ); } } /// Android-specific grouped notification implementation static Future _showAndroidGroupedNotification({ required String title, required String subtext, required int hashcode, String? payload, required String groupId, }) async { final notifications = _notificationGroups[groupId]!; final notificationCount = notifications.length; // Create individual notification with group key final AndroidNotificationDetails androidDetails = AndroidNotificationDetails( 'com.hmg.atoms', 'ATOMS', channelDescription: 'Push notification service for ATOMS', importance: Importance.max, priority: Priority.max, icon: "@drawable/ic_stat_name", playSound: true, channelShowBadge: true, visibility: NotificationVisibility.public, enableVibration: true, groupKey: groupId, // Use dynamic group key setAsGroupSummary: false, ); const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( categoryIdentifier: "atoms", ); final NotificationDetails notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, macOS: iosDetails, ); // Show individual notification await localNotificationsPlugin.show( hashcode, title, subtext, notificationDetails, payload: payload, ); // Create and show summary notification await _showAndroidSummaryNotification(groupId, notificationCount); } /// Create summary notification for Android group static Future _showAndroidSummaryNotification(String groupId, int count) async { // Generate or retrieve summary notification ID if (!_groupSummaryIds.containsKey(groupId)) { _groupSummaryIds[groupId] = groupId.hashCode.abs(); } final summaryId = _groupSummaryIds[groupId]!; final notifications = _notificationGroups[groupId]!; // Build inbox style summary final InboxStyleInformation inboxStyle = InboxStyleInformation( notifications.map((n) => '${n['title']}: ${n['body']}').toList(), contentTitle: '$count new notifications', summaryText: 'ATOMS', ); final AndroidNotificationDetails summaryDetails = AndroidNotificationDetails( 'com.hmg.atoms', 'ATOMS', channelDescription: 'Push notification service for ATOMS', importance: Importance.max, priority: Priority.max, icon: "@drawable/ic_stat_name", styleInformation: inboxStyle, groupKey: groupId, setAsGroupSummary: true, // This is the summary notification playSound: false, // Don't play sound for summary enableVibration: false, // Don't vibrate for summary ); final NotificationDetails summaryNotificationDetails = NotificationDetails( android: summaryDetails, ); await localNotificationsPlugin.show( summaryId, 'ATOMS', '$count new notifications', summaryNotificationDetails, ); } /// iOS-specific grouped notification with thread identifier static Future _showIOSGroupedNotification({ required String title, required String subtext, required int hashcode, String? payload, required String groupId, }) async { final DarwinNotificationDetails iosDetails = DarwinNotificationDetails( categoryIdentifier: "atoms", threadIdentifier: groupId, // iOS uses threadIdentifier for grouping ); const AndroidNotificationDetails androidDetails = androidNotificationDetails; final NotificationDetails notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, macOS: iosDetails, ); await localNotificationsPlugin.show( hashcode, title, subtext, notificationDetails, payload: payload, ); } /// Clear notification group when tapped (optional utility) static void clearNotificationGroup(String groupId) { _notificationGroups.remove(groupId); if (_groupSummaryIds.containsKey(groupId)) { final summaryId = _groupSummaryIds[groupId]!; localNotificationsPlugin.cancel(summaryId); _groupSummaryIds.remove(groupId); } } /// Get notifications in a group (for debugging or UI display) static List>? getNotificationsInGroup(String groupId) { return _notificationGroups[groupId]; } static Future scheduleNotification({int? id, String? title, String? body, String? payLoad, required DateTime scheduledNotificationDateTime}) async { return localNotificationsPlugin.zonedSchedule( id ?? 0, title, body, // tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)), tz.TZDateTime.from( scheduledNotificationDateTime, tz.local, ), const NotificationDetails(android: androidNotificationDetails, iOS: iosNotificationDetails, macOS: iosNotificationDetails), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime); } static Future cancelNotificationById(int notificationId) async { await localNotificationsPlugin.cancel(notificationId); } }