mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-12 00:40:28 +00:00
feat: Implement notification service for SAR messages and add localization support
This commit is contained in:
@@ -9,7 +9,8 @@
|
|||||||
"Bash(dart pub global run intl_utils:generate:*)",
|
"Bash(dart pub global run intl_utils:generate:*)",
|
||||||
"Bash(dart pub global deactivate:*)",
|
"Bash(dart pub global deactivate:*)",
|
||||||
"Bash(flutter precache:*)",
|
"Bash(flutter precache:*)",
|
||||||
"Bash(flutter analyze:*)"
|
"Bash(flutter analyze:*)",
|
||||||
|
"Bash(flutter pub get:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@@ -18,6 +18,12 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<!-- Notifications -->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="meshcore_sar_app"
|
android:label="meshcore_sar_app"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- flutter_compass (0.0.1):
|
- flutter_compass (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_local_notifications (0.0.1):
|
||||||
|
- Flutter
|
||||||
- geolocator_apple (1.2.0):
|
- geolocator_apple (1.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -32,6 +34,7 @@ DEPENDENCIES:
|
|||||||
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
|
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
|
||||||
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
||||||
- flutter_compass (from `.symlinks/plugins/flutter_compass/ios`)
|
- flutter_compass (from `.symlinks/plugins/flutter_compass/ios`)
|
||||||
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||||
- objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`)
|
- objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
@@ -53,6 +56,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
|
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
|
||||||
flutter_compass:
|
flutter_compass:
|
||||||
:path: ".symlinks/plugins/flutter_compass/ios"
|
:path: ".symlinks/plugins/flutter_compass/ios"
|
||||||
|
flutter_local_notifications:
|
||||||
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
geolocator_apple:
|
geolocator_apple:
|
||||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||||
objectbox_flutter_libs:
|
objectbox_flutter_libs:
|
||||||
@@ -73,6 +78,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_background_service_ios: 00d31bdff7b4bfe06d32375df358abe0329cf87e
|
flutter_background_service_ios: 00d31bdff7b4bfe06d32375df358abe0329cf87e
|
||||||
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
||||||
flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1
|
flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1
|
||||||
|
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
|
||||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||||
ObjectBox: 7da4aceb5013d041bfafdbc6d744a26918b09757
|
ObjectBox: 7da4aceb5013d041bfafdbc6d744a26918b09757
|
||||||
objectbox_flutter_libs: 09b1dec1b4cd27bf1a5f9bae7ccaa7e43588bf31
|
objectbox_flutter_libs: 09b1dec1b4cd27bf1a5f9bae7ccaa7e43588bf31
|
||||||
|
|||||||
@@ -68,5 +68,14 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>dev.flutter.background.refresh</string>
|
<string>dev.flutter.background.refresh</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIUserNotificationSettings</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIUserNotificationTypesEnabled</key>
|
||||||
|
<array>
|
||||||
|
<string>UIUserNotificationTypeAlert</string>
|
||||||
|
<string>UIUserNotificationTypeBadge</string>
|
||||||
|
<string>UIUserNotificationTypeSound</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1698,5 +1698,40 @@
|
|||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
"@failed": {
|
"@failed": {
|
||||||
"description": "Delivery status: failed"
|
"description": "Delivery status: failed"
|
||||||
|
},
|
||||||
|
|
||||||
|
"sarMarkerFoundPerson": "Found Person",
|
||||||
|
"@sarMarkerFoundPerson": {
|
||||||
|
"description": "SAR marker type: found person"
|
||||||
|
},
|
||||||
|
|
||||||
|
"sarMarkerFire": "Fire Location",
|
||||||
|
"@sarMarkerFire": {
|
||||||
|
"description": "SAR marker type: fire"
|
||||||
|
},
|
||||||
|
|
||||||
|
"sarMarkerStagingArea": "Staging Area",
|
||||||
|
"@sarMarkerStagingArea": {
|
||||||
|
"description": "SAR marker type: staging area"
|
||||||
|
},
|
||||||
|
|
||||||
|
"sarMarkerObject": "Object Found",
|
||||||
|
"@sarMarkerObject": {
|
||||||
|
"description": "SAR marker type: object"
|
||||||
|
},
|
||||||
|
|
||||||
|
"from": "From",
|
||||||
|
"@from": {
|
||||||
|
"description": "Sender label in notifications"
|
||||||
|
},
|
||||||
|
|
||||||
|
"coordinates": "Coordinates",
|
||||||
|
"@coordinates": {
|
||||||
|
"description": "Coordinates label"
|
||||||
|
},
|
||||||
|
|
||||||
|
"tapToViewOnMap": "Tap to view on map",
|
||||||
|
"@tapToViewOnMap": {
|
||||||
|
"description": "Notification action text"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -567,5 +567,19 @@
|
|||||||
|
|
||||||
"messageDeleted": "Poruka izbrisana",
|
"messageDeleted": "Poruka izbrisana",
|
||||||
|
|
||||||
"refreshedContacts": "Kontakti osvježeni"
|
"refreshedContacts": "Kontakti osvježeni",
|
||||||
|
|
||||||
|
"sarMarkerFoundPerson": "Pronađena osoba",
|
||||||
|
|
||||||
|
"sarMarkerFire": "Lokacija požara",
|
||||||
|
|
||||||
|
"sarMarkerStagingArea": "Zbirno mjesto",
|
||||||
|
|
||||||
|
"sarMarkerObject": "Pronađen objekt",
|
||||||
|
|
||||||
|
"from": "Od",
|
||||||
|
|
||||||
|
"coordinates": "Koordinate",
|
||||||
|
|
||||||
|
"tapToViewOnMap": "Dodirnite za prikaz na karti"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1808,6 +1808,48 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Failed'**
|
/// **'Failed'**
|
||||||
String get failed;
|
String get failed;
|
||||||
|
|
||||||
|
/// SAR marker type: found person
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Found Person'**
|
||||||
|
String get sarMarkerFoundPerson;
|
||||||
|
|
||||||
|
/// SAR marker type: fire
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Fire Location'**
|
||||||
|
String get sarMarkerFire;
|
||||||
|
|
||||||
|
/// SAR marker type: staging area
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Staging Area'**
|
||||||
|
String get sarMarkerStagingArea;
|
||||||
|
|
||||||
|
/// SAR marker type: object
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Object Found'**
|
||||||
|
String get sarMarkerObject;
|
||||||
|
|
||||||
|
/// Sender label in notifications
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'From'**
|
||||||
|
String get from;
|
||||||
|
|
||||||
|
/// Coordinates label
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Coordinates'**
|
||||||
|
String get coordinates;
|
||||||
|
|
||||||
|
/// Notification action text
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Tap to view on map'**
|
||||||
|
String get tapToViewOnMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -988,4 +988,25 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get failed => 'Failed';
|
String get failed => 'Failed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerFoundPerson => 'Found Person';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerFire => 'Fire Location';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerStagingArea => 'Staging Area';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerObject => 'Object Found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get from => 'From';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get coordinates => 'Coordinates';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tapToViewOnMap => 'Tap to view on map';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -990,4 +990,25 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get failed => 'Neuspjelo';
|
String get failed => 'Neuspjelo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerFoundPerson => 'Pronađena osoba';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerFire => 'Lokacija požara';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerStagingArea => 'Zbirno mjesto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerObject => 'Pronađen objekt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get from => 'Od';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get coordinates => 'Koordinate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tapToViewOnMap => 'Dodirnite za prikaz na karti';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -990,4 +990,25 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get failed => 'Neuspešno';
|
String get failed => 'Neuspešno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerFoundPerson => 'Najdena oseba';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerFire => 'Lokacija ognja';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerStagingArea => 'Zbirališče';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get sarMarkerObject => 'Najden predmet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get from => 'Od';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get coordinates => 'Koordinate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tapToViewOnMap => 'Tapnite za prikaz na zemljevidu';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -567,5 +567,19 @@
|
|||||||
|
|
||||||
"messageDeleted": "Sporočilo izbrisano",
|
"messageDeleted": "Sporočilo izbrisano",
|
||||||
|
|
||||||
"refreshedContacts": "Stiki osveženi"
|
"refreshedContacts": "Stiki osveženi",
|
||||||
|
|
||||||
|
"sarMarkerFoundPerson": "Najdena oseba",
|
||||||
|
|
||||||
|
"sarMarkerFire": "Lokacija ognja",
|
||||||
|
|
||||||
|
"sarMarkerStagingArea": "Zbirališče",
|
||||||
|
|
||||||
|
"sarMarkerObject": "Najden predmet",
|
||||||
|
|
||||||
|
"from": "Od",
|
||||||
|
|
||||||
|
"coordinates": "Koordinate",
|
||||||
|
|
||||||
|
"tapToViewOnMap": "Tapnite za prikaz na zemljevidu"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'providers/map_provider.dart';
|
|||||||
import 'providers/drawing_provider.dart';
|
import 'providers/drawing_provider.dart';
|
||||||
import 'providers/app_provider.dart';
|
import 'providers/app_provider.dart';
|
||||||
import 'services/tile_cache_service.dart';
|
import 'services/tile_cache_service.dart';
|
||||||
|
import 'services/notification_service.dart';
|
||||||
import 'services/locale_preferences.dart';
|
import 'services/locale_preferences.dart';
|
||||||
import 'screens/home_screen.dart';
|
import 'screens/home_screen.dart';
|
||||||
import 'theme/app_theme.dart';
|
import 'theme/app_theme.dart';
|
||||||
@@ -39,6 +40,8 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
Future<void> _initializeApp() async {
|
Future<void> _initializeApp() async {
|
||||||
await _loadThemePreference();
|
await _loadThemePreference();
|
||||||
await _loadLocalePreference();
|
await _loadLocalePreference();
|
||||||
|
// Initialize notification service
|
||||||
|
await NotificationService().initialize();
|
||||||
setState(() {
|
setState(() {
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,14 +4,18 @@ import 'package:flutter/foundation.dart';
|
|||||||
import '../models/message.dart';
|
import '../models/message.dart';
|
||||||
import '../models/sar_marker.dart';
|
import '../models/sar_marker.dart';
|
||||||
import '../services/message_storage_service.dart';
|
import '../services/message_storage_service.dart';
|
||||||
|
import '../services/notification_service.dart';
|
||||||
import '../utils/sar_message_parser.dart';
|
import '../utils/sar_message_parser.dart';
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
|
|
||||||
/// Messages Provider - manages message history and SAR markers
|
/// Messages Provider - manages message history and SAR markers
|
||||||
class MessagesProvider with ChangeNotifier {
|
class MessagesProvider with ChangeNotifier {
|
||||||
final List<Message> _messages = [];
|
final List<Message> _messages = [];
|
||||||
final Map<String, SarMarker> _sarMarkers = {};
|
final Map<String, SarMarker> _sarMarkers = {};
|
||||||
final MessageStorageService _storageService = MessageStorageService();
|
final MessageStorageService _storageService = MessageStorageService();
|
||||||
|
final NotificationService _notificationService = NotificationService();
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
AppLocalizations? _localizations;
|
||||||
|
|
||||||
// Track pending sent messages by expected ACK/TAG
|
// Track pending sent messages by expected ACK/TAG
|
||||||
final Map<int, Message> _pendingSentMessages = {};
|
final Map<int, Message> _pendingSentMessages = {};
|
||||||
@@ -49,6 +53,11 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
|
|
||||||
bool get isInitialized => _isInitialized;
|
bool get isInitialized => _isInitialized;
|
||||||
|
|
||||||
|
/// Set localizations for notifications
|
||||||
|
void setLocalizations(AppLocalizations localizations) {
|
||||||
|
_localizations = localizations;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get count of unread messages (excluding sent messages and system messages)
|
/// Get count of unread messages (excluding sent messages and system messages)
|
||||||
int get unreadCount => _messages
|
int get unreadCount => _messages
|
||||||
.where((m) =>
|
.where((m) =>
|
||||||
@@ -146,6 +155,11 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
final marker = finalMessage.toSarMarker();
|
final marker = finalMessage.toSarMarker();
|
||||||
if (marker != null) {
|
if (marker != null) {
|
||||||
_sarMarkers[marker.id] = marker;
|
_sarMarkers[marker.id] = marker;
|
||||||
|
|
||||||
|
// Trigger urgent notification for received SAR messages (not sent by user)
|
||||||
|
if (!finalMessage.isSentMessage) {
|
||||||
|
_triggerSarNotification(finalMessage, marker);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +245,31 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trigger urgent notification for SAR marker
|
||||||
|
Future<void> _triggerSarNotification(Message message, SarMarker marker) async {
|
||||||
|
try {
|
||||||
|
// Format coordinates
|
||||||
|
final coords = '${marker.location.latitude.toStringAsFixed(5)}, ${marker.location.longitude.toStringAsFixed(5)}';
|
||||||
|
|
||||||
|
// Get sender name from message
|
||||||
|
final senderName = message.senderName ?? message.senderKeyShort ?? 'Unknown';
|
||||||
|
|
||||||
|
print('🔔 [MessagesProvider] Triggering SAR notification for ${marker.type.displayName}');
|
||||||
|
print(' Sender: $senderName');
|
||||||
|
print(' Coordinates: $coords');
|
||||||
|
|
||||||
|
await _notificationService.showSarNotification(
|
||||||
|
type: marker.type,
|
||||||
|
senderName: senderName,
|
||||||
|
coordinates: coords,
|
||||||
|
notes: marker.notes,
|
||||||
|
localizations: _localizations,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ [MessagesProvider] Error triggering SAR notification: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Persist messages to storage (async, non-blocking)
|
/// Persist messages to storage (async, non-blocking)
|
||||||
Future<void> _persistMessages() async {
|
Future<void> _persistMessages() async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -451,6 +451,13 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Set localizations for notifications
|
||||||
|
final messagesProvider = context.read<MessagesProvider>();
|
||||||
|
final localizations = AppLocalizations.of(context);
|
||||||
|
if (localizations != null) {
|
||||||
|
messagesProvider.setLocalizations(localizations);
|
||||||
|
}
|
||||||
|
|
||||||
// Determine if we should hide the UI (only in fullscreen on map tab)
|
// Determine if we should hide the UI (only in fullscreen on map tab)
|
||||||
final shouldHideUI = _isMapFullscreen && _currentIndex == 2;
|
final shouldHideUI = _isMapFullscreen && _currentIndex == 2;
|
||||||
|
|
||||||
|
|||||||
353
lib/services/notification_service.dart
Normal file
353
lib/services/notification_service.dart
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:timezone/timezone.dart' as tz;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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)';
|
||||||
|
|
||||||
|
/// Initialize notification service
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (_isInitialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
print('📬 [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,
|
||||||
|
requestCriticalPermission: true, // For urgent SAR notifications
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combined initialization settings
|
||||||
|
final initSettings = InitializationSettings(
|
||||||
|
android: androidSettings,
|
||||||
|
iOS: darwinSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize plugin
|
||||||
|
await _notificationsPlugin.initialize(
|
||||||
|
initSettings,
|
||||||
|
onDidReceiveNotificationResponse: _onNotificationResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Request permissions
|
||||||
|
await _requestPermissions();
|
||||||
|
|
||||||
|
// Create notification channels (Android)
|
||||||
|
await _createNotificationChannels();
|
||||||
|
|
||||||
|
_isInitialized = true;
|
||||||
|
print('✅ [NotificationService] Initialized successfully');
|
||||||
|
print(' Permission granted: $_permissionGranted');
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ [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,
|
||||||
|
critical: true, // Request critical alert permission for urgent SAR notifications
|
||||||
|
);
|
||||||
|
_permissionGranted = granted ?? false;
|
||||||
|
print('📱 [NotificationService] iOS permissions granted: $_permissionGranted');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android 13+ permissions
|
||||||
|
final androidPlugin = _notificationsPlugin
|
||||||
|
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
|
||||||
|
if (androidPlugin != null) {
|
||||||
|
final granted = await androidPlugin.requestNotificationsPermission();
|
||||||
|
_permissionGranted = granted ?? false;
|
||||||
|
print('🤖 [NotificationService] Android permissions granted: $_permissionGranted');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('⚠️ [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'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await androidPlugin.createNotificationChannel(urgentChannel);
|
||||||
|
print('✅ [NotificationService] Created urgent notification channel');
|
||||||
|
} catch (e) {
|
||||||
|
print('⚠️ [NotificationService] Error creating channels: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle notification tap (foreground)
|
||||||
|
void _onNotificationResponse(NotificationResponse response) {
|
||||||
|
print('🔔 [NotificationService] Notification tapped: ${response.payload}');
|
||||||
|
// TODO: Navigate to map tab and show SAR marker
|
||||||
|
// This would require a callback to the app layer
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
|
print('⚠️ [NotificationService] Not initialized, skipping notification');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_permissionGranted) {
|
||||||
|
print('⚠️ [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.critical, // Critical alert (bypasses silent mode)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combined notification details
|
||||||
|
final notificationDetails = NotificationDetails(
|
||||||
|
android: androidDetails,
|
||||||
|
iOS: darwinDetails,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
await _notificationsPlugin.show(
|
||||||
|
notificationId,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
notificationDetails,
|
||||||
|
payload: 'sar:${type.name}:$coordinates',
|
||||||
|
);
|
||||||
|
|
||||||
|
print('✅ [NotificationService] Showed SAR notification: $title');
|
||||||
|
print(' Type: ${type.displayName}');
|
||||||
|
print(' Sender: $senderName');
|
||||||
|
print(' Coordinates: $coordinates');
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ [NotificationService] Error showing notification: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build notification title based on SAR marker type
|
||||||
|
String _buildNotificationTitle(SarMarkerType type, AppLocalizations? localizations) {
|
||||||
|
if (localizations == null) {
|
||||||
|
return '🚨 ${type.displayName} Detected';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case SarMarkerType.foundPerson:
|
||||||
|
return '🚨 ${localizations.sarMarkerFoundPerson}';
|
||||||
|
case SarMarkerType.fire:
|
||||||
|
return '🚨 ${localizations.sarMarkerFire}';
|
||||||
|
case SarMarkerType.stagingArea:
|
||||||
|
return '🚨 ${localizations.sarMarkerStagingArea}';
|
||||||
|
case SarMarkerType.object:
|
||||||
|
return '🚨 ${localizations.sarMarkerObject}';
|
||||||
|
case SarMarkerType.unknown:
|
||||||
|
return '🚨 SAR Alert';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel all notifications
|
||||||
|
Future<void> cancelAll() async {
|
||||||
|
try {
|
||||||
|
await _notificationsPlugin.cancelAll();
|
||||||
|
print('✅ [NotificationService] Cancelled all notifications');
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ [NotificationService] Error canceling notifications: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel specific notification
|
||||||
|
Future<void> cancel(int id) async {
|
||||||
|
try {
|
||||||
|
await _notificationsPlugin.cancel(id);
|
||||||
|
print('✅ [NotificationService] Cancelled notification: $id');
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ [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) {
|
||||||
|
print('⚠️ [NotificationService] Error checking notification status: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get pending notifications
|
||||||
|
Future<List<PendingNotificationRequest>> getPendingNotifications() async {
|
||||||
|
try {
|
||||||
|
return await _notificationsPlugin.pendingNotificationRequests();
|
||||||
|
} catch (e) {
|
||||||
|
print('⚠️ [NotificationService] Error getting pending notifications: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import flutter_blue_plus_darwin
|
import flutter_blue_plus_darwin
|
||||||
|
import flutter_local_notifications
|
||||||
import geolocator_apple
|
import geolocator_apple
|
||||||
import objectbox_flutter_libs
|
import objectbox_flutter_libs
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
@@ -15,6 +16,7 @@ import shared_preferences_foundation
|
|||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
||||||
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||||
ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin"))
|
ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
|
|||||||
32
pubspec.lock
32
pubspec.lock
@@ -246,6 +246,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_local_notifications:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications
|
||||||
|
sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "18.0.1"
|
||||||
|
flutter_local_notifications_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_linux
|
||||||
|
sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.0"
|
||||||
|
flutter_local_notifications_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_platform_interface
|
||||||
|
sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.0.0"
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -794,6 +818,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.6"
|
||||||
|
timezone:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: timezone
|
||||||
|
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.1"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user