import 'dart:async'; import 'dart:convert'; import 'package:mqtt5_client/mqtt5_client.dart'; import 'package:mqtt5_client/mqtt5_server_client.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; /// Service de gestion des notifications chat via MQTT /// /// Utilise MQTT pour recevoir des notifications en temps réel /// et afficher des notifications locales class MqttNotificationService { static final MqttNotificationService _instance = MqttNotificationService._internal(); factory MqttNotificationService() => _instance; MqttNotificationService._internal(); late MqttServerClient _client; final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); // Configuration final String mqttHost; final int mqttPort; final String mqttUsername; final String mqttPassword; final String clientId; // État bool _initialized = false; String? _userId; StreamSubscription? _messageSubscription; // Callbacks Function(String messageId)? onMessageTap; Function(Map)? onNotificationReceived; MqttNotificationService({ this.mqttHost = 'mqtt.geosector.fr', this.mqttPort = 1883, this.mqttUsername = '', this.mqttPassword = '', String? clientId, }) : clientId = clientId ?? 'geosector_chat_${DateTime.now().millisecondsSinceEpoch}'; /// Initialise le service de notifications Future initialize({required String userId}) async { if (_initialized) return; _userId = userId; // Initialiser les notifications locales await _initializeLocalNotifications(); // Initialiser le client MQTT await _initializeMqttClient(); _initialized = true; } /// Initialise le client MQTT Future _initializeMqttClient() async { try { _client = MqttServerClient.withPort(mqttHost, clientId, mqttPort); _client.logging(on: kDebugMode); _client.keepAlivePeriod = 60; _client.onConnected = _onConnected; _client.onDisconnected = _onDisconnected; _client.onSubscribed = _onSubscribed; _client.autoReconnect = true; // Configurer les options de connexion final connMessage = MqttConnectMessage() .authenticateAs(mqttUsername, mqttPassword) .withClientIdentifier(clientId) .startClean() .keepAliveFor(60); _client.connectionMessage = connMessage; // Se connecter await _connect(); } catch (e) { debugPrint('Erreur lors de l\'initialisation MQTT : $e'); rethrow; } } /// Se connecte au broker MQTT Future _connect() async { try { await _client.connect(); } catch (e) { debugPrint('Erreur de connexion MQTT : $e'); _client.disconnect(); rethrow; } } /// Callback lors de la connexion void _onConnected() { debugPrint('Connecté au broker MQTT'); // S'abonner aux topics de l'utilisateur if (_userId != null) { _subscribeToUserTopics(_userId!); } // Écouter les messages _messageSubscription = _client.updates?.listen(_onMessageReceived); } /// Callback lors de la déconnexion void _onDisconnected() { debugPrint('Déconnecté du broker MQTT'); // Tenter une reconnexion if (_client.autoReconnect) { Future.delayed(const Duration(seconds: 5), () { _connect(); }); } } /// Callback lors de l'abonnement void _onSubscribed(MqttSubscription subscription) { debugPrint('Abonné au topic : ${subscription.topic.rawTopic}'); } /// S'abonner aux topics de l'utilisateur void _subscribeToUserTopics(String userId) { // Topic pour les messages personnels _client.subscribe('chat/user/$userId/messages', MqttQos.atLeastOnce); // Topic pour les annonces _client.subscribe('chat/announcement', MqttQos.atLeastOnce); // Topic pour les groupes de l'utilisateur (si disponibles) _client.subscribe('chat/user/$userId/groups/+', MqttQos.atLeastOnce); } /// Gère les messages reçus void _onMessageReceived(List> messages) { for (var message in messages) { final topic = message.topic; final payload = message.payload as MqttPublishMessage; final messageText = MqttUtilities.bytesToStringAsString(payload.payload.message!); try { final data = jsonDecode(messageText) as Map; _handleNotification(topic, data); } catch (e) { debugPrint('Erreur lors du décodage du message : $e'); } } } /// Traite la notification reçue Future _handleNotification(String topic, Map data) async { // Vérifier les paramètres de notification de l'utilisateur if (!await _shouldShowNotification(data)) { return; } String title = ''; String body = ''; String messageId = ''; String conversationId = ''; if (topic.startsWith('chat/user/')) { // Message personnel title = data['senderName'] ?? 'Nouveau message'; body = data['content'] ?? ''; messageId = data['messageId'] ?? ''; conversationId = data['conversationId'] ?? ''; } else if (topic.startsWith('chat/announcement')) { // Annonce title = data['title'] ?? 'Annonce'; body = data['content'] ?? ''; messageId = data['messageId'] ?? ''; conversationId = data['conversationId'] ?? ''; } // Afficher la notification locale await _showLocalNotification( title: title, body: body, payload: jsonEncode({ 'messageId': messageId, 'conversationId': conversationId, }), ); // Appeler le callback si défini onNotificationReceived?.call(data); } /// Vérifie si la notification doit être affichée Future _shouldShowNotification(Map data) async { // TODO: Vérifier les paramètres de notification de l'utilisateur // - Notifications désactivées // - Conversation en silencieux // - Mode Ne pas déranger return true; } /// Initialise les notifications locales Future _initializeLocalNotifications() async { const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); const iosSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); const initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, ); await _localNotifications.initialize( initSettings, onDidReceiveNotificationResponse: _onNotificationTap, ); } /// Affiche une notification locale Future _showLocalNotification({ required String title, required String body, required String payload, }) async { const androidDetails = AndroidNotificationDetails( 'chat_messages', 'Messages de chat', channelDescription: 'Notifications pour les nouveaux messages de chat', importance: Importance.high, priority: Priority.high, icon: '@mipmap/ic_launcher', ); const iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); const notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await _localNotifications.show( DateTime.now().microsecondsSinceEpoch, title, body, notificationDetails, payload: payload, ); } /// Handler pour le clic sur une notification void _onNotificationTap(NotificationResponse response) { final payload = response.payload; if (payload != null) { try { final data = jsonDecode(payload) as Map; final messageId = data['messageId'] as String?; if (messageId != null) { onMessageTap?.call(messageId); } } catch (e) { debugPrint('Erreur lors du traitement du clic sur notification : $e'); } } } /// Publie un message MQTT Future publishMessage(String topic, Map message) async { if (_client.connectionStatus?.state != MqttConnectionState.connected) { await _connect(); } final messagePayload = jsonEncode(message); final builder = MqttPayloadBuilder(); builder.addString(messagePayload); _client.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!); } /// S'abonner à une conversation spécifique Future subscribeToConversation(String conversationId) async { if (_client.connectionStatus?.state == MqttConnectionState.connected) { _client.subscribe('chat/conversation/$conversationId', MqttQos.atLeastOnce); } } /// Se désabonner d'une conversation Future unsubscribeFromConversation(String conversationId) async { if (_client.connectionStatus?.state == MqttConnectionState.connected) { _client.unsubscribeStringTopic('chat/conversation/$conversationId'); } } /// Désactive temporairement les notifications void pauseNotifications() { _client.pause(); } /// Réactive les notifications void resumeNotifications() { _client.resume(); } /// Libère les ressources void dispose() { _messageSubscription?.cancel(); _client.disconnect(); _initialized = false; } }