import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; import 'package:path_provider/path_provider.dart'; import '../config.dart'; import '../models/signaling.dart'; /// SDK 与服务器的连接状态 enum ConnectStatus { idle, connecting, success, failed } /// 通话信令的自定义消息类型(与官方样板工程保持一致,保证互通) class SignalingType { SignalingType._(); static const int callingInvite = 200; // 发起呼叫 static const int callingAccept = 201; // 接听 static const int callingReject = 202; // 拒接 static const int callingCancel = 203; // 取消呼叫 static const int callingHungup = 204; // 挂断 } /// OpenIM SDK 的统一封装:初始化、登录、监听、会话列表。 /// 用 ChangeNotifier 做状态分发,不引入额外状态管理框架。 class IMService extends ChangeNotifier { IMService._(); static final IMService instance = IMService._(); /// SDK 是否已初始化(App 启动时做一次) bool sdkReady = false; /// 与服务器的连接状态(断网横幅用) ConnectStatus connectStatus = ConnectStatus.idle; /// 最近一次连接失败的原始错误(诊断展示用) String? lastConnectError; /// 是否已登录 bool loggedIn = false; /// 会话首次同步中(消息列表的「正在加载」态) bool syncing = true; /// 会话同步失败(消息列表的「加载失败」态) bool syncFailed = false; /// 会话同步是否完成过至少一次(区分「正在加载」和「还没有消息」) bool conversationsLoaded = false; /// 会话列表(置顶在前,其余按最新消息时间倒序) List conversations = []; /// 当前登录用户信息 UserInfo? selfInfo; /// 当前登录凭证(发起通话取 LiveKit token 时要用) String? currentUserID; String? currentToken; /// 被踢下线 / token 失效时的回调(由 main.dart 设置:清缓存、回登录页) void Function()? onForceLogout; /// 收到新消息(聊天页订阅) final StreamController _newMsgController = StreamController.broadcast(); Stream get onNewMessage => _newMsgController.stream; /// 消息被撤回(撤回方的 clientMsgID,聊天页订阅) final StreamController _revokeController = StreamController.broadcast(); Stream get onMessageRevoked => _revokeController.stream; /// 收到通话信令(仅在线自定义消息,通话模块订阅) final StreamController _signalingController = StreamController.broadcast(); Stream get onSignaling => _signalingController.stream; int get platformID => Platform.isIOS ? IMPlatform.ios : IMPlatform.android; /// App 启动时初始化 SDK(只做一次)。 /// 用共享 Future 防止超时后重复触发 initSDK:第一次调用还在跑时, /// 后续调用直接复用同一个 Future;失败了则允许下次重试。 Future? _initFuture; Future init() { if (sdkReady) return Future.value(); final inFlight = _initFuture; if (inFlight != null) return inFlight; final future = _doInit(); _initFuture = future; return future.whenComplete(() { // 失败的初始化允许重试;成功的保留 sdkReady 标记即可 if (!sdkReady && identical(_initFuture, future)) _initFuture = null; }); } Future _doInit() async { final dir = await getApplicationDocumentsDirectory(); final dataDir = '${dir.path}/openim'; await Directory(dataDir).create(recursive: true); final ok = await OpenIM.iMManager.initSDK( platformID: platformID, apiAddr: apiAddr, wsAddr: wsAddr, dataDir: dataDir, logFilePath: dataDir, logLevel: 6, listener: OnConnectListener( onConnecting: () { connectStatus = ConnectStatus.connecting; notifyListeners(); }, onConnectFailed: (code, error) { connectStatus = ConnectStatus.failed; lastConnectError = '$code $error'; notifyListeners(); }, onConnectSuccess: () { connectStatus = ConnectStatus.success; lastConnectError = null; notifyListeners(); }, onKickedOffline: _forceLogout, onUserTokenExpired: _forceLogout, onUserTokenInvalid: _forceLogout, ), ); sdkReady = ok == true; _setBusinessListeners(); notifyListeners(); } void _setBusinessListeners() { OpenIM.iMManager.conversationManager.setConversationListener( OnConversationListener( onSyncServerStart: (reInstall) { syncing = true; syncFailed = false; notifyListeners(); }, onSyncServerFinish: (reInstall) { syncing = false; conversationsLoaded = true; refreshConversations(); }, onSyncServerFailed: (reInstall) { syncing = false; syncFailed = true; notifyListeners(); }, onConversationChanged: (list) => refreshConversations(), onNewConversation: (list) => refreshConversations(), onTotalUnreadMessageCountChanged: (count) => notifyListeners(), ), ); OpenIM.iMManager.messageManager.setAdvancedMsgListener( OnAdvancedMsgListener( onRecvNewMessage: (msg) { _newMsgController.add(msg); }, onRecvOfflineNewMessage: (msg) { _newMsgController.add(msg); }, onNewRecvMessageRevoked: (info) { if (info.clientMsgID != null) _revokeController.add(info.clientMsgID!); }, onRecvOnlineOnlyMessage: (msg) { // 通话信令走「仅在线」的自定义消息,转发给通话模块 if (msg.contentType == MessageType.custom) { _signalingController.add(msg); } }, ), ); // 好友与群变更只负责刷新界面(通讯录页直接监听 IMService) OpenIM.iMManager.friendshipManager.setFriendshipListener( OnFriendshipListener( onFriendAdded: (info) => notifyListeners(), onFriendDeleted: (info) => notifyListeners(), onFriendInfoChanged: (info) => notifyListeners(), onFriendApplicationAdded: (info) => notifyListeners(), onFriendApplicationAccepted: (info) => notifyListeners(), onFriendApplicationRejected: (info) => notifyListeners(), ), ); OpenIM.iMManager.groupManager.setGroupListener( OnGroupListener( onJoinedGroupAdded: (info) => notifyListeners(), onJoinedGroupDeleted: (info) => notifyListeners(), onGroupInfoChanged: (info) => notifyListeners(), ), ); } /// 登录(init 之后调用)。userID/token 来自公司账号登录接口。 Future login({required String userID, required String token}) async { await init(); if (!sdkReady) { // initSDK 返回 false:SDK 没初始化成功,直接登录会无限等待 throw StateError('消息组件初始化失败'); } final user = await OpenIM.iMManager.login( userID: userID, token: token, defaultValue: () async => UserInfo(userID: userID), ); currentUserID = userID; currentToken = token; selfInfo = user; loggedIn = true; notifyListeners(); } /// 刷新自己的资料(我的页面展示用) Future refreshSelfInfo() async { if (!loggedIn) return; try { selfInfo = await OpenIM.iMManager.userManager.getSelfUserInfo(); notifyListeners(); } catch (_) { // 拉取失败沿用内存里的旧数据 } } /// 退出登录 Future logout() async { try { await OpenIM.iMManager.logout(); } catch (_) { // 本地照常清理 } _resetLoginState(); } void _forceLogout() { _resetLoginState(); onForceLogout?.call(); } void _resetLoginState() { loggedIn = false; currentUserID = null; currentToken = null; selfInfo = null; conversations = []; conversationsLoaded = false; syncing = true; syncFailed = false; notifyListeners(); } /// 重新拉取全部会话并排序(置顶在前,其余按最新消息时间倒序) Future refreshConversations() async { if (!loggedIn) return; try { final list = await OpenIM.iMManager.conversationManager.getAllConversationList(); list.sort((a, b) { final ap = a.isPinned == true ? 0 : 1; final bp = b.isPinned == true ? 0 : 1; if (ap != bp) return ap - bp; return (b.latestMsgSendTime ?? 0).compareTo(a.latestMsgSendTime ?? 0); }); conversations = list; conversationsLoaded = true; notifyListeners(); } catch (_) { // 拉取失败保留现有列表 } } /// 进入聊天页后清除该会话未读 Future markConversationRead(String conversationID) async { try { await OpenIM.iMManager.conversationManager.markConversationMessageAsRead(conversationID: conversationID); } catch (_) { // 标记失败不影响聊天 } } /// 解析「仅在线」自定义消息里的通话信令,不是通话信令返回 null static SignalingPayload? parseSignaling(Message msg) { try { final data = msg.customElem?.data; if (data == null || data.isEmpty) return null; final map = jsonDecode(data) as Map; final customType = map['customType']; if (customType is! int || customType < 200 || customType > 204) return null; final payload = SignalingPayload( type: customType, invitation: InvitationInfo.fromJson(Map.from(map['data'] ?? {})), ); return payload; } catch (_) { return null; } } } /// 一条通话信令 class SignalingPayload { /// 见 [SignalingType] final int type; final InvitationInfo invitation; SignalingPayload({required this.type, required this.invitation}); /// 房间号 String? get roomID => invitation.roomID; /// 呼叫发起人 String? get inviterUserID => invitation.inviterUserID; }