Files
xianyan/lib/core/services/notification/notification_service.dart
Developer 7564e8893d chore: 完成多平台适配与代码优化
此提交包含多项变更:
1. 新增鸿蒙平台支持,完善设备检测与数据库适配
2. 替换旧版分享插件API为SharePlus
3. 批量迁移StateNotifier到Notifier以适配新版Riverpod
4. 修复zip编码判断、图表API参数等bug
5. 更新应用图标、启动页资源与多尺寸适配图标
6. 调整Android最小SDK版本与应用名称
7. 优化日志打印与正则表达式使用
8. 修正编辑器画布样式初始化与配置逻辑
9. 更新依赖与CI插件配置
2026-05-17 07:17:07 +08:00

306 lines
9.3 KiB
Dart

/// ============================================================
/// 闲言APP — 本地通知服务
/// 创建时间: 2026-05-10
/// 更新时间: 2026-05-17
/// 作用: 管理本地推送通知(每日推荐/签到提醒/即时通知)
/// 上次更新: 鸿蒙适配-增加OhosInitializationSettings
/// ============================================================
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:xianyan/core/utils/platform_utils.dart' as pu;
import '../../utils/logger.dart';
class NotificationService {
NotificationService._();
static final FlutterLocalNotificationsPlugin _plugin =
FlutterLocalNotificationsPlugin();
static bool _initialized = false;
static const _keyDailyRecommend = 'notif_daily_recommend';
static const _keyDailyRecommendTime = 'notif_daily_recommend_time';
static const _keySigninReminder = 'notif_signin_reminder';
static const _keySigninReminderTime = 'notif_signin_reminder_time';
static const _channelDailyId = 'xianyan_daily_recommend';
static const _channelDailyName = '每日推荐';
static const _channelSigninId = 'xianyan_signin_reminder';
static const _channelSigninName = '签到提醒';
static Future<bool> init() async {
if (_initialized) return true;
try {
tz.initializeTimeZones();
tz.setLocalLocation(tz.getLocation('Asia/Shanghai'));
const androidSettings = AndroidInitializationSettings(
'@mipmap/ic_launcher',
);
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
const macOsSettings = DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
const ohosSettings = OhosInitializationSettings('@mipmap/ic_launcher');
const settings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
macOS: macOsSettings,
ohos: ohosSettings,
);
final result = await _plugin.initialize(
settings: settings,
onDidReceiveNotificationResponse: _onNotificationTapped,
);
if (result ?? false) {
Log.i('通知插件初始化成功');
} else {
Log.w('通知插件初始化返回 false');
}
await _requestPermissions();
_initialized = true;
return true;
} catch (e) {
Log.e('通知插件初始化失败: $e');
return false;
}
}
static Future<void> _requestPermissions() async {
try {
if (pu.isOhos) {
await _plugin
.resolvePlatformSpecificImplementation<
OhosFlutterLocalNotificationsPlugin
>()
?.requestNotificationsPermission();
}
if (Platform.isIOS) {
await _plugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>()
?.requestPermissions(alert: true, badge: true, sound: true);
}
if (Platform.isMacOS) {
await _plugin
.resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin
>()
?.requestPermissions(alert: true, badge: true, sound: true);
}
} catch (e) {
Log.w('请求通知权限异常: $e');
}
}
static void _onNotificationTapped(NotificationResponse response) {
Log.i('通知被点击: id=${response.id}, payload=${response.payload}');
}
static Future<bool> scheduleDailyRecommend(TimeOfDay time) async {
try {
if (!_initialized) await init();
const androidDetails = AndroidNotificationDetails(
_channelDailyId,
_channelDailyName,
channelDescription: '每日诗词、成语、名言推荐',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _plugin.zonedSchedule(
id: 1001,
title: '今日推荐 ✨',
body: '新的诗词、成语、名言等你来看',
scheduledDate: _nextInstanceOfTime(time.hour, time.minute),
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time,
);
await _saveDailyRecommendPrefs(true, time);
Log.i('每日推荐通知已调度: ${time.hour}:${time.minute}');
return true;
} catch (e) {
Log.e('调度每日推荐通知失败: $e');
return false;
}
}
static Future<bool> scheduleSigninReminder(TimeOfDay time) async {
try {
if (!_initialized) await init();
const androidDetails = AndroidNotificationDetails(
_channelSigninId,
_channelSigninName,
channelDescription: '每日签到提醒',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _plugin.zonedSchedule(
id: 1002,
title: '签到提醒 📝',
body: '别忘了今日签到,连续签到有惊喜哦',
scheduledDate: _nextInstanceOfTime(time.hour, time.minute),
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time,
);
await _saveSigninReminderPrefs(true, time);
Log.i('签到提醒通知已调度: ${time.hour}:${time.minute}');
return true;
} catch (e) {
Log.e('调度签到提醒通知失败: $e');
return false;
}
}
static Future<void> cancelAll() async {
try {
await _plugin.cancelAll();
await _saveDailyRecommendPrefs(false, null);
await _saveSigninReminderPrefs(false, null);
Log.i('已取消所有通知');
} catch (e) {
Log.e('取消通知失败: $e');
}
}
static Future<bool> showImmediate(String title, String body) async {
try {
if (!_initialized) await init();
const androidDetails = AndroidNotificationDetails(
'xianyan_immediate',
'即时通知',
channelDescription: '应用内即时消息',
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
await _plugin.show(
id: now,
title: title,
body: body,
notificationDetails: details,
);
Log.i('即时通知已发送: $title');
return true;
} catch (e) {
Log.e('发送即时通知失败: $e');
return false;
}
}
static Future<bool> isDailyRecommendEnabled() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_keyDailyRecommend) ?? false;
}
static Future<TimeOfDay?> getDailyRecommendTime() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_keyDailyRecommendTime);
if (saved == null) return null;
return _parseTimeOfDay(saved);
}
static Future<bool> isSigninReminderEnabled() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_keySigninReminder) ?? false;
}
static Future<TimeOfDay?> getSigninReminderTime() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_keySigninReminderTime);
if (saved == null) return null;
return _parseTimeOfDay(saved);
}
static Future<void> _saveDailyRecommendPrefs(
bool enabled,
TimeOfDay? time,
) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyDailyRecommend, enabled);
if (time != null) {
await prefs.setString(_keyDailyRecommendTime, _formatTimeOfDay(time));
} else {
await prefs.remove(_keyDailyRecommendTime);
}
}
static Future<void> _saveSigninReminderPrefs(
bool enabled,
TimeOfDay? time,
) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keySigninReminder, enabled);
if (time != null) {
await prefs.setString(_keySigninReminderTime, _formatTimeOfDay(time));
} else {
await prefs.remove(_keySigninReminderTime);
}
}
static tz.TZDateTime _nextInstanceOfTime(int hour, int minute) {
final now = tz.TZDateTime.now(tz.local);
var scheduled = tz.TZDateTime(
tz.local,
now.year,
now.month,
now.day,
hour,
minute,
);
if (scheduled.isBefore(now)) {
scheduled = scheduled.add(const Duration(days: 1));
}
return scheduled;
}
static String _formatTimeOfDay(TimeOfDay t) =>
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
static TimeOfDay _parseTimeOfDay(String s) {
final parts = s.split(':');
return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
}
}