Files
tongxunruanjian/mobile-next/lib/core/controller/im_controller.dart
T
3832e080c4 feat(mobile-next): 迁移 Android 一对一语音通话薄适配与入口
复用官方 OpenIMLive 信令与房间,换票改走账号服务 /api/rtc_token;入口仅音频,占线/超时/重复回调与销毁释放由公司层防护。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 07:58:24 +08:00

315 lines
10 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:get/get.dart';
import 'package:openim_common/openim_common.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:openim_live/openim_live.dart';
import '../../company/feature_flags.dart';
import '../../company/rtc/call_permissions.dart';
import '../../company/rtc/call_session_guard.dart';
import '../../company/rtc/rtc_token_api.dart';
import '../im_callback.dart';
class IMController extends GetxController with IMCallback, OpenIMLive {
late Rx<UserFullInfo> userInfo;
late String atAllTag;
final _rtcTokenApi = RtcTokenApi();
final callGuard = CallSessionGuard();
@override
void onClose() {
callGuard.dispose();
super.close();
onCloseLive();
super.onClose();
}
@override
Future<SignalingCertificate> getRtcCertificate(String roomID, String userID) {
final token = DataSp.imToken;
if (token == null || token.isEmpty) {
return Future.error(StateError('登录状态已失效,请重新登录'));
}
return _rtcTokenApi.getToken(room: roomID, identity: userID, authToken: token);
}
@override
void onLiveSessionClosed() {
callGuard.dispose();
}
@override
void onLiveConnected() {
callGuard.markConnected();
}
@override
void receiveNewInvitation(SignalingInfo info) {
if (!FeatureFlags.livekitCall) return;
final invitation = info.invitation;
final roomID = invitation?.roomID ?? '';
if (invitation?.mediaType != 'audio' || invitation?.sessionType != ConversationType.single) {
onTapReject(info);
return;
}
if (!callGuard.acceptSignaling(customType: CustomMessageType.callingInvite, roomID: roomID)) {
return;
}
if (isBusy || callGuard.isBusy) {
onTapReject(info);
return;
}
if (callGuard.startIncoming(roomID) != null) {
onTapReject(info);
return;
}
super.receiveNewInvitation(info);
}
@override
void call({
required CallObj callObj,
required CallType callType,
CallState callState = CallState.call,
String? roomID,
String? inviterUserID,
required List<String> inviteeUserIDList,
String? groupID,
SignalingCertificate? credentials,
}) async {
if (!FeatureFlags.livekitCall) return;
if (callType != CallType.audio || callObj != CallObj.single) {
IMViews.showToast('暂只支持一对一语音通话');
return;
}
if (isBusy || callGuard.isBusy) {
IMViews.showToast(StrRes.callingBusy);
return;
}
final granted = await CallPermissions.ensureMicrophone();
if (!granted) return;
callGuard.startOutgoing(roomID ?? '');
super.call(
callObj: callObj,
callType: callType,
callState: callState,
roomID: roomID,
inviterUserID: inviterUserID,
inviteeUserIDList: inviteeUserIDList,
groupID: groupID,
credentials: credentials,
);
}
@override
Future<SignalingCertificate> onTapPickup(SignalingInfo signaling) async {
final granted = await CallPermissions.ensureMicrophone();
if (!granted) {
await onTapReject(signaling);
return Future.error(StateError('microphone denied'));
}
return super.onTapPickup(signaling);
}
@override
void onInit() async {
super.onInit();
onInitLive();
WidgetsBinding.instance.addPostFrameCallback((_) => initOpenIM());
}
void initOpenIM() async {
final initialized = await OpenIM.iMManager.initSDK(
platformID: IMUtils.getPlatform(),
apiAddr: Config.imApiUrl,
wsAddr: Config.imWsUrl,
dataDir: Config.cachePath,
logLevel: Config.logLevel,
logFilePath: Config.cachePath,
listener: OnConnectListener(
onConnecting: () {
imSdkStatus(IMSdkStatus.connecting);
},
onConnectFailed: (code, error) {
imSdkStatus(IMSdkStatus.connectionFailed);
},
onConnectSuccess: () {
imSdkStatus(IMSdkStatus.connectionSucceeded);
},
onKickedOffline: kickedOffline,
onUserTokenExpired: kickedOffline,
onUserTokenInvalid: userTokenInvalid,
),
);
OpenIM.iMManager
..setUploadLogsListener(OnUploadLogsListener(onUploadProgress: uploadLogsProgress))
..userManager.setUserListener(OnUserListener(
onSelfInfoUpdated: (u) {
selfInfoUpdated(u);
userInfo.update((val) {
val?.nickname = u.nickname;
val?.faceURL = u.faceURL;
val?.remark = u.remark;
val?.ex = u.ex;
val?.globalRecvMsgOpt = u.globalRecvMsgOpt;
});
},
onUserStatusChanged: userStausChanged))
..messageManager.setAdvancedMsgListener(OnAdvancedMsgListener(
onRecvC2CReadReceipt: recvC2CMessageReadReceipt,
onRecvNewMessage: recvNewMessage,
onNewRecvMessageRevoked: recvMessageRevoked,
onRecvOfflineNewMessage: recvOfflineMessage,
onRecvOnlineOnlyMessage: (msg) {
if (msg.isCustomType) {
final data = msg.customElem!.data;
final map = jsonDecode(data!);
final customType = map['customType'];
if (customType == CustomMessageType.callingInvite ||
customType == CustomMessageType.callingAccept ||
customType == CustomMessageType.callingReject ||
customType == CustomMessageType.callingCancel ||
customType == CustomMessageType.callingHungup) {
if (!FeatureFlags.livekitCall) return;
final signaling = SignalingInfo(invitation: InvitationInfo.fromJson(map['data']));
signaling.userID = signaling.invitation?.inviterUserID;
final roomID = signaling.invitation?.roomID ?? '';
if (customType != CustomMessageType.callingInvite &&
!callGuard.acceptSignaling(customType: customType as int, roomID: roomID)) {
return;
}
switch (customType) {
case CustomMessageType.callingInvite:
receiveNewInvitation(signaling);
break;
case CustomMessageType.callingAccept:
inviteeAccepted(signaling);
break;
case CustomMessageType.callingReject:
inviteeRejected(signaling);
break;
case CustomMessageType.callingCancel:
invitationCancelled(signaling);
break;
case CustomMessageType.callingHungup:
beHangup(signaling);
break;
}
}
}
},
))
..messageManager.setMsgSendProgressListener(OnMsgSendProgressListener(
onProgress: progressCallback,
))
..messageManager.setCustomBusinessListener(OnCustomBusinessListener(
onRecvCustomBusinessMessage: recvCustomBusinessMessage,
))
..friendshipManager.setFriendshipListener(OnFriendshipListener(
onBlackAdded: blacklistAdded,
onBlackDeleted: blacklistDeleted,
onFriendApplicationAccepted: friendApplicationAccepted,
onFriendApplicationAdded: friendApplicationAdded,
onFriendApplicationDeleted: friendApplicationDeleted,
onFriendApplicationRejected: friendApplicationRejected,
onFriendInfoChanged: friendInfoChanged,
onFriendAdded: friendAdded,
onFriendDeleted: friendDeleted,
))
..conversationManager.setConversationListener(OnConversationListener(
onConversationChanged: conversationChanged,
onNewConversation: newConversation,
onTotalUnreadMessageCountChanged: totalUnreadMsgCountChanged,
onInputStatusChanged: inputStateChanged,
onSyncServerFailed: (reInstall) {
imSdkStatus(IMSdkStatus.syncFailed, reInstall: reInstall ?? false);
},
onSyncServerFinish: (reInstall) {
imSdkStatus(IMSdkStatus.syncEnded, reInstall: reInstall ?? false);
if (Platform.isAndroid) {
Permissions.request([Permission.systemAlertWindow]);
}
},
onSyncServerStart: (reInstall) {
imSdkStatus(IMSdkStatus.syncStart, reInstall: reInstall ?? false);
},
onSyncServerProgress: (progress) {
imSdkStatus(IMSdkStatus.syncProgress, progress: progress);
}))
..groupManager.setGroupListener(OnGroupListener(
onGroupApplicationAccepted: groupApplicationAccepted,
onGroupApplicationAdded: groupApplicationAdded,
onGroupApplicationDeleted: groupApplicationDeleted,
onGroupApplicationRejected: groupApplicationRejected,
onGroupInfoChanged: groupInfoChanged,
onGroupMemberAdded: groupMemberAdded,
onGroupMemberDeleted: groupMemberDeleted,
onGroupMemberInfoChanged: groupMemberInfoChanged,
onJoinedGroupAdded: joinedGroupAdded,
onJoinedGroupDeleted: joinedGroupDeleted,
));
initializedSubject.sink.add(initialized);
}
Future login(String userID, String token) async {
try {
var user = await OpenIM.iMManager.login(
userID: userID,
token: token,
defaultValue: () async => UserInfo(userID: userID),
);
ApiService().setToken(token);
userInfo = UserFullInfo.fromJson(user.toJson()).obs;
_queryMyFullInfo();
_queryAtAllTag();
} catch (e, s) {
Logger.print('e: $e s:$s');
await _handleLoginRepeatError(e);
return Future.error(e, s);
}
}
Future logout() {
return OpenIM.iMManager.logout();
}
void _queryAtAllTag() async {
atAllTag = OpenIM.iMManager.conversationManager.atAllTag;
}
void _queryMyFullInfo() async {
final data = await Apis.queryMyFullInfo();
if (data is UserFullInfo) {
userInfo.update((val) {
val?.allowAddFriend = data.allowAddFriend;
val?.allowBeep = data.allowBeep;
val?.allowVibration = data.allowVibration;
val?.nickname = data.nickname;
val?.faceURL = data.faceURL;
val?.phoneNumber = data.phoneNumber;
val?.email = data.email;
val?.birth = data.birth;
val?.gender = data.gender;
});
}
}
_handleLoginRepeatError(e) async {
if (e is PlatformException && (e.code == "13002" || e.code == '1507')) {
await logout();
await DataSp.removeLoginCertificate();
}
}
}