diff --git a/android/app/src/main/res/raw/new_alarm.mp3 b/android/app/src/main/res/raw/new_alarm.mp3 new file mode 100644 index 0000000..b63a917 Binary files /dev/null and b/android/app/src/main/res/raw/new_alarm.mp3 differ diff --git a/android/app/src/main/res/raw/normal.mp3 b/android/app/src/main/res/raw/normal.mp3 new file mode 100644 index 0000000..f5b6dc5 Binary files /dev/null and b/android/app/src/main/res/raw/normal.mp3 differ diff --git a/android/app/src/main/res/raw/success_alert.mp3 b/android/app/src/main/res/raw/success_alert.mp3 new file mode 100644 index 0000000..ca42707 Binary files /dev/null and b/android/app/src/main/res/raw/success_alert.mp3 differ diff --git a/android/app/src/main/res/raw/welcome.mp3 b/android/app/src/main/res/raw/welcome.mp3 new file mode 100644 index 0000000..a1beb64 Binary files /dev/null and b/android/app/src/main/res/raw/welcome.mp3 differ diff --git a/assets/sounds/new_alarm.wav b/assets/sounds/new_alarm.wav new file mode 100644 index 0000000..e1d7f42 Binary files /dev/null and b/assets/sounds/new_alarm.wav differ diff --git a/assets/sounds/success_alert.wav b/assets/sounds/success_alert.wav new file mode 100644 index 0000000..c8327e6 Binary files /dev/null and b/assets/sounds/success_alert.wav differ diff --git a/lib/feature/auth/login/screen/login_screen.dart b/lib/feature/auth/login/screen/login_screen.dart index ebce4bc..3d2b137 100644 --- a/lib/feature/auth/login/screen/login_screen.dart +++ b/lib/feature/auth/login/screen/login_screen.dart @@ -192,6 +192,8 @@ class _LoginScreenState extends State { LocaleManager.instance .setString(PreferencesKeys.ROLE, loginModel.role!); context.goNamed(AppRoutes.HOME.name); + // context.goNamed("notification"); + } else { showErrorTopSnackBarCustom( context, appLocalization(context).login_incorrect_usernameOrPass); @@ -214,6 +216,7 @@ class _LoginScreenState extends State { int timeNow = DateTime.now().millisecondsSinceEpoch ~/ 1000; if (token != "" && (exp - timeNow) > 7200) { context.goNamed(AppRoutes.HOME.name); + // context.goNamed("notification"); } } diff --git a/lib/feature/sound_notification_test/notification_screen.dart b/lib/feature/sound_notification_test/notification_screen.dart index 0e29cf3..7f26492 100644 --- a/lib/feature/sound_notification_test/notification_screen.dart +++ b/lib/feature/sound_notification_test/notification_screen.dart @@ -1,6 +1,10 @@ -import 'dart:developer'; +// ignore_for_file: avoid_print + +import 'dart:math' as math; +import 'dart:developer' as dev; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'notification_bloc.dart'; import '../../product/base/bloc/base_bloc.dart'; import '../../product/services/notification_services.dart'; @@ -14,11 +18,12 @@ class NotificationScreen extends StatefulWidget { class _NotificationScreenState extends State { late NotificationBloc notificationBloc; - NotificationServices notificationServices = NotificationServices(); + final notificationPlugin = FlutterLocalNotificationsPlugin(); @override void initState() { super.initState(); + initNotification(); notificationBloc = BlocProvider.of(context); } @@ -30,11 +35,43 @@ class _NotificationScreenState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ TextButton( - onPressed: () { - log("Token: ${notificationServices.getDeviceToken()}"); - }, - child: Text("Get Notification Token")) + onPressed: () async { + showNewAlarmSoundNotification("warning_alarm"); + dev.log("Da vao day"); + }, + child: const Text("Show new Alarm Notification"), + ), ], )); } + + Future initNotification() async{ + const isSettingAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); + const initSetting = InitializationSettings(android: isSettingAndroid); + await notificationPlugin.initialize(initSetting); + + } + + Future showNewAlarmSoundNotification(String sound) async { + AndroidNotificationChannel androidNotificationChannel = + AndroidNotificationChannel( + math.Random.secure().nextInt(1000000).toString(), + 'high Important Notification', + importance: Importance.max); + AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails( + "androidNotificationChannel.id.toString()", + androidNotificationChannel.name.toString(), + sound: RawResourceAndroidNotificationSound(sound), + channelDescription: "Channel description", + importance: androidNotificationChannel.importance, + priority: Priority.high, + ticker: 'ticker', + ); + + final NotificationDetails notificationDetails = + NotificationDetails(android: androidNotificationDetails); + await FlutterLocalNotificationsPlugin().show(0, 'New Notification', + 'Sound Notification Example', notificationDetails); + } } diff --git a/lib/main.dart b/lib/main.dart index 95c2b3a..2f0ef38 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,10 @@ +import 'dart:developer'; + import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'product/services/notification_services.dart'; import 'product/services/theme_services.dart'; import 'product/services/language_services.dart'; import 'bloc/main_bloc.dart'; @@ -10,6 +14,10 @@ import 'product/constant/navigation/navigation_router.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); + FirebaseMessaging + .onBackgroundMessage(_firebaseMessagingBackgroundHandler); + NotificationServices().setupInteractMessage(); + runApp( BlocProvider( child: const MyApp(), @@ -17,6 +25,16 @@ void main() async { ), ); } + +@pragma('vm:entry-point') +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + log("Full background message payload: ${message.toMap()}"); + await Firebase.initializeApp(); + final notificationServices = NotificationServices(); + await notificationServices.initLocalNotifications(); + await notificationServices.showNotification(message); + log("Background message handled: ${message.data['title']}"); +} class MyApp extends StatefulWidget { const MyApp({super.key}); @@ -39,6 +57,7 @@ class _MyAppState extends State { late MainBloc mainBloc; LanguageServices languageServices = LanguageServices(); ThemeServices themeServices = ThemeServices(); + final NotificationServices notificationServices = NotificationServices(); setLocale(Locale locale) { _locale = locale; @@ -49,11 +68,16 @@ class _MyAppState extends State { _themeData = theme; mainBloc.sinkTheme.add(_themeData); } - @override void initState() { super.initState(); mainBloc = BlocProvider.of(context); + notificationServices.initLocalNotifications(); + notificationServices.firebaseInit(context); + // notificationServices.setupInteractMessage(); + notificationServices.getDeviceToken().then((token){ + print("Firebase Token: $token"); + }); } @override diff --git a/lib/product/constant/navigation/navigation_router.dart b/lib/product/constant/navigation/navigation_router.dart index d4f6333..e9154e7 100644 --- a/lib/product/constant/navigation/navigation_router.dart +++ b/lib/product/constant/navigation/navigation_router.dart @@ -1,6 +1,6 @@ import 'package:go_router/go_router.dart'; -import 'package:sfm_app/feature/sound_notification_test/notification_bloc.dart'; -import 'package:sfm_app/feature/sound_notification_test/notification_screen.dart'; +import '../../../feature/sound_notification_test/notification_bloc.dart'; +import '../../../feature/sound_notification_test/notification_screen.dart'; import '../../../bloc/device_detail_bloc.dart'; import '../../../feature/devices/device_detail/device_detail_screen.dart'; import '../../../bloc/device_notification_settings_bloc.dart'; diff --git a/lib/product/services/notification_services.dart b/lib/product/services/notification_services.dart index 0be0f6d..822bdbc 100644 --- a/lib/product/services/notification_services.dart +++ b/lib/product/services/notification_services.dart @@ -1,5 +1,3 @@ -// ignore_for_file: use_build_context_synchronously - import 'dart:developer' as dev; import 'dart:math' as math; import 'package:firebase_messaging/firebase_messaging.dart'; @@ -8,15 +6,34 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; class NotificationServices { FirebaseMessaging messaging = FirebaseMessaging.instance; - final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); + final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - void firebaseInit(BuildContext context) async { + Future initLocalNotifications() async { + const AndroidInitializationSettings androidInitializationSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); + const DarwinInitializationSettings iosInitializationSettings = DarwinInitializationSettings(); + const InitializationSettings initializationSettings = InitializationSettings( + android: androidInitializationSettings, + iOS: iosInitializationSettings, + ); + + await _flutterLocalNotificationsPlugin.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); + }, + ); + dev.log("Local notifications initialized"); + } + + void firebaseInit(BuildContext context) { FirebaseMessaging.onMessage.listen((message) { - initNotifications(context, message); - showNotification(message); - dev.log( - "Title: ${message.notification!.title}, Body: ${message.notification!.body} from ${message.sentTime}"); + dev.log("Foreground message payload: ${message.toMap()}"); + if (WidgetsBinding.instance != null) { + showNotification(message); + } else { + dev.log("App is in background, skipping foreground notification"); + } }); } @@ -25,80 +42,116 @@ class NotificationServices { return token!; } - void isTokenRefresh() async { + void isTokenRefresh() { messaging.onTokenRefresh.listen((newToken) { dev.log("Refresh Firebase Messaging Token: $newToken"); }); } - void initNotifications(BuildContext context, RemoteMessage message) async { - var androidInitializationSettings = - const AndroidInitializationSettings('app_icon'); - var iosInitializationSettings = const DarwinInitializationSettings(); - var initializationSettings = InitializationSettings( - android: androidInitializationSettings, iOS: iosInitializationSettings); - await _flutterLocalNotificationsPlugin.initialize(initializationSettings, - onDidReceiveNotificationResponse: (payload) { - handleMessage(context, message); - }); - } - Future showNotification(RemoteMessage message) async { - AndroidNotificationChannel androidNotificationChannel = - AndroidNotificationChannel( - math.Random.secure().nextInt(1000000).toString(), - 'high Important Notification', - importance: Importance.max); - AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - androidNotificationChannel.id.toString(), - androidNotificationChannel.name.toString(), - sound: RawResourceAndroidNotificationSound(message.data['sound']), + String? title = message.data['title']; + String? body = message.data['body']; + String type = message.data['type'] ?? "normal"; + + if (title == null || body == null) { + dev.log("Skipping notification due to missing title or body"); + return; + } + + AndroidNotificationChannel androidNotificationChannel = AndroidNotificationChannel( + math.Random.secure().nextInt(1000000).toString(), + 'High Importance Notification', + importance: Importance.max, + ); + + final androidPlugin = _flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation(); + await androidPlugin?.deleteNotificationChannel(androidNotificationChannel.id); + + AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( + androidNotificationChannel.id, + androidNotificationChannel.name, channelDescription: "Channel description", + sound: getSound(type), importance: androidNotificationChannel.importance, priority: Priority.high, ticker: 'ticker', + actions: type == "warn1" + ? [ + const AndroidNotificationAction( + "id1", + "Hogg xóa được", + // true thì khi nhấn vào button sẽ mở giao diện ra + showsUserInterface: true, + cancelNotification: false, + ) + ] + : null ); - const DarwinNotificationDetails darwinNotificationDetails = - DarwinNotificationDetails( + const DarwinNotificationDetails darwinNotificationDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentBanner: true, presentSound: true, ); + NotificationDetails notificationDetails = NotificationDetails( - android: androidNotificationDetails, iOS: darwinNotificationDetails); - Future.delayed(Duration.zero, () { - _flutterLocalNotificationsPlugin.show(0, message.notification!.title!, - message.notification!.body, notificationDetails); - }); + android: androidNotificationDetails, + iOS: darwinNotificationDetails, + ); + + // Truyền payload vào thông báo + String payload = message.data['type'] ?? "default"; + await _flutterLocalNotificationsPlugin.show( + math.Random.secure().nextInt(1000000), + title, + body, + notificationDetails, + payload: payload, + ); + dev.log("Displayed notification with title: $title, body: $body, type: $type"); } - void handleMessage(BuildContext context, RemoteMessage message) async { - if (message.data['type'] == "msj") { - // Navigator.push(context, - // MaterialPageRoute(builder: (context) => const MessageScreen())); - } else if (message.data['type'] == "warn") { - // Navigator.push( - // context, MaterialPageRoute(builder: (context) => const MapScreen())); + AndroidNotificationSound getSound(String type) { + if (type == "welcome") { + return const RawResourceAndroidNotificationSound("welcome"); + } else if (type == "success") { + return const RawResourceAndroidNotificationSound("success_alert"); + } else if (type == "warn1") { + return const RawResourceAndroidNotificationSound("warning_alarm"); + } else if (type == "warn2") { + return const RawResourceAndroidNotificationSound("new_alarm"); } else { - dev.log("Not found data"); + return const RawResourceAndroidNotificationSound("normal"); } } - Future setupInteractMessage(BuildContext context) async { - // When app terminate - RemoteMessage? initialMessage = - await FirebaseMessaging.instance.getInitialMessage(); - if (initialMessage != null) { - // showNotification(initialMessage) - handleMessage(context, initialMessage); - } + void handleMessage(String? payload) { + dev.log("Handling notification tap with payload: $payload"); + // Thêm logic xử lý khi nhấn thông báo ở đây + // Ví dụ: Điều hướng màn hình hoặc xử lý dữ liệu + } - // When app is inBackGround + Future setupInteractMessage() async { + // Khi app terminated + RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); + + if (initialMessage != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + try { + handleMessage(initialMessage.data['type']); + } catch (e, stack) { + dev.log("Error handling initial message: $e\n$stack"); + } + }); + } + // Khi app ở background FirebaseMessaging.onMessageOpenedApp.listen((message) { - handleMessage(context, message); + try { + handleMessage(message.data['type']); + } catch (e, stack) { + dev.log("Error in onMessageOpenedApp: $e\n$stack"); + } }); } -} +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 4ddf658..992c87f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -92,6 +92,7 @@ flutter: - assets/images/ - assets/icons/ - assets/map_themes/ + - assets/sounds/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware