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:
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user