import 'dart:convert'; import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:google_api_availability/google_api_availability.dart'; import 'package:huawei_push/huawei_push.dart' as h_push; import 'package:test_sa/controllers/notification/notification_manger.dart'; import 'package:test_sa/models/device/device_transfer.dart'; import 'package:test_sa/models/new_models/gas_refill_model.dart'; import 'package:test_sa/modules/cm_module/cm_detail_page.dart'; import 'package:test_sa/modules/cx_module/survey/survey_page.dart'; import 'package:test_sa/modules/pm_module/ppm_wo/ppm_details_page.dart'; import 'package:test_sa/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart'; import 'package:test_sa/modules/tm_module/device_transfer/device_transfer_details.dart'; import 'package:test_sa/modules/tm_module/tasks/task_request_detail_view.dart'; import 'package:test_sa/modules/tm_module/gas_refill/gas_refill_details.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; /// Background notification handler - runs in separate isolate /// Cannot access BuildContext or UI, must use flutter_local_notifications directly @pragma('vm:entry-point') Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { if (kDebugMode) { print('Background message received: ${message.messageId}'); print('Has notification field: ${message.notification != null}'); print('Data: ${message.data}'); } // Skip if it's a notification that should not be shown if (message.data["notificationType"] == 'NurseConfirmArrive') { return; } // For Android: Show notification manually // Even if android.notification config exists in payload, if message.notification is null, // we need to show it using data from the data field if (Platform.isAndroid) { await _showBackgroundNotification(message); } // For iOS, system handles background notifications automatically // if the payload includes the 'notification' field } /// Show background notification with grouping support /// This runs in background isolate, so we use flutter_local_notifications directly Future _showBackgroundNotification(RemoteMessage message) async { final FlutterLocalNotificationsPlugin notificationsPlugin = FlutterLocalNotificationsPlugin(); // Initialize plugin in background isolate const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@drawable/ic_stat_name'); const InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid); await notificationsPlugin.initialize(initializationSettings); // Create notification channel in background handler final AndroidFlutterLocalNotificationsPlugin? androidImplementation = notificationsPlugin.resolvePlatformSpecificImplementation(); if (androidImplementation != null) { const AndroidNotificationChannel channel = AndroidNotificationChannel( 'com.hmg.atoms', 'ATOMS', description: 'Push notification service for ATOMS', importance: Importance.max, playSound: true, enableVibration: true, showBadge: true, ); await androidImplementation.createNotificationChannel(channel); } // Extract data from message final String title = message.notification?.title ?? message.data['title']?.toString() ?? 'ATOMS'; final String body = message.notification?.body ?? message.data['body']?.toString() ?? ''; final String? groupId = message.data['group-id']?.toString(); final int notificationId = DateTime.now().millisecondsSinceEpoch % 100000; if (kDebugMode) { print('Background notification - Title: $title, Body: $body, GroupId: $groupId'); print('message.data[title]: ${message.data['title']}'); print('message.data[body]: ${message.data['body']}'); print('message.data[group-id]: ${message.data['group-id']}'); } // Create notification details with grouping if group-id is provided final AndroidNotificationDetails androidDetails = AndroidNotificationDetails( 'com.hmg.atoms', 'ATOMS', channelDescription: 'Push notification service for ATOMS', importance: Importance.max, priority: Priority.max, icon: '@drawable/ic_stat_name', playSound: true, enableVibration: true, groupKey: groupId, // Use group-id to separate different notification groups setAsGroupSummary: false, // Individual notification, not summary ); final NotificationDetails notificationDetails = NotificationDetails(android: androidDetails); await notificationsPlugin.show( notificationId, title, body, notificationDetails, payload: json.encode(message.data), ); } class FirebaseNotificationManger { static FirebaseMessaging messaging = FirebaseMessaging.instance; static String? token; // Store initial notification payload when app opened from killed/background state static Map? _pendingNotificationPayload; static Future getToken() async { NotificationSettings settings = await messaging.requestPermission(alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true); if (Platform.isAndroid) { try { if (!(await isGoogleServicesAvailable())) { h_push.Push.enableLogger(); await h_push.Push.setAutoInitEnabled(true); h_push.Push.onMessageReceivedStream.listen(_onMessageReceived, onError: _onMessageReceiveError); h_push.Push.getTokenStream.listen((hToken) { // onToken(token); // print("Huawei Token: ${hToken}"); token = hToken; }, onError: (e) { print("Huawei TokenError" + e.toString()); }); h_push.Push.getToken(''); } else { if (settings.authorizationStatus == AuthorizationStatus.authorized) { try { token = await messaging.getToken(); } catch (ex) {} } } } catch (ex) { print("Notification Exception: " + ex.toString()); } } else { if (settings.authorizationStatus == AuthorizationStatus.authorized) { try { token = await messaging.getToken(); } catch (ex) {} } } //print("pushToken:$token"); } static void _onMessageReceived(h_push.RemoteMessage remoteMessage) { if (kDebugMode) { print("onMessageReceivedStream:${remoteMessage.toMap()}"); } // Handle Huawei push notification display with grouping support // Extract notification data and group-id from the message try { final messageMap = remoteMessage.toMap(); final dataPayload = messageMap['dataPayload'] as Map?; if (dataPayload != null && dataPayload["notificationType"] != 'NurseConfirmArrive') { String? groupId = dataPayload["group-id"]?.toString(); // You may need to show notification here for Huawei devices // depending on your Huawei Push implementation } } catch (e) { if (kDebugMode) { print("Error processing Huawei message: $e"); } } } static void _onMessageReceiveError(Object error) { print("onMessageReceivedStream:${error.toString()}"); } static Future isGoogleServicesAvailable() async { try { GooglePlayServicesAvailability availability = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability(); String status = availability.toString().split('.').last; if (status == "success") { return true; } return false; } catch (ex) { return false; } } static void handleMessage(context, Map messageData) { if (messageData["requestType"] != null && messageData["requestNumber"] != null) { Widget? serviceClass; String? transactionType = messageData["transactionType"]?.toString(); if (transactionType == null) { return; } else if (transactionType == "17" && messageData["requestType"] == "chat") { int moduleId = int.parse(messageData["moduleId"].toString()); int requestNumber = int.parse(messageData["requestNumber"].toString()); switch (moduleId) { case 1: // cm serviceClass = CMDetailPage(requestId: requestNumber, moduleId: moduleId); break; case 2: // gas refill serviceClass = GasRefillDetailsPage( priority: messageData["priority"], date: messageData["createdOn"], moduleId: moduleId, model: GasRefillModel(id: requestNumber), ); break; case 3: //transfer serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: requestNumber), moduleId: moduleId); break; case 6: // task serviceClass = TaskRequestDetailsView( taskId: requestNumber, moduleId: moduleId, // requestDetails: RequestsDetails(nameOfType: requestData?.nameOfType, status: requestData?.statusName, priority: requestData?.priorityName, date: requestData?.transactionDate, ), ); // ServiceRequestDetailMain(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); break; default: serviceClass = const Scaffold(appBar: DefaultAppBar(), body: Center(child: NoDataFound())); } Navigator.of(context).push(MaterialPageRoute(builder: (_) => serviceClass!)); return; } // PPM=1, // ServiceRequestEngineer = 3, // AssetTransfer=7, // SparePartTransaction= 8, // GasRefill=9, // TechnicalRetirmentWO = 11, // Recurrent = 12, switch (transactionType) { case "1": serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString())); break; //these three request are same corrective maintenance.... case "3": serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); break; case "8": serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); break; case "11": serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); break; case "7": serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: int.parse(messageData["requestNumber"].toString())), moduleId: 3); break; case "9": serviceClass = GasRefillDetailsPage( priority: messageData["priority"], date: messageData["createdOn"], moduleId: 2, model: GasRefillModel(id: int.parse(messageData["requestNumber"].toString())), ); break; case "12": serviceClass = RecurrentWorkOrderView(taskId: int.parse(messageData["requestNumber"].toString())); case "17": serviceClass = SurveyPage(surveyId: int.parse(messageData["requestNumber"].toString())); //Didn't handle task request yet... // case 6: // serviceClass = TaskRequestDetailsView( // taskId: int.parse(messageData["requestNumber"].toString()), // requestDetails: RequestsDetails(nameOfType: messageData["sourceName"], status: messageData["statusName"], priority: messageData["priorityName"], date: messageData["createdDate"])); // return; default: serviceClass = const Scaffold(appBar: DefaultAppBar(), body: Center(child: NoDataFound())); } // if (messageData["requestType"] == "Service request to engineer") { // serviceClass = ServiceRequestDetailMain(requestId: messageData["requestNumber"] ?? ''); // } else if (messageData["requestType"] == "Gas Refill") { // serviceClass = GasRefillDetailsPage( // priority: messageData["priority"], // date: messageData["createdOn"], // model: GasRefillModel(id: int.parse(messageData["requestNumber"].toString())), // ); // } else if (messageData["requestType"] == "Asset Transfer") { // serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: int.parse(messageData["requestNumber"].toString()))); // } else if (messageData["requestType"] == "PPM") { // serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString())); // } // else if (data["requestType"] == "WorkOrder") { // // } Navigator.of(context).push(MaterialPageRoute(builder: (_) => serviceClass!)); } } static initialized(BuildContext context) async { //TOD0 add platform check here also if (!(await isGoogleServicesAvailable()) && Platform.isAndroid) { // Huawei device - initial notification already captured in main.dart // Only set up the listener for when app is opened from background if (kDebugMode) { print('[FCM] Huawei device - setting up onNotificationOpenedApp listener'); } h_push.Push.onNotificationOpenedApp.listen((message) { try { if (message is Map) { Map remoteData = message; remoteData = remoteData["extras"]; handleMessage(context, remoteData); } } catch (ex) { print("parsingError:$ex"); } }, onError: (e) => print("onNotificationOpenedApp Error${e.toString()}")); return; } NotificationSettings settings; try { settings = await messaging.requestPermission( alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true, ); } catch (error) { return; } if (settings.authorizationStatus != AuthorizationStatus.authorized) { return; } await FirebaseMessaging.instance.setAutoInitEnabled(true); await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(alert: true, badge: true, sound: true); // Note: getInitialMessage() is now called in main.dart BEFORE runApp() // This ensures we capture the notification that launched the app if (kDebugMode) { print('[FCM] initialized() - listeners set up. Initial message already captured in main.dart'); } FirebaseMessaging.onMessage.listen((RemoteMessage message) { if (Platform.isAndroid) { if (message.data["notificationType"] != 'NurseConfirmArrive') { // Extract group-id from data payload String? groupId = message.data["group-id"]?.toString(); NotificationManger.showNotification( title: message.notification?.title ?? message.data["title"] ?? "", subtext: message.notification?.body ?? message.data["body"] ?? "", hashcode: DateTime.now().millisecondsSinceEpoch % 100000, // Generate unique ID payload: json.encode(message.data), context: context, groupId: groupId, // Pass group-id for notification grouping ); } } else if (Platform.isIOS) { // For iOS data-only notifications, manually show notification with thread-id if (message.data["notificationType"] != 'NurseConfirmArrive') { String? groupId = message.data["group-id"]?.toString(); // Only show notification manually if it's a data-only notification // iOS system handles notification display for notifications with 'notification' payload if (message.notification == null && message.data.isNotEmpty) { NotificationManger.showNotification( title: message.data["title"]?.toString() ?? "ATOMS", subtext: message.data["body"]?.toString() ?? "", hashcode: message.data.hashCode, payload: json.encode(message.data), context: context, groupId: groupId, // Will be used as threadIdentifier on iOS ); } } } return; }); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { handleMessage(context, message.data); }); // Background message handler is registered in main.dart before runApp() } /// Check if there's a pending notification payload from app launch /// Returns the payload and clears it static Map? getPendingNotificationPayload() { final payload = _pendingNotificationPayload; _pendingNotificationPayload = null; // Clear after retrieving return payload; } /// Set the initial notification payload (called from main.dart) static void setInitialNotification(Map payload) { _pendingNotificationPayload = payload; } /// Check if there's a pending notification without consuming it static bool hasPendingNotification() { return _pendingNotificationPayload != null; } }