chore(notifications): fix conflig when show notification in android

This commit is contained in:
Tran Anh Tuan
2025-10-23 16:57:10 +07:00
parent 9bff11a0b1
commit 2afc71e24c
16 changed files with 567 additions and 422 deletions

View File

@@ -3,6 +3,8 @@ PODS:
- Flutter - Flutter
- app_settings (5.1.1): - app_settings (5.1.1):
- Flutter - Flutter
- connectivity_plus (0.0.1):
- Flutter
- Firebase/CoreOnly (11.10.0): - Firebase/CoreOnly (11.10.0):
- FirebaseCore (~> 11.10.0) - FirebaseCore (~> 11.10.0)
- Firebase/Messaging (11.10.0): - Firebase/Messaging (11.10.0):
@@ -102,6 +104,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- alarm (from `.symlinks/plugins/alarm/ios`) - alarm (from `.symlinks/plugins/alarm/ios`)
- app_settings (from `.symlinks/plugins/app_settings/ios`) - app_settings (from `.symlinks/plugins/app_settings/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
@@ -134,6 +137,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/alarm/ios" :path: ".symlinks/plugins/alarm/ios"
app_settings: app_settings:
:path: ".symlinks/plugins/app_settings/ios" :path: ".symlinks/plugins/app_settings/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
firebase_core: firebase_core:
:path: ".symlinks/plugins/firebase_core/ios" :path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging: firebase_messaging:
@@ -162,6 +167,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
alarm: 9ff6d2dae9bd69c4022622f7e0a1e9c1dd70064e alarm: 9ff6d2dae9bd69c4022622f7e0a1e9c1dd70064e
app_settings: 5127ae0678de1dcc19f2293271c51d37c89428b2 app_settings: 5127ae0678de1dcc19f2293271c51d37c89428b2
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
firebase_core: 2d4534e7b489907dcede540c835b48981d890943 firebase_core: 2d4534e7b489907dcede540c835b48981d890943
firebase_messaging: 75bc93a4df25faccad67f6662ae872ac9ae69b64 firebase_messaging: 75bc93a4df25faccad67f6662ae872ac9ae69b64

View File

@@ -17,6 +17,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A241E2BF2EAA2F1C00664284 /* warning_alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = A241E2BE2EAA2F1C00664284 /* warning_alarm.caf */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -64,6 +65,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A241E2BE2EAA2F1C00664284 /* warning_alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = warning_alarm.caf; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -109,6 +111,7 @@
97C146E51CF9000F007C117D = { 97C146E51CF9000F007C117D = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A241E2BE2EAA2F1C00664284 /* warning_alarm.caf */,
9740EEB11CF90186004384FC /* Flutter */, 9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
@@ -256,6 +259,7 @@
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
5EFD34A62DA7D89800351DB2 /* GoogleService-Info.plist in Resources */, 5EFD34A62DA7D89800351DB2 /* GoogleService-Info.plist in Resources */,
A241E2BF2EAA2F1C00664284 /* warning_alarm.caf in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
); );

View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Flutter View Controller--> <!--Flutter View Controller-->
@@ -14,13 +16,14 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/> <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides> </layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC"> <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/> <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view> </view>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="121" y="-34"/>
</scene> </scene>
</scenes> </scenes>
</document> </document>

Binary file not shown.

BIN
ios/warning_alarm.caf Normal file

Binary file not shown.

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:alarm/alarm.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../product/shared/shared_loading_animation.dart'; import '../../product/shared/shared_loading_animation.dart';
@@ -27,7 +28,6 @@ class _HomeScreenState extends State<HomeScreen> {
bool isFunctionCall = false; bool isFunctionCall = false;
Timer? getAllDevicesTimer; Timer? getAllDevicesTimer;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -35,14 +35,16 @@ class _HomeScreenState extends State<HomeScreen> {
const duration = Duration(seconds: 10); const duration = Duration(seconds: 10);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// Code ở đây chạy sau khi giao diện render xong // Code ở đây chạy sau khi giao diện render xong
getAllDevicesTimer = getAllDevicesTimer = Timer.periodic(
Timer.periodic(duration, (Timer t) => homeBloc.getOwnerAndJoinedDevices(context)); duration, (Timer t) => homeBloc.getOwnerAndJoinedDevices(context));
// Ví dụ: gọi API, scroll tới vị trí nào đó, v.v. // Ví dụ: gọi API, scroll tới vị trí nào đó, v.v.
}); });
} }
Future<void> loadAlarms() async {
final alarms = await Alarm.getAlarms();
log("Alarms: $alarms");
}
@override @override
void dispose() { void dispose() {
@@ -54,196 +56,229 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<List<DeviceWithAlias>?>( return StreamBuilder<List<DeviceWithAlias>?>(
stream: homeBloc.streamAliasDevices, stream: homeBloc.streamAliasDevices,
builder: (context, aliasDevicesSnapshot) { builder: (context, aliasDevicesSnapshot) {
if(aliasDevicesSnapshot.data == null){ if (aliasDevicesSnapshot.data == null) {
homeBloc.getOwnerAndJoinedDevices(context); homeBloc.getOwnerAndJoinedDevices(context);
return const SharedLoadingAnimation(); return const SharedLoadingAnimation();
}else{ } else {
homeBloc.getOwnerDeviceState(context, aliasDevicesSnapshot.data ?? []); homeBloc.getOwnerDeviceState(
homeBloc.getDeviceStatusAliasMap(aliasDevicesSnapshot.data ?? []); context, aliasDevicesSnapshot.data ?? []);
checkSettingDevice(aliasDevicesSnapshot.data ?? []); homeBloc.getDeviceStatusAliasMap(aliasDevicesSnapshot.data ?? []);
return Scaffold( checkSettingDevice(aliasDevicesSnapshot.data ?? []);
body: Padding( return Scaffold(
padding: context.paddingLow, floatingActionButton: FloatingActionButton(
child: SingleChildScrollView( onPressed: () {
child: Column( loadAlarms();
children: <Widget>[ },
Row( child: const Icon(Icons.alarm),
children: [ ),
Text( body: Padding(
appLocalization(context).notification, padding: context.paddingLow,
style: context.titleMediumTextStyle, child: SingleChildScrollView(
), child: Column(
SizedBox(width: context.lowValue), children: <Widget>[
StreamBuilder<int>( Row(
stream: homeBloc.streamCountNotification, children: [
builder: (context, countSnapshot) { Text(
if(countSnapshot.data == null){ appLocalization(context).notification,
homeBloc.getOwnerDeviceState(context, aliasDevicesSnapshot.data ?? []); style: context.titleMediumTextStyle,
return const Text("0"); ),
} else{ SizedBox(width: context.lowValue),
return Text( StreamBuilder<int>(
"(${countSnapshot.data ?? 0})", stream: homeBloc.streamCountNotification,
style: context.titleMediumTextStyle, builder: (context, countSnapshot) {
); if (countSnapshot.data == null) {
} homeBloc.getOwnerDeviceState(
}, context, aliasDevicesSnapshot.data ?? []);
) return const Text("0");
], } else {
), return Text(
SizedBox( "(${countSnapshot.data ?? 0})",
child: SingleChildScrollView( style: context.titleMediumTextStyle,
scrollDirection: Axis.horizontal, );
child: StreamBuilder<Map<String, List<DeviceWithAlias>>>( }
stream: homeBloc.streamOwnerDevicesStatus, },
builder: (context, ownerDevicesStatusSnapshot) { )
if(ownerDevicesStatusSnapshot.data == null){ ],
homeBloc.getOwnerDeviceState(context, aliasDevicesSnapshot.data ?? []); ),
return const SharedComponentLoadingAnimation(); SizedBox(
}else{ child: SingleChildScrollView(
return AnimatedSwitcher( scrollDirection: Axis.horizontal,
duration: context.lowDuration, child:
transitionBuilder: StreamBuilder<Map<String, List<DeviceWithAlias>>>(
(Widget child, Animation<double> animation) { stream: homeBloc.streamOwnerDevicesStatus,
final offsetAnimation = Tween<Offset>( builder: (context, ownerDevicesStatusSnapshot) {
begin: const Offset(0.0, 0.2), if (ownerDevicesStatusSnapshot.data == null) {
end: Offset.zero, homeBloc.getOwnerDeviceState(
).animate(CurvedAnimation( context, aliasDevicesSnapshot.data ?? []);
parent: animation, return const SharedComponentLoadingAnimation();
curve: Curves.easeInOut, } else {
)); return AnimatedSwitcher(
return FadeTransition( duration: context.lowDuration,
opacity: animation, transitionBuilder: (Widget child,
child: SlideTransition( Animation<double> animation) {
position: offsetAnimation, final offsetAnimation = Tween<Offset>(
child: child, begin: const Offset(0.0, 0.2),
), end: Offset.zero,
); ).animate(CurvedAnimation(
}, parent: animation,
child: ownerDevicesStatusSnapshot.data?['state'] != null || curve: Curves.easeInOut,
ownerDevicesStatusSnapshot.data?['battery'] != null ));
? ConstrainedBox( return FadeTransition(
key: const ValueKey('data'), opacity: animation,
constraints: BoxConstraints( child: SlideTransition(
minWidth: position: offsetAnimation,
MediaQuery.of(context).size.width), child: child,
child: Row( ),
mainAxisAlignment: MainAxisAlignment.start, );
children: [ },
if (ownerDevicesStatusSnapshot.data?['state'] != null) child: ownerDevicesStatusSnapshot
...ownerDevicesStatusSnapshot.data!['state']! .data?['state'] !=
.map( null ||
(item) => SizedBox( ownerDevicesStatusSnapshot
width: context.dynamicWidth(0.95), .data?['battery'] !=
child: FutureBuilder<Widget>( null
future: warningCard( ? ConstrainedBox(
context, apiServices, item), key: const ValueKey('data'),
builder: (context, constraints: BoxConstraints(
warningCardSnapshot) { minWidth: MediaQuery.of(context)
if (warningCardSnapshot .size
.hasData) { .width),
return warningCardSnapshot child: Row(
.data!; mainAxisAlignment:
} else { MainAxisAlignment.start,
return const SizedBox children: [
.shrink(); if (ownerDevicesStatusSnapshot
} .data?['state'] !=
}, null)
), ...ownerDevicesStatusSnapshot
.data!['state']!
.map(
(item) => SizedBox(
width: context
.dynamicWidth(0.95),
child: FutureBuilder<
Widget>(
future: warningCard(
context,
apiServices,
item),
builder: (context,
warningCardSnapshot) {
if (warningCardSnapshot
.hasData) {
return warningCardSnapshot
.data!;
} else {
return const SizedBox
.shrink();
}
},
),
),
)
.toList(),
if (ownerDevicesStatusSnapshot
.data?['battery'] !=
null)
...ownerDevicesStatusSnapshot
.data!['battery']!
.map(
(batteryItem) => SizedBox(
width: context
.dynamicWidth(0.95),
child: FutureBuilder<
Widget>(
future:
notificationCard(
context,
"lowBattery",
appLocalization(
context)
.low_battery_message,
batteryItem,
),
builder: (context,
warningCardSnapshot) {
if (warningCardSnapshot
.hasData) {
return warningCardSnapshot
.data!;
} else {
return const SizedBox
.shrink();
}
},
),
),
)
.toList(),
],
), ),
) )
.toList(), : Padding(
if (ownerDevicesStatusSnapshot.data?['battery'] != null) key: const ValueKey('no_data'),
...ownerDevicesStatusSnapshot.data!['battery']! padding: context.paddingMedium,
.map( child: Center(
(batteryItem) => SizedBox( child: Row(
width: context.dynamicWidth(0.95), mainAxisSize: MainAxisSize.min,
child: FutureBuilder<Widget>( children: [
future: notificationCard( const Icon(
context, Icons
"lowBattery", .check_circle_outline_rounded,
appLocalization(context) size: 40,
.low_battery_message, color: Colors.green,
batteryItem, ),
), SizedBox(
builder: (context, width: context.lowValue),
warningCardSnapshot) { Text(
if (warningCardSnapshot appLocalization(context)
.hasData) { .notification_description,
return warningCardSnapshot maxLines: 2,
.data!; overflow:
} else { TextOverflow.ellipsis,
return const SizedBox softWrap: true,
.shrink(); textAlign: TextAlign.start,
} ),
}, ],
), ),
), ),
)
.toList(),
],
),
)
: Padding(
key: const ValueKey('no_data'),
padding: context.paddingMedium,
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.check_circle_outline_rounded,
size: 40,
color: Colors.green,
), ),
SizedBox(width: context.lowValue), );
Text( }
appLocalization(context) },
.notification_description, ),
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: true,
textAlign: TextAlign.start,
),
],
),
),
),
);
}
},
), ),
), ),
), StreamBuilder<Map<String, List<DeviceWithAlias>>?>(
StreamBuilder<Map<String, List<DeviceWithAlias>>?>( stream: homeBloc.streamAllDevicesAliasMap,
stream: homeBloc.streamAllDevicesAliasMap, builder: (context, allDevicesAliasMapSnapshot) {
builder: (context, allDevicesAliasMapSnapshot) { if (allDevicesAliasMapSnapshot.data == null) {
if(allDevicesAliasMapSnapshot.data == null){ homeBloc.getDeviceStatusAliasMap(
homeBloc.getDeviceStatusAliasMap(aliasDevicesSnapshot.data ?? []); aliasDevicesSnapshot.data ?? []);
return const SharedComponentLoadingAnimation(); return const SharedComponentLoadingAnimation();
}else{ } else {
final data = allDevicesAliasMapSnapshot.data!; final data = allDevicesAliasMapSnapshot.data!;
return OverviewCard( return OverviewCard(
isOwner: true, isOwner: true,
total: data['all']?.length ?? 0, total: data['all']?.length ?? 0,
active: data['online']?.length ?? 0, active: data['online']?.length ?? 0,
inactive: data['offline']?.length ?? 0, inactive: data['offline']?.length ?? 0,
warning: data['warn']?.length ?? 0, warning: data['warn']?.length ?? 0,
unused: data['not-use']?.length ?? 0, unused: data['not-use']?.length ?? 0,
showUnused: false, showUnused: false,
); );
} }
}, },
), ),
], ],
),
), ),
), ),
), );
); }
} });
}
);
} }
void checkSettingDevice(List<DeviceWithAlias> devices) async { void checkSettingDevice(List<DeviceWithAlias> devices) async {

View File

@@ -76,11 +76,11 @@ class _MainScreenState extends State<MainScreen> with WidgetsBindingObserver {
mainBloc.sinkThemeMode.add(isLight); mainBloc.sinkThemeMode.add(isLight);
checkAndRequestPermission(); checkAndRequestPermission();
NotificationServices.requestNotificationPermission(); NotificationServices.requestNotificationPermission();
NotificationPermission.instance.checkNotificationPermission(context); // NotificationPermission.instance.checkNotificationPermission(context);
bool? notificationStatus = await NotificationPermission.instance.requestNotificationPermission(); // bool? notificationStatus = await NotificationPermission.instance.requestNotificationPermission();
if(!notificationStatus!){ // if(notificationStatus == false){
showNoIconTopSnackBar(context, "Yêu cầu quyền thông báo không thành công", Colors.orange, Colors.white12); // showNoIconTopSnackBar(context, "Yêu cầu quyền thông báo không thành công", Colors.orange, Colors.white12);
} // }
} }
@override @override

View File

@@ -1,9 +1,9 @@
import 'package:alarm/alarm.dart'; 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/material.dart'; import 'package:flutter/material.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart' show PersistentTabController; 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';
@@ -15,16 +15,17 @@ 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';
PersistentTabController controller = PersistentTabController(initialIndex: 0); PersistentTabController controller = PersistentTabController(initialIndex: 0);
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform, name: "sfm-notification"); await Firebase.initializeApp(
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); options: DefaultFirebaseOptions.currentPlatform,
name: "sfm-notification");
FirebaseMessaging.onBackgroundMessage(
NotificationServices.firebaseMessagingBackgroundHandler);
await Alarm.init(); await Alarm.init();
await Alarm.stopAll();
runApp( runApp(
BlocProvider( BlocProvider(
child: const MyApp(), child: const MyApp(),
@@ -33,7 +34,6 @@ void main() async {
); );
} }
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {
const MyApp({super.key}); const MyApp({super.key});
@@ -67,7 +67,7 @@ class _MyAppState extends State<MyApp> {
_themeData = theme; _themeData = theme;
mainBloc.sinkTheme.add(_themeData); mainBloc.sinkTheme.add(_themeData);
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -92,19 +92,18 @@ class _MyAppState extends State<MyApp> {
initialData: _locale, initialData: _locale,
builder: (context, languageSnapshot) { builder: (context, languageSnapshot) {
return StreamBuilder<ThemeData?>( return StreamBuilder<ThemeData?>(
stream: mainBloc.streamTheme, stream: mainBloc.streamTheme,
initialData: _themeData, initialData: _themeData,
builder: (context, themeSnapshot) { builder: (context, themeSnapshot) {
return MaterialApp.router( return MaterialApp.router(
theme: themeSnapshot.data, theme: themeSnapshot.data,
routerConfig: router, routerConfig: router,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: languageSnapshot.data, locale: languageSnapshot.data,
); );
} });
);
}, },
); );
} }

View File

@@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:alarm/alarm.dart'; import 'package:alarm/alarm.dart';
@pragma('vm:entry-point')
class AlarmServices { class AlarmServices {
Future<void> showAlarm(String title, String body) async { Future<void> showAlarm(String title, String body) async {
final DateTime now = DateTime.now(); final DateTime now = DateTime.now();

View File

@@ -2,6 +2,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@@ -27,8 +29,6 @@ import '../constant/enums/local_keys_enums.dart';
import '../network/network_manager.dart'; import '../network/network_manager.dart';
class APIServices { class APIServices {
Map<String, String> headers = { Map<String, String> headers = {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -92,7 +92,7 @@ class APIServices {
// try { // try {
// return await apiCall(); // return await apiCall();
// } catch (e) { // } catch (e) {
// AppLoggerUtils.error(e.toString()); // AppLoggerUtils.error(e.toString());
// return Future.error(e); // return Future.error(e);
// } // }
@@ -104,12 +104,33 @@ class APIServices {
bool checkMounted = true, bool checkMounted = true,
}) async { }) async {
try { try {
// Quick connectivity check before attempting network calls.
final conn = await Connectivity().checkConnectivity();
if (conn == ConnectivityResult.none) {
AppLoggerUtils.warning('No network connectivity');
if (checkMounted && context.mounted) {
showErrorTopSnackBarCustom(context, "Không có kết nối mạng");
}
return Future.error(const SocketException('No network connectivity'));
}
return await apiCall(); return await apiCall();
} catch (e) { } on SocketException catch (e, stackTrace) {
// Network-related errors (DNS, timeout, host lookup...)
AppLoggerUtils.warning('Network error when calling API: $e');
if (checkMounted && context.mounted) {
AppLoggerUtils.error("Không có kết nối mạng");
}
return Future.error(e);
} catch (e, stackTrace) {
// If widget was unmounted (e.g. background isolate), preserve previous behavior
if (checkMounted && !context.mounted) { if (checkMounted && !context.mounted) {
return Future.error('Widget not mounted'); return Future.error('Widget not mounted');
} }
showErrorTopSnackBarCustom(context, "Lỗi hệ thống"); AppLoggerUtils.error("Lỗi hệ thống khi gọi API", e, stackTrace);
if (checkMounted && context.mounted) {
showErrorTopSnackBarCustom(context, "Lỗi hệ thống");
}
return Future.error(e); return Future.error(e);
} }
} }
@@ -308,7 +329,6 @@ class APIServices {
.getDataFromServer(APIPathConstants.PROVINCES_PATH), .getDataFromServer(APIPathConstants.PROVINCES_PATH),
parser: (json) => Province.fromJsonDynamicList(json['items']), parser: (json) => Province.fromJsonDynamicList(json['items']),
errorMessage: 'Lỗi khi GET /${APIPathConstants.PROVINCES_PATH}'); errorMessage: 'Lỗi khi GET /${APIPathConstants.PROVINCES_PATH}');
} }
Future<List<Province>> getProvincesByName(String name) async { Future<List<Province>> getProvincesByName(String name) async {
@@ -319,7 +339,6 @@ class APIServices {
parser: (json) => Province.fromJsonDynamicList(json['items']), parser: (json) => Province.fromJsonDynamicList(json['items']),
errorMessage: 'Lỗi khi GET /${APIPathConstants.PROVINCES_PATH}/$name', errorMessage: 'Lỗi khi GET /${APIPathConstants.PROVINCES_PATH}/$name',
); );
} }
Future<Province> getProvinceByID(String provinceID) async { Future<Province> getProvinceByID(String provinceID) async {

View File

@@ -4,20 +4,21 @@ import 'dart:math' as math;
import 'package:alarm/alarm.dart'; 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'
hide NotificationSettings; as firebase_messaging;
import 'package:flutter/material.dart'; import 'package:flutter/material.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:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../utils/app_logger_utils.dart'; import '../utils/app_logger_utils.dart';
import '../../firebase_options.dart'; import '../../firebase_options.dart';
import '../../main.dart';
import 'alarm_services.dart'; import 'alarm_services.dart';
@pragma('vm:entry-point')
class NotificationServices { class NotificationServices {
static final FlutterLocalNotificationsPlugin _notificationsPlugin = static final FlutterLocalNotificationsPlugin _notificationsPlugin =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
final FirebaseMessaging _messaging = FirebaseMessaging.instance; final firebase_messaging.FirebaseMessaging _messaging =
firebase_messaging.FirebaseMessaging.instance;
AlarmServices alarmServices = AlarmServices(); AlarmServices alarmServices = AlarmServices();
Future<void> initialize() async { Future<void> initialize() async {
@@ -25,36 +26,51 @@ class NotificationServices {
dev.log("NotificationService initialized"); dev.log("NotificationService initialized");
} }
@pragma('vm:entry-point')
Future<void> initializeLocalNotifications() async { Future<void> initializeLocalNotifications() async {
try { try {
const AndroidInitializationSettings androidInitializationSettings = const androidInitSettings =
AndroidInitializationSettings('@mipmap/ic_launcher'); AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings iosInitializationSettings = final darwinInitSettings = DarwinInitializationSettings(
DarwinInitializationSettings(); onDidReceiveLocalNotification: _onDidReceiveLocalNotification,
const InitializationSettings initializationSettings = );
InitializationSettings( final initSettings = InitializationSettings(
android: androidInitializationSettings, android: androidInitSettings,
iOS: iosInitializationSettings, iOS: darwinInitSettings,
); );
await _notificationsPlugin.initialize( await _notificationsPlugin.initialize(
initializationSettings, initSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) { onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse,
dev.log(
"Người dùng click thông báo ở foreground với payload: ${response.payload}");
handleMessage(response.payload, controller);
},
); );
dev.log("Local notifications initialized"); dev.log("Local notifications initialized");
} catch (e) { } 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) { void firebaseInit(BuildContext context) {
FirebaseMessaging.onMessage.listen((message) { firebase_messaging.FirebaseMessaging.onMessage.listen((message) {
dev.log("Foreground message payload: ${message.toMap()}"); _handleForegroundMessage(message);
_showNotification(message);
}); });
} }
@@ -66,75 +82,156 @@ class NotificationServices {
static Future<bool?> requestNotificationPermission() async { static Future<bool?> requestNotificationPermission() async {
try { try {
if (Platform.isAndroid) { final messaging = firebase_messaging.FirebaseMessaging.instance;
return await _notificationsPlugin final settings = await messaging.requestPermission(
.resolvePlatformSpecificImplementation< alert: true,
AndroidFlutterLocalNotificationsPlugin>() announcement: false,
?.requestNotificationsPermission(); badge: true,
} else if (Platform.isIOS) { provisional: false,
return await _notificationsPlugin sound: true,
.resolvePlatformSpecificImplementation< );
IOSFlutterLocalNotificationsPlugin>() return settings.authorizationStatus ==
?.requestPermissions(alert: true, sound: true, badge: true); firebase_messaging.AuthorizationStatus.authorized;
}
return null;
} catch (e) { } catch (e) {
dev.log("Error requesting notification permission: $e"); AppLoggerUtils.error("Failed to request notification permission", e);
return null; return null;
} }
} }
Future<void> _showNotification(RemoteMessage message) async { /// Xử lý message khi app foreground
Future<void> _handleForegroundMessage(
firebase_messaging.RemoteMessage message) async {
try { 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 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('Foreground message: type=$type, title=$title, body=$body');
dev.log('Skipping notification: missing title or body',
name: 'Notification');
return;
}
// Handle smoke warning notifications
if (type == 'smoke_warning') { if (type == 'smoke_warning') {
await alarmServices.showAlarm(title, body); // Hiển thị alarm, không hiển thị notification
dev.log('Displayed smoke warning notification', name: 'Notification'); await _showAlarm(title, body);
return; } 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 /// Hiển thị alarm (smoke warning)
final channelId = math.Random.secure().nextInt(1000000).toString(); Future<void> _showAlarm(String title, String body) async {
const channelName = 'High Importance Notification'; try {
const channelDescription = 'Channel description'; await Alarm.init();
// final androidChannel = AndroidNotificationChannel( final alarmSettings = AlarmSettings(
// channelId, id: 42,
// channelName, dateTime: DateTime.now(),
// importance: Importance.max, assetAudioPath: 'assets/sounds/warning_alarm.mp3',
// description: channelDescription, 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 = await Alarm.set(alarmSettings: alarmSettings);
_notificationsPlugin.resolvePlatformSpecificImplementation< dev.log('Alarm set successfully: $title');
AndroidFlutterLocalNotificationsPlugin>(); } catch (e, stackTrace) {
AppLoggerUtils.error('Failed to set alarm', e, stackTrace);
}
}
// Delete existing channel to prevent conflicts /// Hiển thị notification local khi app foreground
await androidPlugin?.deleteNotificationChannel(channelId); Future<void> _showForegroundNotification(
String title, String body, String type) async {
// Configure notification details try {
final androidDetails = AndroidNotificationDetails( final androidDetails = AndroidNotificationDetails(
channelId, 'sfm_channel_${math.Random.secure().nextInt(1000000)}',
channelName, 'SFM Notifications',
channelDescription: channelDescription, channelDescription: 'Notifications from SFM app',
sound: getSound(type), sound: getSound(type),
importance: Importance.max, importance: Importance.max,
priority: Priority.high, priority: Priority.high,
ticker: 'ticker', 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, presentAlert: true,
presentBadge: true, presentBadge: true,
presentBanner: true, presentBanner: true,
@@ -146,7 +243,6 @@ class NotificationServices {
iOS: iosDetails, iOS: iosDetails,
); );
// Show notification
await _notificationsPlugin.show( await _notificationsPlugin.show(
int.parse(channelId), int.parse(channelId),
title, title,
@@ -156,16 +252,10 @@ class NotificationServices {
); );
dev.log( dev.log(
'Displayed notification - title: $title, body: $body, type: $type', 'Background/terminate notification shown: title=$title, type=$type');
name: 'Notification',
);
} catch (e, stackTrace) { } catch (e, stackTrace) {
dev.log( AppLoggerUtils.error(
'Failed to show notification: $e', 'Failed to show background/terminate notification', e, stackTrace);
name: 'Notification',
error: e,
stackTrace: stackTrace,
);
} }
} }
@@ -179,153 +269,110 @@ class NotificationServices {
} else if (type == "battery_warning") { } else if (type == "battery_warning") {
return const RawResourceAndroidNotificationSound("new_alarm"); return const RawResourceAndroidNotificationSound("new_alarm");
} else { } 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) { 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); 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 { Future<void> setupInteractMessage(PersistentTabController controller) async {
// Khi app terminated // Khi app terminated, được mở bằng notification
RemoteMessage? initialMessage = firebase_messaging.RemoteMessage? initialMessage =
await FirebaseMessaging.instance.getInitialMessage(); await firebase_messaging.FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) { if (initialMessage != null) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
try { final type = initialMessage.data['type'] as String?;
handleMessage(initialMessage.data['type'], controller); handleMessage(type, controller);
} catch (e, stack) {
dev.log("Error handling initial message: $e\n$stack");
}
}); });
} }
// Khi app ở background
FirebaseMessaging.onMessageOpenedApp.listen((message) { // Khi app background, user tap notification
firebase_messaging.FirebaseMessaging.onMessageOpenedApp.listen((message) {
try { try {
handleMessage(message.data['type'], controller); final type = message.data['type'] as String?;
} catch (e, stack) { handleMessage(type, controller);
dev.log("Error in onMessageOpenedApp: $e\n$stack"); } catch (e, stackTrace) {
AppLoggerUtils.error('Error in onMessageOpenedApp', e, stackTrace);
} }
}); });
} }
Future<void> showBackgroundOrTerminateNotification( @pragma('vm:entry-point')
RemoteMessage message) async { static Future<void> firebaseMessagingBackgroundHandler(
firebase_messaging.RemoteMessage message) async {
try { try {
// Early validation of notification data // Log to device storage for debugging background isolate
final title = message.data['title'] as String?; dev.log('═══ BACKGROUND HANDLER STARTED ═══');
final body = message.data['body'] as String?; dev.log('Message data: ${message.data}');
final type = message.data['type'] as String? ?? 'normal'; dev.log('Notification: ${message.notification}');
if (title == null || body == null) { await Alarm.init();
dev.log('Skipping notification: missing title or body', dev.log('✓ Alarm initialized');
name: 'Notification');
return; 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 type = message.data['type'] as String? ?? 'normal';
final channelId = math.Random.secure().nextInt(1000000).toString(); final title = message.notification?.title ??
const channelName = 'High Importance Notification'; message.data['title'] ??
const channelDescription = 'Channel description'; '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 dev.log('Message type: $type');
final androidDetails = AndroidNotificationDetails( dev.log('Title: $title');
channelId, dev.log('Body: $body');
channelName,
channelDescription: channelDescription,
sound: getSound(type),
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
);
const iosDetails = DarwinNotificationDetails( // Logic: Hiển thị local notification cho background/terminate
presentAlert: true, // iOS sẽ phát sound natively thông qua APNs payload (aps.sound)
presentBadge: true, // Android sẽ phát sound thông qua local notification
presentBanner: true, try {
presentSound: true, final notificationService = NotificationServices();
); await notificationService.initialize();
dev.log('✓ Notification service initialized');
final notificationDetails = NotificationDetails( await notificationService
android: androidDetails, .showBackgroundOrTerminateNotification(message);
iOS: iosDetails, dev.log('✓ Background/terminate notification shown');
); } catch (notifError, notifStackTrace) {
dev.log('✗ ERROR showing notification: $notifError');
dev.log('Stack trace: $notifStackTrace');
rethrow;
}
// Show notification dev.log('═══ BACKGROUND HANDLER COMPLETED ═══');
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) { } catch (e, stackTrace) {
dev.log( dev.log('✗ BACKGROUND HANDLER FAILED');
'Failed to show notification: $e', dev.log('Error: $e');
name: 'Notification', dev.log('Stack trace: $stackTrace');
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

@@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation import Foundation
import app_settings import app_settings
import connectivity_plus
import firebase_core import firebase_core
import firebase_messaging import firebase_messaging
import flutter_local_notifications import flutter_local_notifications
@@ -16,6 +17,7 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppSettingsPlugin.register(with: registry.registrar(forPlugin: "AppSettingsPlugin")) AppSettingsPlugin.register(with: registry.registrar(forPlugin: "AppSettingsPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))

View File

@@ -113,6 +113,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
connectivity_plus:
dependency: "direct main"
description:
name: connectivity_plus
sha256: "33bae12a398f841c6cda09d1064212957265869104c478e5ad51e2fb26c3973c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@@ -629,6 +645,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.16.0"
nm:
dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
path: path:
dependency: transitive dependency: transitive
description: description:

View File

@@ -49,6 +49,7 @@ dependencies:
lottie: ^3.3.1 lottie: ^3.3.1
logger: ^2.5.0 logger: ^2.5.0
alarm: ^5.1.4 alarm: ^5.1.4
connectivity_plus: ^7.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <firebase_core/firebase_core_plugin_c_api.h> #include <firebase_core/firebase_core_plugin_c_api.h>
#include <geolocator_windows/geolocator_windows.h> #include <geolocator_windows/geolocator_windows.h>
#include <maps_launcher/maps_launcher_plugin.h> #include <maps_launcher/maps_launcher_plugin.h>
@@ -13,6 +14,8 @@
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FirebaseCorePluginCApiRegisterWithRegistrar( FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
GeolocatorWindowsRegisterWithRegistrar( GeolocatorWindowsRegisterWithRegistrar(

View File

@@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
firebase_core firebase_core
geolocator_windows geolocator_windows
maps_launcher maps_launcher