Update alarm notification when app in foreground, background and terminate

This commit is contained in:
anhtunz
2025-07-11 12:04:20 +07:00
parent cdbd5b7484
commit 4ff2ad4396
13 changed files with 389 additions and 245 deletions

View File

@@ -1,25 +1,29 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.camera"
android:required="false"
tools:targetApi="5" />
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE" />
<!-- <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /> -->
<!-- NOTE: the example app requests USE_EXACT_ALARM to make it easier to run the app.
Developers will need to check if their own app needs to use SCHEDULE_EXACT_ALARM instead -->
<!-- <uses-permission android:name="android.permission.USE_EXACT_ALARM" /> -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- NOTE: Special use was selected as it's the closest match for this example app.
apps should specify the appropriate permission for their use cases. -->
<!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> -->
<!-- START Permissions Package -->
<!-- Permissions options for the `camera` group --> <!-- Permissions options for the `camera` group -->
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<!-- Permissions options for the `location` group --> <!-- Permissions options for the `location` group -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Permissions options for the `alarm` group -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"
tools:ignore="ExactAlarm" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<!-- END Permissions Package --> <!-- END Permissions Package -->
<application <application
android:label="SFM" android:label="SFM"
@@ -63,7 +67,25 @@
<service <service
android:name="com.dexterous.flutterlocalnotifications.ForegroundService" android:name="com.dexterous.flutterlocalnotifications.ForegroundService"
android:exported="false" android:exported="false"
android:stopWithTask="false"/> android:stopWithTask="false"
android:foregroundServiceType="specialUse" />
<service android:name="com.gdelataillade.alarm.services.NotificationOnKillService" />
<service
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver"
android:exported="false"/>
<receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver"
android:enabled="false"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" /> <receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" /> <receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"> <receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -1,5 +1,5 @@
plugins { plugins {
id 'com.google.gms.google-services' version '4.4.2' apply false id 'com.google.gms.google-services' version '4.3.15' apply false
} }
allprojects { allprojects {
repositories { repositories {

View File

@@ -18,8 +18,11 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.3.0" apply false id "com.android.application" version "8.3.2" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false // START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
} }
include ":app" include ":app"

Binary file not shown.

View File

@@ -39,6 +39,7 @@
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>fetch</string> <string>fetch</string>
<string>audio</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
@@ -59,6 +60,10 @@
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.gdelataillade.fetch</string>
</array>
<false/> <false/>
</dict> </dict>
</plist> </plist>

View File

@@ -8,7 +8,9 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:sfm_app/product/shared/shared_snack_bar.dart';
import '../../product/services/notification_services.dart';
import '../../product/utils/permission_handler.dart'; import '../../product/utils/permission_handler.dart';
import '../../product/permission/notification_permission.dart'; import '../../product/permission/notification_permission.dart';
import '../settings/profile/profile_model.dart'; import '../settings/profile/profile_model.dart';
@@ -43,20 +45,10 @@ class MainScreen extends StatefulWidget {
State<MainScreen> createState() => _MainScreenState(); State<MainScreen> createState() => _MainScreenState();
} }
// @pragma('vm:entry-point')
// Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// log("Full background message payload: ${message.toMap()}");
// await Firebase.initializeApp();
// final notificationServices = NotificationServices();
// await notificationServices.initLocalNotifications(controller);
// await notificationServices.showNotification(message);
// log("Background message handled: ${message.data['title']}");
// }
class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver { class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
PersistentTabController controller = PersistentTabController(initialIndex: 0);
APIServices apiServices = APIServices(); APIServices apiServices = APIServices();
// final NotificationServices notificationServices = NotificationServices(); final NotificationServices notificationServices = NotificationServices();
late MainBloc mainBloc; late MainBloc mainBloc;
bool isVN = true; bool isVN = true;
bool isLight = true; bool isLight = true;
@@ -84,7 +76,12 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
mainBloc.sinkIsVNIcon.add(isVN); mainBloc.sinkIsVNIcon.add(isVN);
mainBloc.sinkThemeMode.add(isLight); mainBloc.sinkThemeMode.add(isLight);
checkAndRequestPermission(); checkAndRequestPermission();
NotificationServices.requestNotificationPermission();
NotificationPermission.instance.checkNotificationPermission(context); NotificationPermission.instance.checkNotificationPermission(context);
bool? notificationStatus = await NotificationPermission.instance.requestNotificationPermission();
if(!notificationStatus!){
showNoIconTopSnackBar(context, "Yêu cầu quyền thông báo không thành công", Colors.orange, Colors.white12);
}
} }
@override @override
@@ -103,44 +100,11 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
mainBloc.sendNotificationToken(newToken); mainBloc.sendNotificationToken(newToken);
}); });
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
RemoteNotification? notification = message.notification;
AndroidNotification? android = message.notification?.android;
if (notification != null && android != null) {
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails('your channel id', 'your channel name',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker');
const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);
flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
platformChannelSpecifics,
);
}
});
// notificationServices.initLocalNotifications(controller); // notificationServices.initLocalNotifications(controller);
// notificationServices.firebaseInit(context); // notificationServices.firebaseInit(context);
// NotificationServices().setupInteractMessage(controller); // NotificationServices().setupInteractMessage(controller);
} }
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.inactive) {
log("App Inactive");
} else if (state == AppLifecycleState.resumed) {
log("App Resumed");
} else if (state == AppLifecycleState.paused) {
log("App paused");
} else if (state == AppLifecycleState.detached) {
log("App detached");
}
}
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();

