Browse Source
feat(notification): 添加本地通知功能支持
feat(notification): 添加本地通知功能支持
- 在 AndroidManifest.xml 中添加 RECEIVE_BOOT_COMPLETED 权限和启动器徽章权限 - 为应用启动器 Activity 添加 showWhenLocked 和 turnScreenOn 属性 - 集成 flutter_local_notifications 插件并配置 Android 和 iOS 平台设置 - 创建 LocalNotificationService 服务处理本地通知的初始化和显示 - 实现消息类型判断和内容解析功能 - 添加视频通话通知的特殊处理逻辑 - 支持通知点击跳转到对应聊天页面 - 在 IMManager 中集成本地通知服务 - 优化 iOS 平台通知权限申请 - 配置 Podfile 依赖并更新原生项目设置master
6 changed files with 358 additions and 12 deletions
Unified View
Diff Options
-
12android/app/src/main/AndroidManifest.xml
-
18ios/Podfile.lock
-
8ios/Runner.xcodeproj/project.pbxproj
-
3ios/Runner/AppDelegate.swift
-
14lib/im/im_manager.dart
-
315lib/service/local_notification_service.dart
@ -0,0 +1,315 @@ |
|||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:im_flutter_sdk/im_flutter_sdk.dart'; |
||||
|
import '../pages/message/chat_page.dart'; |
||||
|
import '../controller/message/conversation_controller.dart'; |
||||
|
|
||||
|
/// 本地通知服务 |
||||
|
class LocalNotificationService { |
||||
|
// 单例模式 |
||||
|
static final LocalNotificationService _instance = LocalNotificationService._internal(); |
||||
|
factory LocalNotificationService() => _instance; |
||||
|
static LocalNotificationService get instance => _instance; |
||||
|
|
||||
|
final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); |
||||
|
bool _notificationsInitialized = false; |
||||
|
static int _notificationId = 0; // 通知ID计数器 |
||||
|
|
||||
|
LocalNotificationService._internal(); |
||||
|
|
||||
|
/// 初始化本地通知 |
||||
|
Future<void> initialize() async { |
||||
|
if (_notificationsInitialized) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// Android 初始化设置 |
||||
|
const AndroidInitializationSettings initializationSettingsAndroid = |
||||
|
AndroidInitializationSettings('@mipmap/ic_launcher'); |
||||
|
|
||||
|
// iOS 初始化设置 |
||||
|
const DarwinInitializationSettings initializationSettingsIOS = |
||||
|
DarwinInitializationSettings( |
||||
|
requestAlertPermission: true, |
||||
|
requestBadgePermission: true, |
||||
|
requestSoundPermission: true, |
||||
|
); |
||||
|
|
||||
|
// 初始化设置 |
||||
|
const InitializationSettings initializationSettings = |
||||
|
InitializationSettings( |
||||
|
android: initializationSettingsAndroid, |
||||
|
iOS: initializationSettingsIOS, |
||||
|
); |
||||
|
|
||||
|
// 初始化插件 |
||||
|
final bool? initialized = await _localNotifications.initialize( |
||||
|
initializationSettings, |
||||
|
onDidReceiveNotificationResponse: _onNotificationTapped, |
||||
|
); |
||||
|
|
||||
|
if (initialized == true) { |
||||
|
_notificationsInitialized = true; |
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('✅ [LocalNotificationService] 本地通知初始化成功'); |
||||
|
} |
||||
|
} else { |
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('⚠️ [LocalNotificationService] 本地通知初始化失败'); |
||||
|
} |
||||
|
} |
||||
|
} catch (e) { |
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('❌ [LocalNotificationService] 本地通知初始化异常: $e'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 通知点击回调 |
||||
|
void _onNotificationTapped(NotificationResponse response) { |
||||
|
try { |
||||
|
final payload = response.payload; |
||||
|
if (payload != null && payload.isNotEmpty) { |
||||
|
// payload 格式: "type|fromId" |
||||
|
final parts = payload.split('|'); |
||||
|
if (parts.length >= 2) { |
||||
|
final fromId = parts[1]; |
||||
|
// 跳转到聊天页面 |
||||
|
Get.to(() => ChatPage( |
||||
|
userId: fromId, |
||||
|
userData: null, |
||||
|
)); |
||||
|
} |
||||
|
} |
||||
|
} catch (e) { |
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('⚠️ [LocalNotificationService] 处理通知点击失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取消息内容文本 |
||||
|
String _getMessageContent(EMMessage message) { |
||||
|
try { |
||||
|
if (message.body.type == MessageType.TXT) { |
||||
|
final body = message.body as EMTextMessageBody; |
||||
|
return body.content ?? ''; |
||||
|
} else if (message.body.type == MessageType.IMAGE) { |
||||
|
return '[图片]'; |
||||
|
} else if (message.body.type == MessageType.VOICE) { |
||||
|
return '[语音]'; |
||||
|
} else if (message.body.type == MessageType.VIDEO) { |
||||
|
return '[视频]'; |
||||
|
} else if (message.body.type == MessageType.FILE) { |
||||
|
return '[文件]'; |
||||
|
} else if (message.body.type == MessageType.LOCATION) { |
||||
|
return '[位置]'; |
||||
|
} else if (message.body.type == MessageType.CUSTOM) { |
||||
|
final body = message.body as EMCustomMessageBody; |
||||
|
if (body.event == 'live_room_invite') { |
||||
|
return '[分享房间]'; |
||||
|
} else if (body.event == 'gift') { |
||||
|
return '[礼物]'; |
||||
|
} else if (body.event == 'call') { |
||||
|
// 解析通话类型 |
||||
|
try { |
||||
|
if (body.params != null) { |
||||
|
final callType = body.params!['callType'] ?? 'voice'; |
||||
|
if (callType == 'video') { |
||||
|
return '[视频通话]'; |
||||
|
} else if (callType == 'voice') { |
||||
|
return '[语音通话]'; |
||||
|
} |
||||
|
} |
||||
|
} catch (e) { |
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('⚠️ [LocalNotificationService] 解析通话消息类型失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
return '[通话消息]'; |
||||
|
} |
||||
|
return '[自定义消息]'; |
||||
|
} |
||||
|
} catch (e) { |
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('⚠️ [LocalNotificationService] 获取消息内容失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
/// 显示本地通知(当 APP 在后台时) |
||||
|
Future<void> showNotification(EMMessage message, {Set<String>? activeChatUserIds}) async { |
||||
|
if (!_notificationsInitialized) { |
||||
|
await initialize(); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// 获取消息发送者ID |
||||
|
final fromId = message.from; |
||||
|
if (fromId == null || fromId.isEmpty) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 检查发送者是否是当前正在聊天的用户 |
||||
|
if (activeChatUserIds != null && activeChatUserIds.contains(fromId)) { |
||||
|
// 是当前正在聊天的用户,不显示通知 |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 获取消息内容 |
||||
|
String messageContent = _getMessageContent(message); |
||||
|
if (messageContent.isEmpty) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 从消息扩展字段中获取用户信息(头像、昵称) |
||||
|
Map<String, dynamic>? attributes; |
||||
|
try { |
||||
|
attributes = message.attributes; |
||||
|
} catch (e) { |
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('⚠️ [LocalNotificationService] 无法访问消息扩展字段: $e'); |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
String? nickName; |
||||
|
String? avatarUrl; |
||||
|
|
||||
|
if (attributes != null) { |
||||
|
// 从扩展字段中获取发送者信息 |
||||
|
nickName = attributes['sender_nickName'] as String?; |
||||
|
avatarUrl = attributes['sender_avatarUrl'] as String?; |
||||
|
} |
||||
|
|
||||
|
// 如果从消息扩展字段中获取不到,尝试从 ConversationController 的缓存中获取 |
||||
|
if ((nickName == null || nickName.isEmpty) || (avatarUrl == null || avatarUrl.isEmpty)) { |
||||
|
try { |
||||
|
if (Get.isRegistered<ConversationController>()) { |
||||
|
final conversationController = Get.find<ConversationController>(); |
||||
|
final cachedUserInfo = conversationController.getCachedUserInfo(fromId); |
||||
|
if (cachedUserInfo != null) { |
||||
|
nickName = nickName ?? cachedUserInfo.nickName; |
||||
|
avatarUrl = avatarUrl ?? cachedUserInfo.avatarUrl; |
||||
|
} |
||||
|
} |
||||
|
} catch (e) { |
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('⚠️ [LocalNotificationService] 从ConversationController获取用户信息失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 使用默认值 |
||||
|
final finalNickName = nickName ?? fromId; |
||||
|
|
||||
|
// 处理视频通话消息 |
||||
|
Map<String, dynamic>? callInfo; |
||||
|
String? callType; |
||||
|
String? callStatus; |
||||
|
|
||||
|
try { |
||||
|
// 自定义消息 |
||||
|
if (message.body.type == MessageType.CUSTOM) { |
||||
|
final customBody = message.body as EMCustomMessageBody; |
||||
|
if (customBody.event == 'call' && customBody.params != null) { |
||||
|
final params = customBody.params!; |
||||
|
callType = params['callType'] ?? 'voice'; |
||||
|
callStatus = params['callStatus'] ?? 'missed'; |
||||
|
callInfo = { |
||||
|
'callType': callType, |
||||
|
'callStatus': callStatus, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 如果是视频通话且状态为 missed 或 calling |
||||
|
if (callInfo != null && callType != null && callStatus != null) { |
||||
|
if (callType == 'video' && (callStatus == 'missed' || callStatus == 'calling')) { |
||||
|
// 显示视频通话通知 |
||||
|
final notificationId = _notificationId++; |
||||
|
const AndroidNotificationDetails androidPlatformChannelSpecifics = |
||||
|
AndroidNotificationDetails( |
||||
|
'video_call_channel', |
||||
|
'视频通话', |
||||
|
channelDescription: '视频通话通知', |
||||
|
importance: Importance.max, |
||||
|
priority: Priority.high, |
||||
|
showWhen: true, |
||||
|
); |
||||
|
const DarwinNotificationDetails iOSPlatformChannelSpecifics = |
||||
|
DarwinNotificationDetails( |
||||
|
presentAlert: true, |
||||
|
presentBadge: true, |
||||
|
presentSound: true, |
||||
|
); |
||||
|
const NotificationDetails platformChannelSpecifics = |
||||
|
NotificationDetails( |
||||
|
android: androidPlatformChannelSpecifics, |
||||
|
iOS: iOSPlatformChannelSpecifics, |
||||
|
); |
||||
|
|
||||
|
await _localNotifications.show( |
||||
|
notificationId, |
||||
|
'视频通话', |
||||
|
'$finalNickName 邀请您进行视频通话', |
||||
|
platformChannelSpecifics, |
||||
|
payload: 'video_call|$fromId', |
||||
|
); |
||||
|
|
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('📱 [LocalNotificationService] 显示视频通话本地通知: $fromId'); |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
} catch (e) { |
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('⚠️ [LocalNotificationService] 处理视频通话通知失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 显示普通消息通知 |
||||
|
final notificationId = _notificationId++; |
||||
|
const AndroidNotificationDetails androidPlatformChannelSpecifics = |
||||
|
AndroidNotificationDetails( |
||||
|
'message_channel', |
||||
|
'消息通知', |
||||
|
channelDescription: '新消息通知', |
||||
|
importance: Importance.high, |
||||
|
priority: Priority.high, |
||||
|
showWhen: true, |
||||
|
); |
||||
|
const DarwinNotificationDetails iOSPlatformChannelSpecifics = |
||||
|
DarwinNotificationDetails( |
||||
|
presentAlert: true, |
||||
|
presentBadge: true, |
||||
|
presentSound: true, |
||||
|
); |
||||
|
const NotificationDetails platformChannelSpecifics = |
||||
|
NotificationDetails( |
||||
|
android: androidPlatformChannelSpecifics, |
||||
|
iOS: iOSPlatformChannelSpecifics, |
||||
|
); |
||||
|
|
||||
|
await _localNotifications.show( |
||||
|
notificationId, |
||||
|
finalNickName, |
||||
|
messageContent, |
||||
|
platformChannelSpecifics, |
||||
|
payload: 'message|$fromId', |
||||
|
); |
||||
|
|
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('📱 [LocalNotificationService] 显示本地通知: $fromId, $messageContent'); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
if (Get.isLogEnable) { |
||||
|
Get.log('❌ [LocalNotificationService] 显示本地通知失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
Write
Preview
Loading…
Cancel
Save