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 01:10:39 +08:00
co-authored by multica-agent
parent fa8584a73f
commit 0a2fbac92a
767 changed files with 57658 additions and 0 deletions
@@ -0,0 +1,125 @@
import 'dart:convert';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:openim_common/openim_common.dart';
import 'package:uuid/uuid.dart';
class CacheController extends GetxController {
final favoriteList = <EmojiInfo>[].obs;
final callRecordList = <CallRecords>[].obs;
Box? favoriteBox;
Box? callRecordBox;
bool _isInitFavoriteList = false;
bool _isInitCallRecords = false;
String get userID => DataSp.getLoginCertificate()!.userID;
void addFavoriteFromUrl(String? url, int? width, int? height) {
var emoji = EmojiInfo(url: url, width: width, height: height);
favoriteList.insert(0, emoji);
final list = favoriteList.value;
favoriteBox?.put(userID, list);
}
void addFavoriteFromPath(String path, int width, int height) async {
var result = await LoadingView.singleton.wrap(
asyncFunction: () => OpenIM.iMManager.uploadFile(
id: const Uuid().v4(),
filePath: path,
fileName: path,
),
);
if (result is String) {
final url = jsonDecode(result)['url'];
Logger.print('url:$url');
var emoji = EmojiInfo(url: url, width: width, height: height);
Logger.print('addFavoriteFromPath :$url');
favoriteList.insert(0, emoji);
favoriteBox?.put(userID, favoriteList.value);
}
}
void delFavorite(String url) {
favoriteList.removeWhere((element) => element.url == url);
favoriteBox?.put(userID, favoriteList.value);
}
void delFavoriteList(List<String> urlList) {
for (final url in urlList) {
favoriteList.removeWhere((element) => element.url == url);
}
favoriteBox?.put(userID, favoriteList.value);
}
initFavoriteEmoji() {
if (!_isInitFavoriteList) {
_isInitFavoriteList = true;
var list = favoriteBox?.get(userID, defaultValue: <EmojiInfo>[]);
if (list != null) {
favoriteList.assignAll((list as List).cast());
}
}
}
List<String> get urlList => favoriteList.map((e) => e.url!).toList();
initCallRecords() {
if (!_isInitCallRecords) {
_isInitCallRecords = true;
var list = callRecordBox?.get(userID, defaultValue: <CallRecords>[]);
if (list != null) {
callRecordList.assignAll((list as List).cast());
}
}
}
void resetCache() {
if (_isInitCallRecords) {
callRecordList.value = [];
final list = callRecordBox?.get(userID, defaultValue: <CallRecords>[]);
if (list != null) {
callRecordList.assignAll((list as List).cast());
}
}
if (_isInitFavoriteList) {
favoriteList.value = [];
final list = favoriteBox?.get(userID, defaultValue: <CallRecords>[]);
if (list != null) {
favoriteList.assignAll((list as List).cast());
}
}
}
addCallRecords(CallRecords records) {
callRecordList.insert(0, records);
callRecordBox?.put(userID, callRecordList.value);
}
deleteCallRecords(CallRecords records) async {
callRecordList.removeWhere((element) => element.userID == records.userID && element.date == records.date);
await callRecordBox?.put(userID, callRecordList.value);
}
@override
void onClose() {
_isInitFavoriteList = false;
_isInitCallRecords = false;
Hive.close();
super.onClose();
}
@override
void onInit() async {
Hive.registerAdapter(EmojiInfoAdapter());
Hive.registerAdapter(CallRecordsAdapter());
favoriteBox = await Hive.openBox<List>('favoriteEmoji');
callRecordBox = await Hive.openBox<List>('callRecords');
super.onInit();
}
}
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_download_manager/flutter_download_manager.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:get/get.dart';
import 'package:openim_common/openim_common.dart';
class DownloadController extends GetxController {
final downloadManager = DownloadManager();
String? savedDir;
final downloadTaskList = <String?, DownloadTask>{}.obs;
@override
void onInit() {
_initDir();
super.onInit();
}
_initDir() async {
savedDir ??= await IMUtils.getDownloadFileDir();
}
DownloadTask? getTask(String url) {
return downloadManager.getDownload(url);
}
bool isExistTask(String url) {
return null != downloadTaskList[url];
}
bool isExistMessageTask(Message message) =>
message.isFileType && null != message.fileElem?.sourceUrl && isExistTask(message.fileElem!.sourceUrl!);
ValueNotifier<DownloadStatus> getStatus(Message message) => downloadTaskList[message.fileElem!.sourceUrl!]!.status;
ValueNotifier<double> getProgress(Message message) => downloadTaskList[message.fileElem!.sourceUrl!]!.progress;
void addDownload(String url, {String? path}) {
Permissions.storage(() async {
await _initDir();
path ??= "$savedDir/${downloadManager.getFileNameFromUrl(url)}";
DownloadTask? task = await downloadManager.addDownload(url, path!);
if (null != task) downloadTaskList[url] = task;
});
}
void addDownloadForMessage(Message message, {String? path}) {
if (message.isFileType) {
final url = message.fileElem?.sourceUrl;
if (null != url) {
Permissions.storage(() async {
await _initDir();
path ??= "$savedDir/${downloadManager.getFileNameFromUrl(url)}";
DownloadTask? task = await downloadManager.addDownload(url, path!);
if (null != task) downloadTaskList[url] = task;
});
}
}
}
void clickFileMessage(String url, String path) async {
var task = getTask(url);
Logger.print(
'clickFileMessage 当前状态: ${task?.status.value} 进度:${task?.progress.value} 完成:${task?.status.value.isCompleted} $url $path');
if (task != null && !task.status.value.isCompleted) {
switch (task.status.value) {
case DownloadStatus.downloading:
downloadManager.pauseDownload(url);
break;
case DownloadStatus.paused:
downloadManager.resumeDownload(url);
break;
case DownloadStatus.queued:
break;
case DownloadStatus.completed:
break;
case DownloadStatus.failed:
await downloadManager.removeDownload(url);
addDownload(url, path: path);
break;
case DownloadStatus.canceled:
addDownload(url, path: path);
break;
}
} else {
addDownload(url, path: path);
}
}
}
@@ -0,0 +1,55 @@
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform;
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyCw-ohvxFisvm3Yrb4kCzPBuL0YfZkzI9Q',
appId: '1:299075855003:android:292449dc34ad498109dc3e',
messagingSenderId: '299075855003',
projectId: 'im-fer-c3347',
storageBucket: 'im-fer-c3347.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyC0BhV-mo84LvOuqWf2bS966jlf0f8mh74',
appId: '1:299075855003:ios:23b06300e33d984309dc3e',
messagingSenderId: '299075855003',
projectId: 'im-fer-c3347',
storageBucket: 'im-fer-c3347.firebasestorage.app',
iosBundleId: 'io.openim.flutter.full.NotificationService',
);
}
@@ -0,0 +1,234 @@
import 'dart:async';
import 'dart:io';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:get/get.dart';
import 'package:getuiflut/getuiflut.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:google_api_availability/google_api_availability.dart';
import 'package:openim_common/openim_common.dart';
import 'firebase_options.dart';
enum PushType { getui, FCM }
const appID = 'your-app-id';
const appKey = 'your-app-key';
const appSecret = 'your-app-secret';
class PushController extends GetxService {
PushType pushType = PushType.getui;
@override
void onInit() {
super.onInit();
if (PushController().pushType == PushType.getui) {
GetuiPushController()._addEventHandler();
GetuiPushController()._initialize();
}
}
/// Logs in the user with the specified alias to the push notification service.
///
/// Depending on the push type configured, it either logs in using the Getui or
/// FCM push service.
///
/// If using Getui, it binds the alias to the Getui service.
///
/// If using FCM, it listens for token refresh events and logs in, invoking the
/// provided callback with the new token.
///
/// Throws an assertion error if the FCM push type is selected but the
/// `onTokenRefresh` callback is not provided.
///
/// - Parameters:
/// - alias: The alias to bind to the push notification service for getui.
/// - onTokenRefresh: A callback function that is invoked with the refreshed
/// token when using FCM. Required if the push type is FCM.
static void login(String alias, {void Function(String token)? onTokenRefresh}) {
assert((PushController().pushType == PushType.FCM && onTokenRefresh != null) ||
(PushController().pushType == PushType.getui && alias.isNotEmpty));
if (PushController().pushType == PushType.getui) {
GetuiPushController()._login(alias);
} else {
FCMPushController()._initialize().then((_) {
FCMPushController()._getToken().then((token) => onTokenRefresh!(token));
FCMPushController()._listenToTokenRefresh((token) => onTokenRefresh);
});
}
}
static void logout() {
if (PushController().pushType == PushType.getui) {
GetuiPushController()._logout();
} else {
FCMPushController()._deleteToken();
}
}
static void setBadge(int badge) {
if (PushController().pushType == PushType.getui) {
GetuiPushController()._setBadge(badge);
}
}
static void resetBadge() {
if (PushController().pushType == PushType.getui) {
GetuiPushController()._resetBadge();
}
}
}
class GetuiPushController {
static final GetuiPushController _instance = GetuiPushController._();
factory GetuiPushController() => _instance;
GetuiPushController._();
Future<void> _initialize() async {
Permissions.notification().then((isGranted) {
if (isGranted) {
try {
Getuiflut.initGetuiSdk;
} catch (e) {
e.toString();
}
}
});
}
void _addEventHandler() {
if (Platform.isIOS) {
Getuiflut().startSdk(
appId: appID,
appKey: appKey,
appSecret: appSecret,
);
Getuiflut().runBackgroundEnable(0);
}
Getuiflut().addEventHandler(
onReceiveClientId: (String message) async {
print("flutter onReceiveClientId: $message");
},
onRegisterDeviceToken: (String message) async {
print("flutter onRegisterDeviceToken: $message");
},
onReceivePayload: (Map<String, dynamic> message) async {},
onReceiveNotificationResponse: (Map<String, dynamic> message) async {},
onAppLinkPayload: (String message) async {},
onReceiveOnlineState: (bool online) async {},
onPushModeResult: (Map<String, dynamic> message) async {},
onSetTagResult: (Map<String, dynamic> message) async {},
onAliasResult: (Map<String, dynamic> message) async {},
onQueryTagResult: (Map<String, dynamic> message) async {},
onWillPresentNotification: (Map<String, dynamic> message) async {},
onOpenSettingsForNotification: (Map<String, dynamic> message) async {},
onGrantAuthorization: (String granted) async {},
onReceiveMessageData: (Map<String, dynamic> event) async {
print("flutter onReceiveMessageData: $event");
},
onNotificationMessageArrived: (Map<String, dynamic> event) async {},
onNotificationMessageClicked: (Map<String, dynamic> event) async {},
onTransmitUserMessageReceive: (Map<String, dynamic> event) async {},
onLiveActivityResult: (Map<String, dynamic> event) async {},
onRegisterPushToStartTokenResult: (Map<String, dynamic> event) async {},
);
}
Future<void> _login(String uid) async {
print('login user ID: $uid, client id: ${await Getuiflut.getClientId}');
Getuiflut().bindAlias(uid, 'openim');
}
void _logout() {
Getuiflut().unbindAlias(OpenIM.iMManager.userID, 'openim', true);
}
void _setBadge(int badge) {
Getuiflut().setBadge(badge);
}
void _resetBadge() {
Getuiflut().resetBadge();
}
}
class FCMPushController {
static final FCMPushController _instance = FCMPushController._internal();
factory FCMPushController() => _instance;
FCMPushController._internal();
Future<void> _initialize() async {
GooglePlayServicesAvailability? availability = GooglePlayServicesAvailability.success;
if (Platform.isAndroid) {
availability = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability();
}
if (availability != GooglePlayServicesAvailability.serviceInvalid) {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
} else {
Logger.print('Google Play Services are not available');
return;
}
await _requestPermission();
_configureForegroundNotification();
_configureBackgroundNotification();
return;
}
Future<void> _requestPermission() async {
NotificationSettings settings = await FirebaseMessaging.instance.requestPermission();
print('User granted permission: ${settings.authorizationStatus}');
}
void _configureForegroundNotification() {
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
print('Foreground notification received: ${message.notification?.title}');
if (message.notification != null) {}
});
}
void _configureBackgroundNotification() {
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print('App opened from background: ${message.notification?.title}');
});
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) {
if (message != null) {
print('App opened from terminated state: ${message.notification?.title}');
}
});
}
Future<String> _getToken() async {
final token = await FirebaseMessaging.instance.getToken();
Logger.print("FCM Token: $token");
if (token == null) {
throw Exception('FCM Token is null');
}
return token;
}
Future<void> _deleteToken() {
return FirebaseMessaging.instance.deleteToken();
}
void _listenToTokenRefresh(void Function(String token) onTokenRefresh) {
FirebaseMessaging.instance.onTokenRefresh.listen((String newToken) {
print("FCM Token refreshed: $newToken");
onTokenRefresh(newToken);
});
}
}