awesome_notifications_helper.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import 'package:awesome_notifications/awesome_notifications.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:get/get.dart';
  4. import '../app/constants/sp_keys.dart';
  5. import '../app/dialog/custom_dialog.dart';
  6. import '../app/data/sp/ix_sp.dart';
  7. import '../app/routes/app_pages.dart';
  8. import '../config/translations/strings_enum.dart';
  9. import 'log/logger.dart';
  10. import 'ntp_time_service.dart';
  11. class AwesomeNotificationsHelper {
  12. // prevent making instance
  13. AwesomeNotificationsHelper._();
  14. static const String TAG = 'AwesomeNotificationsHelper';
  15. // Notification lib
  16. static AwesomeNotifications awesomeNotifications = AwesomeNotifications();
  17. // 标记是否已经请求过权限
  18. static bool _hasRequestedPermission = false;
  19. /// initialize local notifications service, create channels and groups
  20. /// setup notifications button actions handlers
  21. static init() async {
  22. try {
  23. // 初始化通知渠道和组
  24. await _initNotification();
  25. // 设置监听
  26. listenToActionButtons();
  27. // 检查权限但不立即请求
  28. bool isAllowed = await checkNotificationPermission();
  29. if (!isAllowed) {}
  30. } catch (e) {
  31. log(TAG, 'Notification initialization failed: $e');
  32. }
  33. }
  34. /// 检查通知权限状态
  35. static Future<bool> checkNotificationPermission() async {
  36. return await awesomeNotifications.isNotificationAllowed();
  37. }
  38. /// 打开通知权限设置
  39. static Future<void> showNotificationConfigPage() async {
  40. return await awesomeNotifications.showNotificationConfigPage();
  41. }
  42. /// 请求通知权限(可以被Firebase或其他组件调用)
  43. static Future<bool> requestNotificationPermission() async {
  44. // 如果已经请求过权限,先检查当前状态
  45. if (_hasRequestedPermission) {
  46. final isAllowed = await checkNotificationPermission();
  47. if (isAllowed) return true;
  48. }
  49. // 请求权限
  50. final isAllowed = await awesomeNotifications
  51. .requestPermissionToSendNotifications();
  52. // 标记已请求过权限
  53. _hasRequestedPermission = true;
  54. return isAllowed;
  55. }
  56. /// 检查是否应该显示通知权限提醒弹窗
  57. /// 返回 true 表示应该显示
  58. static Future<bool> shouldShowPushNoticeDialog() async {
  59. try {
  60. // 如果已经开启通知权限,不显示
  61. final isAllowed = await checkNotificationPermission();
  62. if (isAllowed) {
  63. log(TAG, 'Notification already allowed, skip dialog');
  64. return false;
  65. }
  66. // 检查是否超过 pushNoticeTime 分钟
  67. final appConfig = IXSP.getAppConfig();
  68. final pushNoticeMinutes = appConfig?.pushNoticeTime ?? 10080; // 默认7天
  69. final lastNoticeTimeStr =
  70. IXSP.getString(SPKeys.lastPushNoticeTime) ?? '0';
  71. final lastNoticeTime = int.tryParse(lastNoticeTimeStr) ?? 0;
  72. final now = NtpTimeService().getCurrentTimestamp();
  73. // 第一次(lastNoticeTime == 0)或超过间隔时间
  74. if (lastNoticeTime == 0) {
  75. log(TAG, 'First time, should show push notice dialog');
  76. return true;
  77. }
  78. final elapsedMinutes = (now - lastNoticeTime) / (1000 * 60);
  79. if (elapsedMinutes >= pushNoticeMinutes) {
  80. log(
  81. TAG,
  82. 'Push notice time elapsed: ${elapsedMinutes.toStringAsFixed(1)}min >= ${pushNoticeMinutes}min',
  83. );
  84. return true;
  85. }
  86. log(
  87. TAG,
  88. 'Push notice skipped: ${elapsedMinutes.toStringAsFixed(1)}min elapsed, '
  89. 'need ${pushNoticeMinutes}min',
  90. );
  91. return false;
  92. } catch (e) {
  93. log(TAG, 'Error checking push notice: $e');
  94. return false;
  95. }
  96. }
  97. /// 显示通知权限提醒弹窗
  98. static Future<void> showPushNoticeDialog() async {
  99. final shouldShow = await shouldShowPushNoticeDialog();
  100. if (!shouldShow) return;
  101. // 记录本次提醒时间
  102. _saveLastPushNoticeTime();
  103. CustomDialog.showConfirm(
  104. title: Strings.enableNotifications.tr,
  105. message: Strings.enableNotificationsDesc.tr,
  106. confirmText: Strings.enable.tr,
  107. cancelText: Strings.notNow.tr,
  108. icon: Icons.notifications_active_outlined,
  109. iconColor: Get.theme.primaryColor,
  110. confirmButtonColor: Get.theme.primaryColor,
  111. onConfirm: () async {
  112. Navigator.of(Get.context!).pop();
  113. await requestNotificationPermission();
  114. },
  115. onCancel: () {
  116. Navigator.of(Get.context!).pop();
  117. },
  118. );
  119. }
  120. /// 记录上次提醒时间
  121. static void _saveLastPushNoticeTime() {
  122. IXSP.setString(
  123. SPKeys.lastPushNoticeTime,
  124. NtpTimeService().getCurrentTimestamp().toString(),
  125. );
  126. }
  127. /// when user click on notification or click on button on the notification
  128. static listenToActionButtons() {
  129. // Only after at least the action method is set, the notification events are delivered
  130. awesomeNotifications.setListeners(
  131. onActionReceivedMethod: NotificationController.onActionReceivedMethod,
  132. onNotificationCreatedMethod:
  133. NotificationController.onNotificationCreatedMethod,
  134. onNotificationDisplayedMethod:
  135. NotificationController.onNotificationDisplayedMethod,
  136. onDismissActionReceivedMethod:
  137. NotificationController.onDismissActionReceivedMethod,
  138. );
  139. }
  140. ///init notifications channels
  141. static _initNotification() async {
  142. await awesomeNotifications.initialize(
  143. 'resource://mipmap/launcher_icon', // 添加小图标资源路径,
  144. [
  145. NotificationChannel(
  146. channelGroupKey: NotificationChannels.generalChannelGroupKey,
  147. channelKey: NotificationChannels.generalChannelKey,
  148. channelName: NotificationChannels.generalChannelName,
  149. groupKey: NotificationChannels.generalGroupKey,
  150. channelDescription: NotificationChannels.generalChannelDescription,
  151. defaultColor: Colors.green,
  152. ledColor: Colors.white,
  153. channelShowBadge: true,
  154. playSound: true,
  155. importance: NotificationImportance.Max,
  156. ),
  157. NotificationChannel(
  158. channelGroupKey: NotificationChannels.chatChannelGroupKey,
  159. channelKey: NotificationChannels.chatChannelKey,
  160. channelName: NotificationChannels.chatChannelName,
  161. groupKey: NotificationChannels.chatGroupKey,
  162. channelDescription: NotificationChannels.chatChannelDescription,
  163. defaultColor: Colors.green,
  164. ledColor: Colors.white,
  165. channelShowBadge: true,
  166. playSound: true,
  167. importance: NotificationImportance.Max,
  168. ),
  169. ],
  170. channelGroups: [
  171. NotificationChannelGroup(
  172. channelGroupKey: NotificationChannels.generalChannelGroupKey,
  173. channelGroupName: NotificationChannels.generalChannelGroupName,
  174. ),
  175. NotificationChannelGroup(
  176. channelGroupKey: NotificationChannels.chatChannelGroupKey,
  177. channelGroupName: NotificationChannels.chatChannelGroupName,
  178. ),
  179. ],
  180. );
  181. }
  182. //display notification for user with sound
  183. static Future<bool> showNotification({
  184. required String title,
  185. required String body,
  186. required int id,
  187. String? channelKey,
  188. String? groupKey,
  189. NotificationLayout? notificationLayout,
  190. String? summary,
  191. List<NotificationActionButton>? actionButtons,
  192. Map<String, String>? payload,
  193. String? largeIcon,
  194. }) async {
  195. try {
  196. final isAllowed = await checkNotificationPermission();
  197. if (!isAllowed) {
  198. final requested = await requestNotificationPermission();
  199. if (!requested) return false;
  200. }
  201. await awesomeNotifications.createNotification(
  202. content: NotificationContent(
  203. id: id,
  204. title: title,
  205. body: body,
  206. groupKey: groupKey ?? NotificationChannels.generalGroupKey,
  207. channelKey: channelKey ?? NotificationChannels.generalChannelKey,
  208. showWhen: true,
  209. payload: payload,
  210. notificationLayout: notificationLayout ?? NotificationLayout.Default,
  211. autoDismissible: true,
  212. summary: summary,
  213. largeIcon: largeIcon,
  214. ),
  215. actionButtons: actionButtons,
  216. );
  217. return true;
  218. } catch (e) {
  219. log(TAG, 'Sending notification failed: $e');
  220. return false;
  221. }
  222. }
  223. }
  224. class NotificationController {
  225. /// Use this method to detect when a new notification or a schedule is created
  226. @pragma("vm:entry-point")
  227. static Future<void> onNotificationCreatedMethod(
  228. ReceivedNotification receivedNotification,
  229. ) async {
  230. // Your code goes here
  231. }
  232. /// Use this method to detect every time that a new notification is displayed
  233. @pragma("vm:entry-point")
  234. static Future<void> onNotificationDisplayedMethod(
  235. ReceivedNotification receivedNotification,
  236. ) async {
  237. // Your code goes here
  238. }
  239. /// Use this method to detect if the user dismissed a notification
  240. @pragma("vm:entry-point")
  241. static Future<void> onDismissActionReceivedMethod(
  242. ReceivedAction receivedAction,
  243. ) async {
  244. // Your code goes here
  245. }
  246. /// Use this method to detect when the user taps on a notification or action button
  247. @pragma("vm:entry-point")
  248. static Future<void> onActionReceivedMethod(
  249. ReceivedAction receivedAction,
  250. ) async {
  251. try {
  252. final payload = receivedAction.payload;
  253. if (payload != null) {
  254. final route = payload['route'];
  255. if (route != null) {
  256. Get.key.currentState?.pushNamed(route);
  257. } else {
  258. Get.key.currentState?.pushNamed(Routes.HOME);
  259. }
  260. }
  261. } catch (e) {
  262. log(
  263. "onActionReceivedMethod",
  264. 'Handling notification click event failed: $e',
  265. );
  266. }
  267. }
  268. }
  269. class NotificationChannels {
  270. // chat channel (for messages only)
  271. static String get chatChannelKey => "chat_channel";
  272. static String get chatChannelName => "Chat Notifications";
  273. static String get chatGroupKey => "chat_group_key";
  274. static String get chatChannelGroupKey => "chat_channel_group";
  275. static String get chatChannelGroupName => "Chat Notifications";
  276. static String get chatChannelDescription =>
  277. "Receive chat and message notifications";
  278. // general channel (for all other notifications)
  279. static String get generalChannelKey => "general_channel";
  280. static String get generalGroupKey => "general_group_key";
  281. static String get generalChannelGroupKey => "general_channel_group";
  282. static String get generalChannelGroupName => "General Notifications";
  283. static String get generalChannelName => "General Notifications";
  284. static String get generalChannelDescription =>
  285. "Receive general app notifications";
  286. }