feat(mobile-next): 导入官方 openim-flutter-demo 固定基线 3.8.3-patch.3 并完成双构建

- 上游: github.com/OpenIMSDK/openim-flutter-demo tag 3.8.3-patch.3
  commit b3dfdb1e8aaeaaf6f0793e10cadd20d5c184a31f
- 并行目录 mobile-next/ 原样导入(含 LICENSE),不覆盖现网 mobile/
- 锁定 Flutter 3.24.5 / Dart 3.5.4 / JDK 17 / Gradle 7.6.3 / AGP 7.3.1
- 实测 Android debug 与 release 构建均成功(见 docs/mobile-next-baseline-import.md)
- 本提交可单独回退:git revert 1db1229(重写前)

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
编码工程师
2026-08-19 15:24:33 +08:00
co-authored by multica-agent
parent fa8584a73f
commit 8046ec7483
767 changed files with 57658 additions and 0 deletions
@@ -0,0 +1,288 @@
import 'dart:io';
import 'package:audio_session/audio_session.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_new_badger/flutter_new_badger.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart' as im;
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:openim/core/im_callback.dart';
import 'package:openim_common/openim_common.dart';
import 'package:sound_mode/sound_mode.dart';
import 'package:sound_mode/utils/ringer_mode_statuses.dart';
import 'package:vibration/vibration.dart';
import '../../utils/upgrade_manager.dart';
import 'im_controller.dart';
class AppController extends GetxController with UpgradeManger {
var isRunningBackground = false;
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final initializationSettingsAndroid = const AndroidInitializationSettings('@mipmap/ic_launcher');
final DarwinInitializationSettings initializationSettingsDarwin = const DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
RTCBridge? get rtcBridge => PackageBridge.rtcBridge;
bool get shouldMuted =>
rtcBridge?.hasConnection == true ||
Get.find<IMController>().imSdkStatusSubject.values.last.status != IMSdkStatus.syncEnded;
final _ring = 'assets/audio/message_ring.wav';
final _audioPlayer = AudioPlayer();
final configuration = const AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.ambient,
avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.mixWithOthers,
androidAudioFocusGainType: AndroidAudioFocusGainType.gainTransientMayDuck,
androidAudioAttributes: AndroidAudioAttributes(
contentType: AndroidAudioContentType.sonification,
usage: AndroidAudioUsage.notification,
),
);
late AudioSession session;
late BaseDeviceInfo deviceInfo;
final clientConfigMap = <String, dynamic>{}.obs;
Future<void> runningBackground(bool run) async {
Logger.print('-----App running background : $run-------------');
if (isRunningBackground && !run) {}
isRunningBackground = run;
if (!run) {
_cancelAllNotifications();
}
}
@override
void onInit() async {
_initPlayer();
final initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (notificationResponse) {},
);
autoCheckVersionUpgrade();
super.onInit();
}
void _requestPermissions() {
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
Future<void> showNotification(im.Message message, {bool showNotification = true}) async {
if (_isGlobalNotDisturb() ||
message.attachedInfoElem?.notSenderNotificationPush == true ||
message.contentType == im.MessageType.typing ||
message.sendID == OpenIM.iMManager.userID ||
(message.contentType! >= 1000 && message.contentType != 1400)) return;
var sourceID = message.sessionType == ConversationType.single ? message.sendID : message.groupID;
if (sourceID != null && message.sessionType != null) {
var i = await OpenIM.iMManager.conversationManager.getOneConversation(
sourceID: sourceID,
sessionType: message.sessionType!,
);
if (i.recvMsgOpt != 0) return;
}
if (showNotification) {
promptSoundOrNotification(message.seq!);
}
}
Future<void> promptSoundOrNotification(int seq) async {
if (Get.find<IMController>().imSdkStatusSubject.values.lastOrNull?.status != IMSdkStatus.syncEnded) {
return;
}
if (!isRunningBackground) {
_playMessageSound();
} else {
if (Platform.isAndroid) {
final id = seq;
const androidPlatformChannelSpecifics = AndroidNotificationDetails('chat', 'OpenIM聊天消息',
channelDescription: '来自OpenIM的信息', importance: Importance.max, priority: Priority.high, ticker: 'ticker');
const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(id, '您收到了一条新消息', '消息内容:.....', platformChannelSpecifics,
payload: '');
}
}
}
Future<void> _cancelAllNotifications() async {
await flutterLocalNotificationsPlugin.cancelAll();
}
Future<void> _startForegroundService() async {
await getAppInfo();
const androidPlatformChannelSpecifics = AndroidNotificationDetails('pro', 'OpenIM后台进程',
channelDescription: '保证app能收到信息', importance: Importance.max, priority: Priority.high, ticker: 'ticker');
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.startForegroundService(1, packageInfo!.appName, '正在运行...',
notificationDetails: androidPlatformChannelSpecifics, payload: '');
}
Future<void> _stopForegroundService() async {
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.stopForegroundService();
}
void showBadge(count) {
OpenIM.iMManager.messageManager.setAppBadge(count);
if (count == 0) {
removeBadge();
PushController.resetBadge();
} else {
FlutterNewBadger.setBadge(count);
PushController.setBadge(count);
}
}
void removeBadge() {
FlutterNewBadger.removeBadge();
}
@override
void onClose() {
closeSubject();
_audioPlayer.dispose();
super.onClose();
}
Locale? getLocale() {
var local = Get.locale;
var index = DataSp.getLanguage() ?? 0;
switch (index) {
case 1:
local = const Locale('zh', 'CN');
break;
case 2:
local = const Locale('en', 'US');
break;
}
return local;
}
@override
void onReady() {
queryClientConfig();
_getDeviceInfo();
_cancelAllNotifications();
super.onReady();
}
bool _isGlobalNotDisturb() {
bool isRegistered = Get.isRegistered<IMController>();
if (isRegistered) {
var logic = Get.find<IMController>();
return logic.userInfo.value.globalRecvMsgOpt == 2;
}
return false;
}
void _initPlayer() async {
session = await AudioSession.instance;
await session.configure(configuration);
_audioPlayer.setAsset(_ring, package: 'openim_common');
_audioPlayer.playerStateStream.listen((state) {
switch (state.processingState) {
case ProcessingState.idle:
case ProcessingState.loading:
case ProcessingState.buffering:
case ProcessingState.ready:
break;
case ProcessingState.completed:
_stopMessageSound();
break;
}
});
}
void _playMessageSound() async {
if (shouldMuted) {
return;
}
bool isRegistered = Get.isRegistered<IMController>();
bool isAllowVibration = true;
bool isAllowBeep = true;
if (isRegistered) {
var logic = Get.find<IMController>();
isAllowVibration = logic.userInfo.value.allowVibration == 1;
isAllowBeep = logic.userInfo.value.allowBeep == 1;
}
RingerModeStatus ringerStatus = await SoundMode.ringerModeStatus;
Logger.print('System ringer status: $ringerStatus, user is allow beep: $isAllowBeep',
fileName: 'app_controller.dart');
if (!_audioPlayer.playerState.playing &&
isAllowBeep &&
(ringerStatus == RingerModeStatus.normal || ringerStatus == RingerModeStatus.unknown)) {
await session.setActive(true);
_audioPlayer.setAsset(_ring, package: 'openim_common');
_audioPlayer.setLoopMode(LoopMode.off);
_audioPlayer.setVolume(1.0);
_audioPlayer.play();
}
if (isAllowVibration &&
(ringerStatus == RingerModeStatus.normal ||
ringerStatus == RingerModeStatus.vibrate ||
ringerStatus == RingerModeStatus.unknown)) {
if (await Vibration.hasVibrator() == true) {
Vibration.vibrate();
}
}
}
void _stopMessageSound() async {
if (_audioPlayer.playerState.playing) {
_audioPlayer.stop();
}
await session.setActive(false);
}
void _getDeviceInfo() async {
final deviceInfoPlugin = DeviceInfoPlugin();
deviceInfo = await deviceInfoPlugin.deviceInfo;
}
Future queryClientConfig() async {
final map = await Apis.getClientConfig();
clientConfigMap.assignAll(map);
return clientConfigMap;
}
}
@@ -0,0 +1,214 @@
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 '../im_callback.dart';
class IMController extends GetxController with IMCallback, OpenIMLive {
late Rx<UserFullInfo> userInfo;
late String atAllTag;
@override
void onClose() {
super.close();
onCloseLive();
super.onClose();
}
@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) {
final signaling = SignalingInfo(invitation: InvitationInfo.fromJson(map['data']));
signaling.userID = signaling.invitation?.inviterUserID;
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();
}
}
}