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.
cloudsolutions-atoms/lib/controllers/notification/notification_manger.dart

333 lines
12 KiB
Dart

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<String, List<Map<String, dynamic>>> _notificationGroups = {};
static final Map<String, int> _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<void> 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<IOSFlutterLocalNotificationsPlugin>()?.requestPermissions(alert: true, badge: true, sound: true);
} else if (Platform.isAndroid) {
final AndroidFlutterLocalNotificationsPlugin? androidImplementation = localNotificationsPlugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
// 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<void> 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<void> _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<void> _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<void> _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<void> _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<Map<String, dynamic>>? 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<void> cancelNotificationById(int notificationId) async {
await localNotificationsPlugin.cancel(notificationId);
}
}