呼出/通话中按钮、计时器、底色、60s 未接与进出场动效对齐视觉规范。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
316 lines
10 KiB
Dart
316 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/livekit_call_config.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(inviteTimeout: LiveKitCallConfig.inviteTimeout);
|
|
|
|
@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();
|
|
}
|
|
}
|
|
}
|