feat(mobile-next): 迁移 Android 一对一语音通话薄适配与入口

复用官方 OpenIMLive 信令与房间,换票改走账号服务 /api/rtc_token;入口仅音频,占线/超时/重复回调与销毁释放由公司层防护。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
编码工程师
2026-08-20 07:58:24 +08:00
co-authored by Cursor multica-agent
parent 2a817e77d8
commit 3832e080c4
19 changed files with 597 additions and 81 deletions
@@ -59,6 +59,9 @@ class AppController extends GetxController with UpgradeManger {
if (isRunningBackground && !run) {}
isRunningBackground = run;
if (Get.isRegistered<IMController>()) {
Get.find<IMController>().backgroundSubject.add(run);
}
if (!run) {
_cancelAllNotifications();
}
@@ -9,19 +9,113 @@ 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();
@@ -84,8 +178,14 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
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: