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