From 1b801a10ef507a2dcf2dc5bba668568ade7a78c0 Mon Sep 17 00:00:00 2001 From: Janez T Date: Sat, 14 Mar 2026 10:07:11 +0100 Subject: [PATCH] Remove hex logs and keep path data --- lib/providers/app_provider.dart | 16 ++-- lib/providers/drawing_provider.dart | 3 + lib/providers/messages_provider.dart | 29 +++++-- lib/screens/settings_screen.dart | 91 ++++++++++++++++++++-- lib/services/notification_service.dart | 101 +++++++++++++++++++++++++ 5 files changed, 218 insertions(+), 22 deletions(-) diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 2be800b..615d39f 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -249,17 +249,14 @@ class AppProvider with ChangeNotifier { /// Sync drawings from messages on app startup (before BLE connection) Future _syncDrawingsOnStartup() async { - // Wait for MessagesProvider to finish initializing - // DrawingProvider loads around the same time + // Wait for both MessagesProvider and DrawingProvider to finish initializing int attempts = 0; - while (!messagesProvider.isInitialized && attempts < 20) { + while ((!messagesProvider.isInitialized || !drawingProvider.isInitialized) && + attempts < 40) { await Future.delayed(const Duration(milliseconds: 50)); attempts++; } - // Give DrawingProvider a moment to finish loading too - await Future.delayed(const Duration(milliseconds: 100)); - _restoreSessionMetadataFromMessages(); debugPrint( @@ -975,9 +972,10 @@ class AppProvider with ChangeNotifier { if (AppProvider.shouldIgnoreSelfReplay( message: message, ownPublicKey: connectionProvider.deviceInfo.publicKey, - ownName: - connectionProvider.deviceInfo.deviceName ?? - connectionProvider.deviceInfo.selfName, + ownName: _preferredSelfDisplayName( + deviceName: connectionProvider.deviceInfo.deviceName, + selfName: connectionProvider.deviceInfo.selfName, + ), )) { debugPrint('⏭️ [AppProvider] Ignoring self replay: ${message.id}'); return; diff --git a/lib/providers/drawing_provider.dart b/lib/providers/drawing_provider.dart index f97c551..a986a7a 100644 --- a/lib/providers/drawing_provider.dart +++ b/lib/providers/drawing_provider.dart @@ -22,6 +22,7 @@ class DrawingProvider with ChangeNotifier { // Completed drawings final List _drawings = []; + bool _isInitialized = false; // In-progress drawing MapDrawing? _currentDrawing; @@ -53,6 +54,7 @@ class DrawingProvider with ChangeNotifier { List get currentLinePoints => List.unmodifiable(_currentLinePoints); LatLng? get rectangleStartPoint => _rectangleStartPoint; bool get isDrawing => _drawingMode != DrawingMode.none; + bool get isInitialized => _isInitialized; LatLng? get measurementPoint1 => _measurementPoint1; LatLng? get measurementPoint2 => _measurementPoint2; double? get measuredDistance => _measuredDistance; @@ -61,6 +63,7 @@ class DrawingProvider with ChangeNotifier { Future initialize() async { await _loadPreferences(); await _loadDrawings(); + _isInitialized = true; } /// Set drawing mode diff --git a/lib/providers/messages_provider.dart b/lib/providers/messages_provider.dart index ded9d76..36e2fae 100644 --- a/lib/providers/messages_provider.dart +++ b/lib/providers/messages_provider.dart @@ -28,6 +28,8 @@ class MessagesProvider with ChangeNotifier { final MessageStorageService _storageService = MessageStorageService(); final NotificationService _notificationService = NotificationService(); bool _isInitialized = false; + bool _isPersisting = false; + bool _persistRequested = false; AppLocalizations? _localizations; final Map _messageContactLocations = {}; final Map _messageReceptionDetails = {}; @@ -1018,18 +1020,29 @@ class MessagesProvider with ChangeNotifier { return '${mib.toStringAsFixed(mib >= 10 ? 0 : 1)} MB'; } - /// Persist messages to storage (async, non-blocking) + /// Persist messages to storage (async, non-blocking, coalescing). + /// + /// Multiple rapid calls are coalesced into a single write to avoid + /// overlapping serialization and redundant SharedPreferences writes. Future _persistMessages() async { + _persistRequested = true; + if (_isPersisting) return; // A write is in flight; it will pick up our changes. + _isPersisting = true; try { - await _storageService.saveMessages( - _messages, - messageContactLocations: _messageContactLocations, - messageReceptionDetails: _messageReceptionDetails, - messageTransferDetails: _messageTransferDetails, - messageRouteMetadata: _messageRouteMetadata, - ); + while (_persistRequested) { + _persistRequested = false; + await _storageService.saveMessages( + _messages, + messageContactLocations: _messageContactLocations, + messageReceptionDetails: _messageReceptionDetails, + messageTransferDetails: _messageTransferDetails, + messageRouteMetadata: _messageRouteMetadata, + ); + } } catch (e) { debugPrint('❌ [MessagesProvider] Error persisting messages: $e'); + } finally { + _isPersisting = false; } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e284091..7102274 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -25,6 +25,7 @@ import '../services/image_preferences.dart'; import '../services/route_hash_preferences.dart'; import '../services/image_codec_service.dart'; import '../services/developer_mode_service.dart'; +import '../services/notification_service.dart'; import '../utils/sample_data_generator.dart'; import '../utils/image_message_parser.dart'; import '../utils/voice_message_parser.dart'; @@ -77,6 +78,10 @@ class _SettingsScreenState extends State { bool _rotateMapWithHeading = false; bool _showMapDebugInfo = false; bool _openMapInFullscreen = false; + bool _messageNotificationsEnabled = true; + bool _sarNotificationsEnabled = true; + bool _updateNotificationsEnabled = true; + bool _muteForegroundNotifications = true; bool _isDeveloperModeEnabled = false; DateTime? _onlineTraceCacheUpdatedAt; bool _isClearingOnlineTraceCache = false; @@ -99,6 +104,7 @@ class _SettingsScreenState extends State { _loadDeveloperMode(); _loadOnlineTraceCacheStatus(); _loadMapPreferences(); + _loadNotificationPreferences(); } @override @@ -136,6 +142,18 @@ class _SettingsScreenState extends State { }); } + Future _loadNotificationPreferences() async { + final service = NotificationService(); + await service.initialize(); + if (!mounted) return; + setState(() { + _messageNotificationsEnabled = service.messageNotificationsEnabled; + _sarNotificationsEnabled = service.sarNotificationsEnabled; + _updateNotificationsEnabled = service.updateNotificationsEnabled; + _muteForegroundNotifications = service.muteForegroundNotifications; + }); + } + Future _loadOnlineTraceCacheStatus() async { final cachedAt = await MeshMapNodesService.cachedAt(); if (!mounted) return; @@ -187,8 +205,7 @@ class _SettingsScreenState extends State { final prefs = await SharedPreferences.getInstance(); if (!mounted) return; setState(() { - _rotateMapWithHeading = - prefs.getBool('map_rotate_with_heading') ?? false; + _rotateMapWithHeading = prefs.getBool('map_rotate_with_heading') ?? false; _showMapDebugInfo = prefs.getBool('map_show_debug_info') ?? false; _openMapInFullscreen = prefs.getBool('map_fullscreen') ?? false; }); @@ -1002,6 +1019,72 @@ class _SettingsScreenState extends State { ), ]), + _buildSectionHeader('Notifications'), + _buildSettingsCard([ + SwitchListTile( + secondary: const Icon(Icons.chat_bubble_outline), + title: const Text('Message notifications'), + subtitle: const Text( + 'Notify for incoming direct and channel messages', + ), + value: _messageNotificationsEnabled, + onChanged: (value) async { + setState(() { + _messageNotificationsEnabled = value; + }); + await NotificationService().setMessageNotificationsEnabled( + value, + ); + }, + ), + SwitchListTile( + secondary: const Icon(Icons.warning_amber_outlined), + title: const Text('SAR alerts'), + subtitle: const Text( + 'Notify for incoming SAR markers such as found person or fire', + ), + value: _sarNotificationsEnabled, + onChanged: (value) async { + setState(() { + _sarNotificationsEnabled = value; + }); + await NotificationService().setSarNotificationsEnabled(value); + }, + ), + SwitchListTile( + secondary: const Icon(Icons.system_update), + title: const Text('Update notifications'), + subtitle: const Text( + 'Notify when a newer app version is available', + ), + value: _updateNotificationsEnabled, + onChanged: (value) async { + setState(() { + _updateNotificationsEnabled = value; + }); + await NotificationService().setUpdateNotificationsEnabled( + value, + ); + }, + ), + SwitchListTile( + secondary: const Icon(Icons.visibility_off_outlined), + title: const Text('Mute while app is open'), + subtitle: const Text( + 'Do not show local notifications while the app is in the foreground', + ), + value: _muteForegroundNotifications, + onChanged: (value) async { + setState(() { + _muteForegroundNotifications = value; + }); + await NotificationService().setMuteForegroundNotifications( + value, + ); + }, + ), + ]), + _buildSectionHeader('Navigation'), _buildSettingsCard([ Consumer( @@ -1224,9 +1307,7 @@ class _SettingsScreenState extends State { builder: (context, drawingProvider, child) => SwitchListTile( secondary: const Icon(Icons.fmd_good_outlined), title: const Text('Show SAR markers'), - subtitle: const Text( - 'Display SAR markers on the main map', - ), + subtitle: const Text('Display SAR markers on the main map'), value: drawingProvider.showSarMarkers, onChanged: (value) { drawingProvider.toggleSarMarkers(); diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 7f25e80..3cc1e0e 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:timezone/data/latest_all.dart' as tz; import '../models/sar_marker.dart'; import '../l10n/app_localizations.dart'; @@ -13,9 +14,18 @@ class NotificationService { final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin(); + static const String _prefMessagesEnabled = 'notifications_messages_enabled'; + static const String _prefSarEnabled = 'notifications_sar_enabled'; + static const String _prefUpdatesEnabled = 'notifications_updates_enabled'; + static const String _prefMuteForeground = 'notifications_mute_foreground'; bool _isInitialized = false; bool _permissionGranted = false; + bool _messageNotificationsEnabled = true; + bool _sarNotificationsEnabled = true; + bool _updateNotificationsEnabled = true; + bool _muteForegroundNotifications = true; + AppLifecycleState _lifecycleState = AppLifecycleState.resumed; // Notification IDs static const int _sarNotificationId = 1000; @@ -38,12 +48,21 @@ class NotificationService { static const String _updateChannelDescription = 'Notifications for available app updates'; + bool get messageNotificationsEnabled => _messageNotificationsEnabled; + bool get sarNotificationsEnabled => _sarNotificationsEnabled; + bool get updateNotificationsEnabled => _updateNotificationsEnabled; + bool get muteForegroundNotifications => _muteForegroundNotifications; + bool get isAppInForeground => _lifecycleState == AppLifecycleState.resumed; + /// Initialize notification service Future initialize() async { if (_isInitialized) return; try { debugPrint('📬 [NotificationService] Initializing...'); + WidgetsBinding.instance.addObserver(_LifecycleObserver(this)); + _lifecycleState = + WidgetsBinding.instance.lifecycleState ?? AppLifecycleState.resumed; // Initialize timezone data tz.initializeTimeZones(); @@ -74,6 +93,7 @@ class NotificationService { // Request permissions await _requestPermissions(); + await _loadPreferences(); // Create notification channels (Android) await _createNotificationChannels(); @@ -132,6 +152,46 @@ class NotificationService { } } + Future _loadPreferences() async { + final prefs = await SharedPreferences.getInstance(); + _messageNotificationsEnabled = prefs.getBool(_prefMessagesEnabled) ?? true; + _sarNotificationsEnabled = prefs.getBool(_prefSarEnabled) ?? true; + _updateNotificationsEnabled = prefs.getBool(_prefUpdatesEnabled) ?? true; + _muteForegroundNotifications = prefs.getBool(_prefMuteForeground) ?? true; + } + + Future setMessageNotificationsEnabled(bool value) async { + _messageNotificationsEnabled = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_prefMessagesEnabled, value); + } + + Future setSarNotificationsEnabled(bool value) async { + _sarNotificationsEnabled = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_prefSarEnabled, value); + } + + Future setUpdateNotificationsEnabled(bool value) async { + _updateNotificationsEnabled = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_prefUpdatesEnabled, value); + } + + Future setMuteForegroundNotifications(bool value) async { + _muteForegroundNotifications = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_prefMuteForeground, value); + } + + void handleAppLifecycleStateChanged(AppLifecycleState state) { + _lifecycleState = state; + } + + bool _shouldSuppressForegroundNotifications() { + return _muteForegroundNotifications && isAppInForeground; + } + /// Create notification channels for Android Future _createNotificationChannels() async { try { @@ -222,6 +282,16 @@ class NotificationService { ); return; } + if (!_sarNotificationsEnabled) { + debugPrint('ℹ️ [NotificationService] SAR notifications disabled'); + return; + } + if (_shouldSuppressForegroundNotifications()) { + debugPrint( + 'ℹ️ [NotificationService] App in foreground, skipping SAR notification', + ); + return; + } try { // Generate unique notification ID based on timestamp @@ -394,6 +464,16 @@ class NotificationService { ); return; } + if (!_messageNotificationsEnabled) { + debugPrint('ℹ️ [NotificationService] Message notifications disabled'); + return; + } + if (_shouldSuppressForegroundNotifications()) { + debugPrint( + 'ℹ️ [NotificationService] App in foreground, skipping message notification', + ); + return; + } try { // Generate unique notification ID based on timestamp @@ -547,6 +627,16 @@ class NotificationService { ); return; } + if (!_updateNotificationsEnabled) { + debugPrint('ℹ️ [NotificationService] Update notifications disabled'); + return; + } + if (_shouldSuppressForegroundNotifications()) { + debugPrint( + 'ℹ️ [NotificationService] App in foreground, skipping update notification', + ); + return; + } try { // Build notification title and body @@ -618,3 +708,14 @@ class NotificationService { } } } + +class _LifecycleObserver with WidgetsBindingObserver { + _LifecycleObserver(this._service); + + final NotificationService _service; + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + _service.handleAppLifecycleStateChanged(state); + } +}