379 lines
13 KiB
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');
|
|
}
|
|
}
|
|
}
|