View File

@@ -1,12 +1,12 @@
import 'dart:developer'; import 'dart:developer';
import 'package:alarm/alarm.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart' show PersistentTabController;
import 'firebase_options.dart';
import 'firebase_options.dart';
import 'product/lang/l10n/app_localizations.dart'; import 'product/lang/l10n/app_localizations.dart';
import 'product/services/api_services.dart'; import 'product/services/api_services.dart';
import 'product/services/notification_services.dart'; import 'product/services/notification_services.dart';
@@ -16,88 +16,16 @@ import 'bloc/main_bloc.dart';
import 'product/base/bloc/base_bloc.dart'; import 'product/base/bloc/base_bloc.dart';
import 'product/constant/navigation/navigation_router.dart'; import 'product/constant/navigation/navigation_router.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await setupFlutterNotifications();
showFlutterNotification(message);
print('Handling a background message ${message.messageId}');
}
/// Create a [AndroidNotificationChannel] for heads up notifications PersistentTabController controller = PersistentTabController(initialIndex: 0);
late AndroidNotificationChannel channel;
bool isFlutterLocalNotificationsInitialized = false;
Future<void> setupFlutterNotifications() async {
if (isFlutterLocalNotificationsInitialized) {
return;
}
channel = const AndroidNotificationChannel(
'high_importance_channel', // id
'High Importance Notifications', // title
description:
'This channel is used for important notifications.', // description
importance: Importance.high,
);
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
/// Create an Android Notification Channel.
///
/// We use this channel in the `AndroidManifest.xml` file to override the
/// default FCM channel to enable heads up notifications.
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
/// Update the iOS foreground notification presentation options to allow
/// heads up notifications.
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
isFlutterLocalNotificationsInitialized = true;
}
void showFlutterNotification(RemoteMessage message) {
RemoteNotification? notification = message.notification;
AndroidNotification? android = message.notification?.android;
if (notification != null && android != null && !kIsWeb) {
flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
channel.id,
channel.name,
channelDescription: channel.description,
// TODO add a proper drawable resource to android, for now using
// one that already exists in example app.
icon: 'launch_background',
),
),
);
}
}
/// Initialize the [FlutterLocalNotificationsPlugin] package.
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform, name: "sfm-notification");
// Set the background messaging handler early on, as a named top-level function FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); await Alarm.init();
if (!kIsWeb) {
await setupFlutterNotifications();
}
runApp( runApp(
BlocProvider( BlocProvider(
child: const MyApp(), child: const MyApp(),
@@ -129,7 +57,7 @@ class _MyAppState extends State<MyApp> {
late MainBloc mainBloc; late MainBloc mainBloc;
LanguageServices languageServices = LanguageServices(); LanguageServices languageServices = LanguageServices();
ThemeServices themeServices = ThemeServices(); ThemeServices themeServices = ThemeServices();
// final NotificationServices notificationServices = NotificationServices(); final NotificationServices notificationServices = NotificationServices();
APIServices apiServices = APIServices(); APIServices apiServices = APIServices();
setLocale(Locale locale) { setLocale(Locale locale) {
_locale = locale; _locale = locale;
@@ -145,20 +73,11 @@ class _MyAppState extends State<MyApp> {
void initState() { void initState() {
super.initState(); super.initState();
mainBloc = BlocProvider.of(context); mainBloc = BlocProvider.of(context);
// // notificationServices.initLocalNotifications(); notificationServices.initialize();
// // notificationServices.firebaseInit(context); notificationServices.firebaseInit(context);
// // notificationServices.setupInteractMessage(); notificationServices.setupInteractMessage(controller);
// notificationServices.getDeviceToken().then((token){
// log("Firebase Token: $token");
// sendNotificationToken(token);
// });
} }
// void sendNotificationToken (String token) async {
// int statusCode = await apiServices.sendNotificationToken(token);
// log("Notification Send StatusCode : $statusCode");
// }
@override @override
void didChangeDependencies() { void didChangeDependencies() {
languageServices.getLocale().then((locale) => {setLocale(locale)}); languageServices.getLocale().then((locale) => {setLocale(locale)});

View File

@@ -1,8 +1,10 @@
import 'dart:developer'; import 'dart:developer';
import 'dart:io';
import 'package:app_settings/app_settings.dart'; import 'package:app_settings/app_settings.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../base/widget/dialog/request_permission_dialog.dart'; import '../base/widget/dialog/request_permission_dialog.dart';
@@ -11,7 +13,7 @@ class NotificationPermission {
static NotificationPermission? _instance; static NotificationPermission? _instance;
static NotificationPermission get instance => static NotificationPermission get instance =>
_instance ??= NotificationPermission._init(); _instance ??= NotificationPermission._init();
static final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin();
Future<bool> checkNotificationPermission(context) async { Future<bool> checkNotificationPermission(context) async {
var status = await Permission.notification.status; var status = await Permission.notification.status;
log("Status: $status"); log("Status: $status");
@@ -44,4 +46,21 @@ class NotificationPermission {
Icons.location_on_outlined, "ABCDE", AppSettingsType.notification); Icons.location_on_outlined, "ABCDE", AppSettingsType.notification);
} }
} }
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;
} catch (e) {
log("Error requesting notification permission: $e");
return null;
}
}
} }

View File

@@ -0,0 +1,34 @@
import 'dart:io';
import 'package:alarm/alarm.dart';
class AlarmServices {
Future<void> showAlarm(String title, String body) async {
final DateTime now = DateTime.now();
final AlarmSettings alarmSettings = AlarmSettings(
id: 42,
dateTime: now,
assetAudioPath: 'assets/sounds/warning_alarm.mp3',
loopAudio: true,
vibrate: true,
warningNotificationOnKill: Platform.isIOS,
androidFullScreenIntent: true,
allowAlarmOverlap: true,
volumeSettings: VolumeSettings.fade(
volume: 1.0,
fadeDuration: const Duration(seconds: 3),
volumeEnforced: true,
),
notificationSettings: NotificationSettings(
title: title,
body: body,
stopButton: 'Dừng cảnh báo',
icon: 'ic_launcher',
),
);
await Alarm.set(alarmSettings: alarmSettings);
}
void cancelAlarm({int id = 42}) async {
await Alarm.stop(id);
}
}

View File

@@ -1,15 +1,30 @@
import 'dart:developer' as dev; import 'dart:developer' as dev;
import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:alarm/alarm.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart' hide NotificationSettings;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:sfm_app/product/utils/app_logger_utils.dart';
import '../../firebase_options.dart';
import '../../main.dart';
import 'alarm_services.dart';
class NotificationServices { class NotificationServices {
FirebaseMessaging messaging = FirebaseMessaging.instance; static final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin();
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final FirebaseMessaging _messaging = FirebaseMessaging.instance;
AlarmServices alarmServices = AlarmServices();
Future<void> initLocalNotifications(PersistentTabController controller) async { Future<void> initialize() async {
await initializeLocalNotifications();
dev.log("NotificationService initialized");
}
Future<void> initializeLocalNotifications() async {
try{
const AndroidInitializationSettings androidInitializationSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); const AndroidInitializationSettings androidInitializationSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings iosInitializationSettings = DarwinInitializationSettings(); const DarwinInitializationSettings iosInitializationSettings = DarwinInitializationSettings();
const InitializationSettings initializationSettings = InitializationSettings( const InitializationSettings initializationSettings = InitializationSettings(
@@ -17,7 +32,7 @@ class NotificationServices {
iOS: iosInitializationSettings, iOS: iosInitializationSettings,
); );
await _flutterLocalNotificationsPlugin.initialize( await _notificationsPlugin.initialize(
initializationSettings, initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) { onDidReceiveNotificationResponse: (NotificationResponse response) {
dev.log("Người dùng click thông báo ở foreground với payload: ${response.payload}"); dev.log("Người dùng click thông báo ở foreground với payload: ${response.payload}");
@@ -25,93 +40,123 @@ class NotificationServices {
}, },
); );
dev.log("Local notifications initialized"); dev.log("Local notifications initialized");
}catch(e){
dev.log("Error initializing local notifications: $e");
}
} }
void firebaseInit(BuildContext context) { void firebaseInit(BuildContext context) {
FirebaseMessaging.onMessage.listen((message) { FirebaseMessaging.onMessage.listen((message) {
dev.log("Foreground message payload: ${message.toMap()}"); dev.log("Foreground message payload: ${message.toMap()}");
showNotification(message); _showNotification(message);
}); });
} }
Future<String> getDeviceToken() async {
print("GET FB TOKEN");
String? token = await messaging.getToken();
print("GET FB: ${token}");
return token!;
}
void isTokenRefresh() { void isTokenRefresh() {
messaging.onTokenRefresh.listen((newToken) { _messaging.onTokenRefresh.listen((newToken) {
dev.log("Refresh Firebase Messaging Token: $newToken"); dev.log("Refresh Firebase Messaging Token: $newToken");
}); });
} }
Future<void> showNotification(RemoteMessage message) async { static Future<bool?> requestNotificationPermission() async {
dev.log(message.toString()); try {
dev.log(message.data.toString()); if (Platform.isAndroid) {
dev.log(message.data["notification"].toString()); return await _notificationsPlugin
String? title = message.data['title']; .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
String? body = message.data['body']; ?.requestNotificationsPermission();
String type = message.data['type'] ?? "normal"; } else if (Platform.isIOS) {
return await _notificationsPlugin
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(alert: true, sound: true, badge: true);
}
return null;
} catch (e) {
dev.log("Error requesting notification permission: $e");
return null;
}
}
Future<void> _showNotification(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';
if (title == null || body == null) { if (title == null || body == null) {
dev.log("Skipping notification due to missing title or body"); dev.log('Skipping notification: missing title or body', name: 'Notification');
return; return;
} }
AndroidNotificationChannel androidNotificationChannel = AndroidNotificationChannel( // Handle smoke warning notifications
math.Random.secure().nextInt(1000000).toString(), if (type == 'smoke_warning') {
'High Importance Notification', await alarmServices.showAlarm(title, body);
dev.log('Displayed smoke warning notification', name: 'Notification');
return;
}
// Create notification channel
final channelId = math.Random.secure().nextInt(1000000).toString();
const channelName = 'High Importance Notification';
const channelDescription = 'Channel description';
final androidChannel = AndroidNotificationChannel(
channelId,
channelName,
importance: Importance.max, importance: Importance.max,
description: channelDescription,
); );
final androidPlugin = _flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>(); final androidPlugin = _notificationsPlugin
await androidPlugin?.deleteNotificationChannel(androidNotificationChannel.id); .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( // Delete existing channel to prevent conflicts
androidNotificationChannel.id, await androidPlugin?.deleteNotificationChannel(channelId);
androidNotificationChannel.name,
channelDescription: "Channel description", // Configure notification details
final androidDetails = AndroidNotificationDetails(
channelId,
channelName,
channelDescription: channelDescription,
sound: getSound(type), sound: getSound(type),
importance: androidNotificationChannel.importance, importance: Importance.max,
priority: Priority.high, priority: Priority.high,
ticker: 'ticker', ticker: 'ticker',
actions: type == "smoke_warning"
? [
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 iosDetails = DarwinNotificationDetails(
presentAlert: true, presentAlert: true,
presentBadge: true, presentBadge: true,
presentBanner: true, presentBanner: true,
presentSound: true, presentSound: true,
); );
NotificationDetails notificationDetails = NotificationDetails( final notificationDetails = NotificationDetails(
android: androidNotificationDetails, android: androidDetails,
iOS: darwinNotificationDetails, iOS: iosDetails,
); );
// Truyền payload vào thông báo // Show notification
String payload = message.data['type'] ?? "default"; await _notificationsPlugin.show(
await _flutterLocalNotificationsPlugin.show( int.parse(channelId),
math.Random.secure().nextInt(1000000),
title, title,
body, body,
notificationDetails, notificationDetails,
payload: payload, payload: type,
); );
dev.log("Displayed notification with title: $title, body: $body, type: $type");
dev.log(
'Displayed notification - title: $title, body: $body, type: $type',
name: 'Notification',
);
} catch (e, stackTrace) {
dev.log(
'Failed to show notification: $e',
name: 'Notification',
error: e,
stackTrace: stackTrace,
);
}
} }
AndroidNotificationSound getSound(String type) { AndroidNotificationSound getSound(String type) {
@@ -155,4 +200,120 @@ class NotificationServices {
} }
}); });
} }
Future<void> showBackgroundOrTerminateNotification(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';
if (title == null || body == null) {
dev.log('Skipping notification: missing title or body', name: 'Notification');
return;
}
// Create notification channel
final channelId = math.Random.secure().nextInt(1000000).toString();
const channelName = 'High Importance Notification';
const channelDescription = 'Channel description';
// Configure notification details
final androidDetails = AndroidNotificationDetails(
channelId,
channelName,
channelDescription: channelDescription,
sound: getSound(type),
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentBanner: true,
presentSound: true,
);
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
// 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',
);
} catch (e, stackTrace) {
dev.log(
'Failed to show notification: $e',
name: 'Notification',
error: e,
stackTrace: 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");
}
} }

