Files
meshcore-sar_android/lib/services/notification_service.dart
2026-03-07 20:09:07 +01:00

618 lines
19 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import '../models/sar_marker.dart';
import '../l10n/app_localizations.dart';
/// Notification Service - manages urgent notifications for SAR messages
/// Provides critical alert functionality for SAR marker events
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final FlutterLocalNotificationsPlugin _notificationsPlugin =
FlutterLocalNotificationsPlugin();
bool _isInitialized = false;
bool _permissionGranted = false;
// Notification IDs
static const int _sarNotificationId = 1000;
static const int _messageNotificationId = 2000;
static const int _updateNotificationId = 3000;
// Notification channels
static const String _urgentChannelId = 'sar_urgent';
static const String _urgentChannelName = 'SAR Urgent Alerts';
static const String _urgentChannelDescription =
'Critical alerts for SAR markers (found persons, fires, staging areas)';
static const String _messagesChannelId = 'messages';
static const String _messagesChannelName = 'Messages';
static const String _messagesChannelDescription =
'Notifications for incoming messages from contacts and channels';
static const String _updateChannelId = 'app_updates';
static const String _updateChannelName = 'App Updates';
static const String _updateChannelDescription =
'Notifications for available app updates';
/// Initialize notification service
Future<void> initialize() async {
if (_isInitialized) return;
try {
debugPrint('📬 [NotificationService] Initializing...');
// Initialize timezone data
tz.initializeTimeZones();
// Android initialization settings
const androidSettings = AndroidInitializationSettings(
'@mipmap/ic_launcher',
);
// iOS initialization settings
final darwinSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
// Combined initialization settings
final initSettings = InitializationSettings(
android: androidSettings,
iOS: darwinSettings,
);
// Initialize plugin
await _notificationsPlugin.initialize(
settings: initSettings,
onDidReceiveNotificationResponse: _onNotificationResponse,
);
// Request permissions
await _requestPermissions();
// Create notification channels (Android)
await _createNotificationChannels();
_isInitialized = true;
debugPrint('✅ [NotificationService] Initialized successfully');
debugPrint(' Permission granted: $_permissionGranted');
} catch (e) {
debugPrint('❌ [NotificationService] Initialization error: $e');
}
}
/// Request notification permissions
Future<void> _requestPermissions() async {
try {
// iOS permissions
final iosPlugin = _notificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>();
if (iosPlugin != null) {
final granted = await iosPlugin.requestPermissions(
alert: true,
badge: true,
sound: true,
);
_permissionGranted = granted ?? false;
debugPrint(
'📱 [NotificationService] iOS permissions granted: $_permissionGranted',
);
return; // Exit early if on iOS
}
// Android 13+ permissions
final androidPlugin = _notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidPlugin != null) {
final granted = await androidPlugin.requestNotificationsPermission();
_permissionGranted = granted ?? false;
debugPrint(
'🤖 [NotificationService] Android permissions granted: $_permissionGranted',
);
return; // Exit early if on Android
}
// If neither platform plugin is available, assume permissions are granted
// This handles older Android versions that don't require runtime permissions
_permissionGranted = true;
debugPrint(
'✅ [NotificationService] No platform plugin found, assuming permissions granted',
);
} catch (e) {
debugPrint('⚠️ [NotificationService] Error requesting permissions: $e');
}
}
/// Create notification channels for Android
Future<void> _createNotificationChannels() async {
try {
final androidPlugin = _notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidPlugin == null) return;
// Urgent SAR channel with maximum priority
const urgentChannel = AndroidNotificationChannel(
_urgentChannelId,
_urgentChannelName,
description: _urgentChannelDescription,
importance: Importance.max,
playSound: true,
enableVibration: true,
enableLights: true,
showBadge: true,
sound: RawResourceAndroidNotificationSound('notification'),
);
// Messages channel with high priority
const messagesChannel = AndroidNotificationChannel(
_messagesChannelId,
_messagesChannelName,
description: _messagesChannelDescription,
importance: Importance.high,
playSound: true,
enableVibration: true,
showBadge: true,
);
// App updates channel with default priority
const updateChannel = AndroidNotificationChannel(
_updateChannelId,
_updateChannelName,
description: _updateChannelDescription,
importance: Importance.defaultImportance,
playSound: false,
enableVibration: false,
showBadge: true,
);
await androidPlugin.createNotificationChannel(urgentChannel);
await androidPlugin.createNotificationChannel(messagesChannel);
await androidPlugin.createNotificationChannel(updateChannel);
debugPrint('✅ [NotificationService] Created notification channels');
} catch (e) {
debugPrint('⚠️ [NotificationService] Error creating channels: $e');
}
}
/// Callback for handling notification taps (set by main.dart)
void Function(String?)? onNotificationTapped;
/// Handle notification tap (foreground)
void _onNotificationResponse(NotificationResponse response) {
debugPrint(
'🔔 [NotificationService] Notification tapped: ${response.payload}',
);
// Call the registered callback if available
if (onNotificationTapped != null) {
onNotificationTapped!(response.payload);
}
}
/// Show urgent notification for SAR marker
Future<void> showSarNotification({
required SarMarkerType type,
required String senderName,
required String coordinates,
String? notes,
AppLocalizations? localizations,
}) async {
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
);
return;
}
if (!_permissionGranted) {
debugPrint(
'⚠️ [NotificationService] Permission not granted, skipping notification',
);
return;
}
try {
// Generate unique notification ID based on timestamp
final notificationId =
_sarNotificationId + (DateTime.now().millisecondsSinceEpoch % 1000);
// Build notification title and body
final title = _buildNotificationTitle(type, localizations);
final body = _buildNotificationBody(
type: type,
senderName: senderName,
coordinates: coordinates,
notes: notes,
localizations: localizations,
);
// Android notification details
final androidDetails = AndroidNotificationDetails(
_urgentChannelId,
_urgentChannelName,
channelDescription: _urgentChannelDescription,
importance: Importance.max,
priority: Priority.high,
ticker: title,
playSound: true,
enableVibration: true,
enableLights: true,
color: Color(_getNotificationColor(type)),
colorized: true,
showWhen: true,
when: DateTime.now().millisecondsSinceEpoch,
category: AndroidNotificationCategory.alarm, // High priority category
fullScreenIntent: true, // Show as full screen on some devices
styleInformation: BigTextStyleInformation(
body,
contentTitle: title,
summaryText: _getSummaryText(type, localizations),
),
);
// iOS notification details
final darwinDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
sound: 'default',
badgeNumber: 1,
threadIdentifier: 'sar_markers',
categoryIdentifier: 'SAR_ALERT',
interruptionLevel: InterruptionLevel.timeSensitive,
);
// Combined notification details
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: darwinDetails,
);
// Show notification
await _notificationsPlugin.show(
id: notificationId,
title: title,
body: body,
notificationDetails: notificationDetails,
payload: 'sar:${type.name}:$coordinates',
);
debugPrint('✅ [NotificationService] Showed SAR notification: $title');
debugPrint(' Type: ${type.displayName}');
debugPrint(' Sender: $senderName');
debugPrint(' Coordinates: $coordinates');
} catch (e) {
debugPrint('❌ [NotificationService] Error showing notification: $e');
}
}
/// Build notification title based on SAR marker type
String _buildNotificationTitle(
SarMarkerType type,
AppLocalizations? localizations,
) {
if (localizations == null) {
return '${type.emoji} ${type.displayName} Detected';
}
switch (type) {
case SarMarkerType.foundPerson:
return '${type.emoji} ${localizations.sarMarkerFoundPerson}';
case SarMarkerType.fire:
return '${type.emoji} ${localizations.sarMarkerFire}';
case SarMarkerType.stagingArea:
return '${type.emoji} ${localizations.sarMarkerStagingArea}';
case SarMarkerType.object:
return '${type.emoji} ${localizations.sarMarkerObject}';
case SarMarkerType.unknown:
return '${type.emoji} ${localizations.sarAlert}';
}
}
/// Build notification body with all details
String _buildNotificationBody({
required SarMarkerType type,
required String senderName,
required String coordinates,
String? notes,
AppLocalizations? localizations,
}) {
final buffer = StringBuffer();
// Sender
if (localizations != null) {
buffer.write('${localizations.from}: $senderName\n');
buffer.write('${localizations.coordinates}: $coordinates');
} else {
buffer.write('From: $senderName\n');
buffer.write('Coordinates: $coordinates');
}
// Optional notes
if (notes != null && notes.isNotEmpty) {
buffer.write('\n\n$notes');
}
return buffer.toString();
}
/// Get summary text for notification
String _getSummaryText(SarMarkerType type, AppLocalizations? localizations) {
if (localizations == null) {
return 'Tap to view on map';
}
return localizations.tapToViewOnMap;
}
/// Get notification color based on SAR marker type
int _getNotificationColor(SarMarkerType type) {
// Return ARGB color codes
switch (type) {
case SarMarkerType.foundPerson:
return 0xFF4CAF50; // Green
case SarMarkerType.fire:
return 0xFFF44336; // Red
case SarMarkerType.stagingArea:
return 0xFFFF9800; // Orange
case SarMarkerType.object:
return 0xFF2196F3; // Blue
case SarMarkerType.unknown:
return 0xFF9E9E9E; // Gray
}
}
/// Show notification for regular message (contact or channel)
Future<void> showMessageNotification({
required String senderName,
required String messageText,
required bool isChannelMessage,
String? channelName,
AppLocalizations? localizations,
}) async {
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
);
return;
}
if (!_permissionGranted) {
debugPrint(
'⚠️ [NotificationService] Permission not granted, skipping notification',
);
return;
}
try {
// Generate unique notification ID based on timestamp
final notificationId =
_messageNotificationId +
(DateTime.now().millisecondsSinceEpoch % 1000);
// Build notification title and body
final title = isChannelMessage
? (localizations != null
? '${localizations.channel}: ${channelName ?? "Public"}'
: 'Channel: ${channelName ?? "Public"}')
: (localizations != null
? '${localizations.newMessage} ${localizations.from} $senderName'
: 'New message from $senderName');
final body = messageText.length > 200
? '${messageText.substring(0, 200)}...'
: messageText;
// Android notification details
final androidDetails = AndroidNotificationDetails(
_messagesChannelId,
_messagesChannelName,
channelDescription: _messagesChannelDescription,
importance: Importance.high,
priority: Priority.high,
ticker: title,
playSound: true,
enableVibration: true,
showWhen: true,
when: DateTime.now().millisecondsSinceEpoch,
styleInformation: BigTextStyleInformation(
body,
contentTitle: title,
summaryText: senderName,
),
);
// iOS notification details
final darwinDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
sound: 'default',
threadIdentifier: isChannelMessage
? 'channel_messages'
: 'direct_messages',
subtitle: senderName,
);
// Combined notification details
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: darwinDetails,
);
// Show notification
await _notificationsPlugin.show(
id: notificationId,
title: title,
body: body,
notificationDetails: notificationDetails,
payload: 'message:${isChannelMessage ? "channel" : "contact"}',
);
debugPrint('✅ [NotificationService] Showed message notification');
debugPrint(' Sender: $senderName');
debugPrint(' Type: ${isChannelMessage ? "Channel" : "Direct"}');
} catch (e) {
debugPrint(
'❌ [NotificationService] Error showing message notification: $e',
);
}
}
/// Cancel all notifications
Future<void> cancelAll() async {
try {
await _notificationsPlugin.cancelAll();
debugPrint('✅ [NotificationService] Cancelled all notifications');
} catch (e) {
debugPrint('❌ [NotificationService] Error canceling notifications: $e');
}
}
/// Cancel specific notification
Future<void> cancel(int id) async {
try {
await _notificationsPlugin.cancel(id: id);
debugPrint('✅ [NotificationService] Cancelled notification: $id');
} catch (e) {
debugPrint('❌ [NotificationService] Error canceling notification: $e');
}
}
/// Check if notifications are enabled
Future<bool> areNotificationsEnabled() async {
try {
final androidPlugin = _notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidPlugin != null) {
final enabled = await androidPlugin.areNotificationsEnabled();
return enabled ?? false;
}
// For iOS, assume enabled if permission was granted
return _permissionGranted;
} catch (e) {
debugPrint(
'⚠️ [NotificationService] Error checking notification status: $e',
);
return false;
}
}
/// Get pending notifications
Future<List<PendingNotificationRequest>> getPendingNotifications() async {
try {
return await _notificationsPlugin.pendingNotificationRequests();
} catch (e) {
debugPrint(
'⚠️ [NotificationService] Error getting pending notifications: $e',
);
return [];
}
}
/// Show notification for available app update
Future<void> showUpdateNotification({
required String currentVersion,
required String latestVersion,
required String downloadUrl,
AppLocalizations? localizations,
}) async {
if (!_isInitialized) {
debugPrint(
'⚠️ [NotificationService] Not initialized, skipping notification',
);
return;
}
if (!_permissionGranted) {
debugPrint(
'⚠️ [NotificationService] Permission not granted, skipping notification',
);
return;
}
try {
// Build notification title and body
final title = localizations?.updateAvailable ?? 'App Update Available';
final body = localizations != null
? '${localizations.currentVersion}: $currentVersion\n'
'${localizations.latestVersion}: $latestVersion'
: 'Current: $currentVersion\nLatest: $latestVersion';
// Android notification details
final androidDetails = AndroidNotificationDetails(
_updateChannelId,
_updateChannelName,
channelDescription: _updateChannelDescription,
importance: Importance.defaultImportance,
priority: Priority.defaultPriority,
ticker: title,
playSound: false,
enableVibration: false,
showWhen: true,
when: DateTime.now().millisecondsSinceEpoch,
icon: '@mipmap/ic_launcher',
color: const Color(0xFF2196F3), // Blue
colorized: true,
category: AndroidNotificationCategory.recommendation,
styleInformation: BigTextStyleInformation(
body,
contentTitle: title,
summaryText: localizations?.downloadUpdate ?? 'Tap to download',
),
// Make notification ongoing so it doesn't get dismissed easily
ongoing: false,
autoCancel: true,
);
// iOS notification details
final darwinDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: false,
threadIdentifier: 'app_updates',
categoryIdentifier: 'APP_UPDATE',
subtitle: 'New version: $latestVersion',
);
// Combined notification details
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: darwinDetails,
);
// Show notification
await _notificationsPlugin.show(
id: _updateNotificationId,
title: title,
body: body,
notificationDetails: notificationDetails,
payload: 'update:$downloadUrl',
);
debugPrint('✅ [NotificationService] Showed update notification');
debugPrint(' Current: $currentVersion');
debugPrint(' Latest: $latestVersion');
debugPrint(' Download URL: $downloadUrl');
} catch (e) {
debugPrint(
'❌ [NotificationService] Error showing update notification: $e',
);
}
}
}