chore(notifications): fix conflig when show notification in android
This commit is contained in:
@@ -4,20 +4,21 @@ import 'dart:math' as math;
|
||||
import 'package:alarm/alarm.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart'
|
||||
hide NotificationSettings;
|
||||
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 '../../main.dart';
|
||||
import 'alarm_services.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
class NotificationServices {
|
||||
static final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
|
||||
final firebase_messaging.FirebaseMessaging _messaging =
|
||||
firebase_messaging.FirebaseMessaging.instance;
|
||||
AlarmServices alarmServices = AlarmServices();
|
||||
|
||||
Future<void> initialize() async {
|
||||
@@ -25,36 +26,51 @@ class NotificationServices {
|
||||
dev.log("NotificationService initialized");
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> initializeLocalNotifications() async {
|
||||
try {
|
||||
const AndroidInitializationSettings androidInitializationSettings =
|
||||
const androidInitSettings =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const DarwinInitializationSettings iosInitializationSettings =
|
||||
DarwinInitializationSettings();
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: androidInitializationSettings,
|
||||
iOS: iosInitializationSettings,
|
||||
final darwinInitSettings = DarwinInitializationSettings(
|
||||
onDidReceiveLocalNotification: _onDidReceiveLocalNotification,
|
||||
);
|
||||
final initSettings = InitializationSettings(
|
||||
android: androidInitSettings,
|
||||
iOS: darwinInitSettings,
|
||||
);
|
||||
|
||||
await _notificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
||||
dev.log(
|
||||
"Người dùng click thông báo ở foreground với payload: ${response.payload}");
|
||||
handleMessage(response.payload, controller);
|
||||
},
|
||||
initSettings,
|
||||
onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse,
|
||||
);
|
||||
|
||||
dev.log("Local notifications initialized");
|
||||
} catch (e) {
|
||||
dev.log("Error initializing local notifications: $e");
|
||||
AppLoggerUtils.error("Failed to initialize local notifications", e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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) {
|
||||
FirebaseMessaging.onMessage.listen((message) {
|
||||
dev.log("Foreground message payload: ${message.toMap()}");
|
||||
_showNotification(message);
|
||||
firebase_messaging.FirebaseMessaging.onMessage.listen((message) {
|
||||
_handleForegroundMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,75 +82,156 @@ class NotificationServices {
|
||||
|
||||
static Future<bool?> requestNotificationPermission() async {
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
return await _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestNotificationsPermission();
|
||||
} else if (Platform.isIOS) {
|
||||
return await _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(alert: true, sound: true, badge: true);
|
||||
}
|
||||
return null;
|
||||
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) {
|
||||
dev.log("Error requesting notification permission: $e");
|
||||
AppLoggerUtils.error("Failed to request notification permission", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showNotification(RemoteMessage message) async {
|
||||
/// Xử lý message khi app foreground
|
||||
Future<void> _handleForegroundMessage(
|
||||
firebase_messaging.RemoteMessage message) async {
|
||||
try {
|
||||
// Early validation of notification data
|
||||
final title = message.notification?.title ?? "Title" as String?;
|
||||
final body = message.notification?.body ?? "Body" as String?;
|
||||
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'] ?? '';
|
||||
|
||||
if (title == null || body == null) {
|
||||
dev.log('Skipping notification: missing title or body',
|
||||
name: 'Notification');
|
||||
return;
|
||||
}
|
||||
dev.log('Foreground message: type=$type, title=$title, body=$body');
|
||||
|
||||
// Handle smoke warning notifications
|
||||
if (type == 'smoke_warning') {
|
||||
await alarmServices.showAlarm(title, body);
|
||||
dev.log('Displayed smoke warning notification', name: 'Notification');
|
||||
return;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Create notification channel
|
||||
final channelId = math.Random.secure().nextInt(1000000).toString();
|
||||
const channelName = 'High Importance Notification';
|
||||
const channelDescription = 'Channel description';
|
||||
/// Hiển thị alarm (smoke warning)
|
||||
Future<void> _showAlarm(String title, String body) async {
|
||||
try {
|
||||
await Alarm.init();
|
||||
|
||||
// final androidChannel = AndroidNotificationChannel(
|
||||
// channelId,
|
||||
// channelName,
|
||||
// importance: Importance.max,
|
||||
// description: channelDescription,
|
||||
// );
|
||||
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",
|
||||
),
|
||||
);
|
||||
|
||||
final androidPlugin =
|
||||
_notificationsPlugin.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
await Alarm.set(alarmSettings: alarmSettings);
|
||||
dev.log('Alarm set successfully: $title');
|
||||
} catch (e, stackTrace) {
|
||||
AppLoggerUtils.error('Failed to set alarm', e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete existing channel to prevent conflicts
|
||||
await androidPlugin?.deleteNotificationChannel(channelId);
|
||||
|
||||
// Configure notification details
|
||||
/// Hiển thị notification local khi app foreground
|
||||
Future<void> _showForegroundNotification(
|
||||
String title, String body, String type) async {
|
||||
try {
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
channelId,
|
||||
channelName,
|
||||
channelDescription: channelDescription,
|
||||
'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',
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
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<void> 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,
|
||||
@@ -146,7 +243,6 @@ class NotificationServices {
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
// Show notification
|
||||
await _notificationsPlugin.show(
|
||||
int.parse(channelId),
|
||||
title,
|
||||
@@ -156,16 +252,10 @@ class NotificationServices {
|
||||
);
|
||||
|
||||
dev.log(
|
||||
'Displayed notification - title: $title, body: $body, type: $type',
|
||||
name: 'Notification',
|
||||
);
|
||||
'Background/terminate notification shown: title=$title, type=$type');
|
||||
} catch (e, stackTrace) {
|
||||
dev.log(
|
||||
'Failed to show notification: $e',
|
||||
name: 'Notification',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
AppLoggerUtils.error(
|
||||
'Failed to show background/terminate notification', e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,153 +269,110 @@ class NotificationServices {
|
||||
} else if (type == "battery_warning") {
|
||||
return const RawResourceAndroidNotificationSound("new_alarm");
|
||||
} else {
|
||||
return const RawResourceAndroidNotificationSound("normal");
|
||||
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) {
|
||||
dev.log("Handling notification tap with payload: $payload");
|
||||
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<void> setupInteractMessage(PersistentTabController controller) async {
|
||||
// Khi app terminated
|
||||
RemoteMessage? initialMessage =
|
||||
await FirebaseMessaging.instance.getInitialMessage();
|
||||
// Khi app terminated, được mở bằng notification
|
||||
firebase_messaging.RemoteMessage? initialMessage =
|
||||
await firebase_messaging.FirebaseMessaging.instance.getInitialMessage();
|
||||
|
||||
if (initialMessage != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
try {
|
||||
handleMessage(initialMessage.data['type'], controller);
|
||||
} catch (e, stack) {
|
||||
dev.log("Error handling initial message: $e\n$stack");
|
||||
}
|
||||
final type = initialMessage.data['type'] as String?;
|
||||
handleMessage(type, controller);
|
||||
});
|
||||
}
|
||||
// Khi app ở background
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((message) {
|
||||
|
||||
// Khi app background, user tap notification
|
||||
firebase_messaging.FirebaseMessaging.onMessageOpenedApp.listen((message) {
|
||||
try {
|
||||
handleMessage(message.data['type'], controller);
|
||||
} catch (e, stack) {
|
||||
dev.log("Error in onMessageOpenedApp: $e\n$stack");
|
||||
final type = message.data['type'] as String?;
|
||||
handleMessage(type, controller);
|
||||
} catch (e, stackTrace) {
|
||||
AppLoggerUtils.error('Error in onMessageOpenedApp', e, stackTrace);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> showBackgroundOrTerminateNotification(
|
||||
RemoteMessage message) async {
|
||||
@pragma('vm:entry-point')
|
||||
static Future<void> firebaseMessagingBackgroundHandler(
|
||||
firebase_messaging.RemoteMessage message) async {
|
||||
try {
|
||||
// Early validation of notification data
|
||||
final title = message.data['title'] as String?;
|
||||
final body = message.data['body'] as String?;
|
||||
final type = message.data['type'] as String? ?? 'normal';
|
||||
// Log to device storage for debugging background isolate
|
||||
dev.log('═══ BACKGROUND HANDLER STARTED ═══');
|
||||
dev.log('Message data: ${message.data}');
|
||||
dev.log('Notification: ${message.notification}');
|
||||
|
||||
if (title == null || body == null) {
|
||||
dev.log('Skipping notification: missing title or body',
|
||||
name: 'Notification');
|
||||
return;
|
||||
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');
|
||||
}
|
||||
|
||||
// Create notification channel
|
||||
final channelId = math.Random.secure().nextInt(1000000).toString();
|
||||
const channelName = 'High Importance Notification';
|
||||
const channelDescription = 'Channel description';
|
||||
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';
|
||||
|
||||
// Configure notification details
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
channelId,
|
||||
channelName,
|
||||
channelDescription: channelDescription,
|
||||
sound: getSound(type),
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
ticker: 'ticker',
|
||||
);
|
||||
dev.log('Message type: $type');
|
||||
dev.log('Title: $title');
|
||||
dev.log('Body: $body');
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentBanner: true,
|
||||
presentSound: true,
|
||||
);
|
||||
// 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');
|
||||
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
// Show notification
|
||||
await _notificationsPlugin.show(
|
||||
int.parse(channelId),
|
||||
title,
|
||||
body,
|
||||
notificationDetails,
|
||||
payload: type,
|
||||
);
|
||||
|
||||
dev.log(
|
||||
'Displayed notification - title: $title, body: $body, type: $type',
|
||||
name: 'Notification',
|
||||
);
|
||||
dev.log('═══ BACKGROUND HANDLER COMPLETED ═══');
|
||||
} catch (e, stackTrace) {
|
||||
dev.log(
|
||||
'Failed to show notification: $e',
|
||||
name: 'Notification',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
dev.log('✗ BACKGROUND HANDLER FAILED');
|
||||
dev.log('Error: $e');
|
||||
dev.log('Stack trace: $stackTrace');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
try {
|
||||
AppLoggerUtils.warning("Background handler started: ${message.data}");
|
||||
await Alarm.init();
|
||||
if (Firebase.apps.isEmpty) {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
name: "sfm-notification");
|
||||
AppLoggerUtils.warning(
|
||||
"Firebase đã được khởi tạo trong background handler");
|
||||
} else {
|
||||
AppLoggerUtils.warning("Firebase đã được khởi tạo trước đó");
|
||||
}
|
||||
AppLoggerUtils.warning(message.toString());
|
||||
AppLoggerUtils.warning(message.data.toString());
|
||||
AppLoggerUtils.warning(message.data["notification"].toString());
|
||||
String? title = message.data['title'];
|
||||
String? body = message.data['body'];
|
||||
String type = message.data['type'] ?? "normal";
|
||||
final notificationService = NotificationServices();
|
||||
await notificationService.initialize();
|
||||
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 ?? "SFM",
|
||||
body: body ?? "",
|
||||
stopButton: 'Dừng thông báo',
|
||||
icon: "ic_launcher",
|
||||
),
|
||||
);
|
||||
if (type == "smoke_warning") {
|
||||
await Alarm.set(alarmSettings: alarmSettings);
|
||||
} else {
|
||||
await notificationService.showBackgroundOrTerminateNotification(message);
|
||||
}
|
||||
} catch (e) {
|
||||
AppLoggerUtils.warning("Error in background handler: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user