Files
sfm_app_final/lib/product/services/notification_services.dart

379 lines
13 KiB
Dart

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<void> initialize() async {
await initializeLocalNotifications();
dev.log("NotificationService initialized");
}
@pragma('vm:entry-point')
Future<void> 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<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) {
firebase_messaging.FirebaseMessaging.onMessage.listen((message) {
_handleForegroundMessage(message);
});
}
void isTokenRefresh() {
_messaging.onTokenRefresh.listen((newToken) {
dev.log("Refresh Firebase Messaging Token: $newToken");
});
}
static Future<bool?> 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<void> _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<void> _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<void> _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<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,
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<void> 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<void> 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');
}
}
}