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
Split 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