import 'dart:developer' as dev; import 'dart:io'; import 'dart:math' as math; import 'package:alarm/alarm.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart' as firebase_messaging; import 'package:flutter/material.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import '../utils/app_logger_utils.dart'; import '../../firebase_options.dart'; import 'alarm_services.dart'; @pragma('vm:entry-point') class NotificationServices { static final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin(); final firebase_messaging.FirebaseMessaging _messaging = firebase_messaging.FirebaseMessaging.instance; AlarmServices alarmServices = AlarmServices(); Future initialize() async { await initializeLocalNotifications(); dev.log("NotificationService initialized"); } @pragma('vm:entry-point') Future initializeLocalNotifications() async { try { const androidInitSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); final darwinInitSettings = DarwinInitializationSettings( onDidReceiveLocalNotification: _onDidReceiveLocalNotification, ); final initSettings = InitializationSettings( android: androidInitSettings, iOS: darwinInitSettings, ); await _notificationsPlugin.initialize( initSettings, onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse, ); dev.log("Local notifications initialized"); } catch (e) { AppLoggerUtils.error("Failed to initialize local notifications", e); } } Future _onDidReceiveLocalNotification( int id, String? title, String? body, String? payload) async { // Handle local notification tap when app is foreground (iOS) dev.log("Local notification tapped (foreground): payload=$payload"); } Future _onDidReceiveNotificationResponse( NotificationResponse response) async { // Handle local notification tap (both foreground & background) final payload = response.payload; dev.log("Notification tapped: payload=$payload"); if (payload != null && payload.isNotEmpty) { final controller = PersistentTabController(initialIndex: 0); handleMessage(payload, controller); } } void firebaseInit(BuildContext context) { firebase_messaging.FirebaseMessaging.onMessage.listen((message) { _handleForegroundMessage(message); }); } void isTokenRefresh() { _messaging.onTokenRefresh.listen((newToken) { dev.log("Refresh Firebase Messaging Token: $newToken"); }); } static Future requestNotificationPermission() async { try { final messaging = firebase_messaging.FirebaseMessaging.instance; final settings = await messaging.requestPermission( alert: true, announcement: false, badge: true, provisional: false, sound: true, ); return settings.authorizationStatus == firebase_messaging.AuthorizationStatus.authorized; } catch (e) { AppLoggerUtils.error("Failed to request notification permission", e); return null; } } /// Xử lý message khi app foreground Future _handleForegroundMessage( firebase_messaging.RemoteMessage message) async { try { final type = message.data['type'] as String? ?? 'normal'; final title = message.notification?.title ?? message.data['title'] ?? 'SFM'; final body = message.notification?.body ?? message.data['body'] ?? ''; dev.log('Foreground message: type=$type, title=$title, body=$body'); if (type == 'smoke_warning') { // Hiển thị alarm, không hiển thị notification await _showAlarm(title, body); } else { // Hiển thị notification local await _showForegroundNotification(title, body, type); } } catch (e, stackTrace) { AppLoggerUtils.error('Error handling foreground message', e, stackTrace); } } /// Hiển thị alarm (smoke warning) Future _showAlarm(String title, String body) async { try { await Alarm.init(); final alarmSettings = AlarmSettings( id: 42, dateTime: DateTime.now(), assetAudioPath: 'assets/sounds/warning_alarm.mp3', loopAudio: true, vibrate: true, warningNotificationOnKill: Platform.isIOS, androidFullScreenIntent: true, volumeSettings: VolumeSettings.fade( volume: 0.8, fadeDuration: const Duration(seconds: 5), volumeEnforced: true, ), notificationSettings: NotificationSettings( title: title, body: body, stopButton: 'Dừng thông báo', icon: "ic_launcher", ), ); await Alarm.set(alarmSettings: alarmSettings); dev.log('Alarm set successfully: $title'); } catch (e, stackTrace) { AppLoggerUtils.error('Failed to set alarm', e, stackTrace); } } /// Hiển thị notification local khi app foreground Future _showForegroundNotification( String title, String body, String type) async { try { final androidDetails = AndroidNotificationDetails( 'sfm_channel_${math.Random.secure().nextInt(1000000)}', 'SFM Notifications', channelDescription: 'Notifications from SFM app', sound: getSound(type), importance: Importance.max, priority: Priority.high, ticker: 'ticker', ); final iosDetails = DarwinNotificationDetails( sound: _getSoundNameForIOS(type), presentAlert: true, presentBadge: true, presentBanner: true, presentSound: true, ); final notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await _notificationsPlugin.show( math.Random.secure().nextInt(1000000), title, body, notificationDetails, payload: type, ); dev.log('Foreground notification shown: title=$title, type=$type'); } catch (e, stackTrace) { AppLoggerUtils.error( 'Failed to show foreground notification', e, stackTrace); } } /// Hiển thị notification cho background/terminate Future showBackgroundOrTerminateNotification( firebase_messaging.RemoteMessage message) async { try { final type = message.data['type'] as String? ?? 'normal'; final title = message.notification?.title ?? message.data['title'] ?? 'SFM'; final body = message.notification?.body ?? message.data['body'] ?? ''; dev.log('Background/terminate notification: type=$type, title=$title'); // Cho Android: nếu type = smoke_warning, hiển thị alarm // Cho iOS: chỉ hiển thị notification, APNs payload sẽ phát sound natively if (type == 'smoke_warning' && Platform.isAndroid) { dev.log('→ Showing alarm for Android background'); await _showAlarm(title, body); return; } // Hiển thị notification (cho cả iOS lẫn Android, nhưng iOS không có alarm) final channelId = math.Random.secure().nextInt(1000000).toString(); const channelName = 'SFM Notifications'; const channelDescription = 'Notifications from SFM app'; final androidDetails = AndroidNotificationDetails( channelId, channelName, channelDescription: channelDescription, sound: getSound(type), importance: Importance.max, priority: Priority.high, ticker: 'ticker', ); final iosDetails = DarwinNotificationDetails( sound: _getSoundNameForIOS(type), presentAlert: true, presentBadge: true, presentBanner: true, presentSound: true, ); final notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await _notificationsPlugin.show( int.parse(channelId), title, body, notificationDetails, payload: type, ); dev.log( 'Background/terminate notification shown: title=$title, type=$type'); } catch (e, stackTrace) { AppLoggerUtils.error( 'Failed to show background/terminate notification', e, stackTrace); } } AndroidNotificationSound getSound(String type) { if (type == "welcome") { return const RawResourceAndroidNotificationSound("welcome"); } else if (type == "success") { return const RawResourceAndroidNotificationSound("success_alert"); } else if (type == "smoke_warning") { return const RawResourceAndroidNotificationSound("warning_alarm"); } else if (type == "battery_warning") { return const RawResourceAndroidNotificationSound("new_alarm"); } else { return const RawResourceAndroidNotificationSound("warning_alarm"); } } String _getSoundNameForIOS(String type) { // iOS tìm file trong app bundle (không có đuôi) if (type == "smoke_warning" || type == "battery_warning") { return "warning_alarm"; // file: warning_alarm.caf trong bundle } return "default"; } void handleMessage(String? payload, PersistentTabController controller) { AppLoggerUtils.info("Handling notification tap with payload: $payload"); controller.jumpToTab(1); if (payload == "smoke_warning") { // TODO: Navigate to smoke warning screen nếu cần // NavigationRouter.navigateToSmokeWarningScreen(); } else if (payload == "battery_warning") { // TODO: Navigate to battery warning screen nếu cần // NavigationRouter.navigateToBatteryWarningScreen(); } } Future setupInteractMessage(PersistentTabController controller) async { // Khi app terminated, được mở bằng notification firebase_messaging.RemoteMessage? initialMessage = await firebase_messaging.FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { WidgetsBinding.instance.addPostFrameCallback((_) { final type = initialMessage.data['type'] as String?; handleMessage(type, controller); }); } // Khi app background, user tap notification firebase_messaging.FirebaseMessaging.onMessageOpenedApp.listen((message) { try { final type = message.data['type'] as String?; handleMessage(type, controller); } catch (e, stackTrace) { AppLoggerUtils.error('Error in onMessageOpenedApp', e, stackTrace); } }); } @pragma('vm:entry-point') static Future firebaseMessagingBackgroundHandler( firebase_messaging.RemoteMessage message) async { try { // Log to device storage for debugging background isolate dev.log('═══ BACKGROUND HANDLER STARTED ═══'); dev.log('Message data: ${message.data}'); dev.log('Notification: ${message.notification}'); await Alarm.init(); dev.log('✓ Alarm initialized'); if (Firebase.apps.isEmpty) { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, name: "sfm-notification", ); dev.log('✓ Firebase initialized in background'); } else { dev.log('✓ Firebase already initialized'); } final type = message.data['type'] as String? ?? 'normal'; final title = message.notification?.title ?? message.data['title'] ?? 'Thông báo từ SmartFM'; final body = message.notification?.body ?? message.data['body'] ?? 'Bạn có một thông báo mới'; dev.log('Message type: $type'); dev.log('Title: $title'); dev.log('Body: $body'); // Logic: Hiển thị local notification cho background/terminate // iOS sẽ phát sound natively thông qua APNs payload (aps.sound) // Android sẽ phát sound thông qua local notification try { final notificationService = NotificationServices(); await notificationService.initialize(); dev.log('✓ Notification service initialized'); await notificationService .showBackgroundOrTerminateNotification(message); dev.log('✓ Background/terminate notification shown'); } catch (notifError, notifStackTrace) { dev.log('✗ ERROR showing notification: $notifError'); dev.log('Stack trace: $notifStackTrace'); rethrow; } dev.log('═══ BACKGROUND HANDLER COMPLETED ═══'); } catch (e, stackTrace) { dev.log('✗ BACKGROUND HANDLER FAILED'); dev.log('Error: $e'); dev.log('Stack trace: $stackTrace'); } } }