View File

@@ -9,6 +9,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.54" version: "1.3.54"
alarm:
dependency: "direct main"
description:
name: alarm
sha256: "1eef91f0b803a2370137e0dada9c7c24cc31edf4f1c30b06442dcf486cc192e0"
url: "https://pub.dev"
source: hosted
version: "5.1.4"
app_settings: app_settings:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -294,6 +302,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.8" version: "3.0.8"
flutter_fgbg:
dependency: transitive
description:
name: flutter_fgbg
sha256: eb6da9b2047372566a6e17b505975fe5bace94af01f6fc825c4b6f81baa6c447
url: "https://pub.dev"
source: hosted
version: "0.7.1"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:

View File

@@ -22,7 +22,7 @@ dependencies:
permission_handler: ^11.0.1 permission_handler: ^11.0.1
# flex_color_scheme: ^7.2.0 # flex_color_scheme: ^7.2.0
flex_color_scheme: ^8.2.0 flex_color_scheme: ^8.2.0
go_router: ^15.2.3 go_router: ^15.1.2
http: ^1.3.0 http: ^1.3.0
top_snackbar_flutter: ^3.1.0 top_snackbar_flutter: ^3.1.0
badges: ^3.1.2 badges: ^3.1.2
@@ -48,6 +48,7 @@ dependencies:
app_settings: ^5.1.1 app_settings: ^5.1.1
lottie: ^3.3.1 lottie: ^3.3.1
logger: ^2.5.0 logger: ^2.5.0
alarm: ^5.1.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: