修复
This commit is contained in:
parent
c810e645dc
commit
53dbacef91
@ -6,9 +6,13 @@
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
@ -130,6 +134,12 @@
|
||||
android:name="io.agora.rtc2.extensions.MediaProjectionMgr$LocalScreenSharingService"
|
||||
tools:node="remove" />
|
||||
|
||||
<service
|
||||
android:name=".VoiceRoomForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback|microphone"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="high_importance_channel" />
|
||||
|
||||
@ -30,6 +30,33 @@ class MainActivity : FlutterActivity() {
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
VOICE_ROOM_FOREGROUND_CHANNEL
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"start" -> {
|
||||
try {
|
||||
VoiceRoomForegroundService.start(
|
||||
applicationContext,
|
||||
call.argument<String>("roomName")
|
||||
)
|
||||
result.success(null)
|
||||
} catch (e: Exception) {
|
||||
result.error("VOICE_ROOM_FOREGROUND_START_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
"stop" -> {
|
||||
try {
|
||||
VoiceRoomForegroundService.stop(applicationContext)
|
||||
result.success(null)
|
||||
} catch (e: Exception) {
|
||||
result.error("VOICE_ROOM_FOREGROUND_STOP_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@ -90,5 +117,7 @@ class MainActivity : FlutterActivity() {
|
||||
companion object {
|
||||
private const val INSTALL_REFERRER_CHANNEL = "com.org.yumiparty/install_referrer"
|
||||
private const val MOBILE_CONTEXT_CHANNEL = "com.org.yumiparty/mobile_context"
|
||||
private const val VOICE_ROOM_FOREGROUND_CHANNEL =
|
||||
"com.org.yumiparty/voice_room_foreground_service"
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,131 @@
|
||||
package com.org.yumiparty
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
|
||||
class VoiceRoomForegroundService : Service() {
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ensureNotificationChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val roomName = intent?.getStringExtra(EXTRA_ROOM_NAME)
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: DEFAULT_ROOM_NAME
|
||||
startAsForeground(roomName)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun startAsForeground(roomName: String) {
|
||||
val notification = buildNotification(roomName)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val serviceType =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
} else {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||
}
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
serviceType
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun buildNotification(roomName: String): Notification {
|
||||
val launchIntent = (
|
||||
packageManager.getLaunchIntentForPackage(packageName)
|
||||
?: Intent(this, MainActivity::class.java)
|
||||
).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
launchIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or immutableFlag()
|
||||
)
|
||||
|
||||
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, CHANNEL_ID)
|
||||
} else {
|
||||
Notification.Builder(this)
|
||||
}
|
||||
|
||||
return builder
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle("Yumi voice room")
|
||||
.setContentText(roomName)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(Notification.PRIORITY_LOW)
|
||||
.setCategory(Notification.CATEGORY_CALL)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun ensureNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return
|
||||
}
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
if (manager.getNotificationChannel(CHANNEL_ID) != null) {
|
||||
return
|
||||
}
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Voice room",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Keeps voice rooms active while Yumi is in the background."
|
||||
setShowBadge(false)
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun immutableFlag(): Int =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "yumi_voice_room"
|
||||
private const val NOTIFICATION_ID = 24031
|
||||
private const val EXTRA_ROOM_NAME = "room_name"
|
||||
private const val DEFAULT_ROOM_NAME = "Voice room is running"
|
||||
|
||||
fun start(context: Context, roomName: String?) {
|
||||
val intent = Intent(context, VoiceRoomForegroundService::class.java)
|
||||
.putExtra(EXTRA_ROOM_NAME, roomName)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
context.stopService(Intent(context, VoiceRoomForegroundService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -502,7 +502,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_TEAM = S9X2AJ2US9;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -693,7 +693,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -722,7 +722,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@ -15,6 +15,7 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
|
||||
import 'package:yumi/services/room/rc_room_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/audio/voice_room_foreground_service.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
@ -302,6 +303,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
'roomStartupSeatInteractionLoading';
|
||||
static const String _selfMicGoUpRetryLoadingTag = 'selfMicGoUpRetryLoading';
|
||||
static const int _selfMicGoUpFailureLimit = 3;
|
||||
static const List<Duration> _roomRedPacketEntryRefreshDelays = [
|
||||
Duration.zero,
|
||||
Duration(seconds: 2),
|
||||
Duration(seconds: 6),
|
||||
];
|
||||
|
||||
bool needUpDataUserInfo = false;
|
||||
bool _roomVisualEffectsEnabled = false;
|
||||
@ -443,6 +449,21 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
bool get shouldShowRoomVisualEffects =>
|
||||
currenRoom != null && _roomVisualEffectsEnabled;
|
||||
|
||||
void _startVoiceRoomForegroundService() {
|
||||
final roomProfile = currenRoom?.roomProfile?.roomProfile;
|
||||
final roomName = (roomProfile?.roomName ?? "").trim();
|
||||
final roomAccount = (roomProfile?.roomAccount ?? "").trim();
|
||||
unawaited(
|
||||
SCVoiceRoomForegroundService.start(
|
||||
roomName: roomName.isNotEmpty ? roomName : roomAccount,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _stopVoiceRoomForegroundService() {
|
||||
unawaited(SCVoiceRoomForegroundService.stop());
|
||||
}
|
||||
|
||||
bool get _shouldDeferSeatInteractionForRoomStartup =>
|
||||
_roomStartupStatus == RoomStartupStatus.loading;
|
||||
|
||||
@ -1943,6 +1964,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Future<void> releaseRtcEngineForAppTermination() async {
|
||||
final rtcEngine = engine;
|
||||
if (rtcEngine == null) {
|
||||
await SCVoiceRoomForegroundService.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1968,6 +1990,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
} catch (e) {
|
||||
debugPrint('rtc释放引擎出错: $e');
|
||||
}
|
||||
await SCVoiceRoomForegroundService.stop();
|
||||
}
|
||||
|
||||
void initializeAudioVolumeIndicationCallback(
|
||||
@ -2114,6 +2137,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
_startVoiceRoomForegroundService();
|
||||
VoiceRoomRoute.openVoiceRoom(context);
|
||||
return;
|
||||
}
|
||||
@ -2137,6 +2161,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_startRoomStatePolling();
|
||||
_setRoomStartupReady();
|
||||
setRoomVisualEffectsEnabled(true);
|
||||
_startRoomRedPacketPresenceHeartbeat(roomId);
|
||||
_refreshRoomRedPacketListAfterEntry(roomId: roomId);
|
||||
_startVoiceRoomForegroundService();
|
||||
VoiceRoomRoute.openVoiceRoom(context);
|
||||
} else {
|
||||
if (currenRoom != null) {
|
||||
@ -2157,6 +2184,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
previewData: previewData,
|
||||
previewSeatCount: previewSeatCount,
|
||||
);
|
||||
_startVoiceRoomForegroundService();
|
||||
VoiceRoomRoute.openVoiceRoom(context);
|
||||
notifyListeners();
|
||||
final entryRequestSerial = ++_roomEntryRequestSerial;
|
||||
@ -2192,6 +2220,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom);
|
||||
_syncCurrentUserVipHintFromRoom(currenRoom);
|
||||
_startVoiceRoomForegroundService();
|
||||
unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial));
|
||||
_currentRoomIsEntryPreview = false;
|
||||
_previewRoomSeatCount = null;
|
||||
@ -2207,6 +2236,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
debugPrint('[RoomStartup] entry room failed: $e\n$stackTrace');
|
||||
_stopVoiceRoomForegroundService();
|
||||
_setRoomStartupFailed(RoomStartupFailureType.entry);
|
||||
}
|
||||
}
|
||||
@ -2674,6 +2704,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
debugPrint(
|
||||
'[RoomStartup] join IM group failed code=${joinResult?.code} desc=${joinResult?.desc}',
|
||||
);
|
||||
_stopVoiceRoomForegroundService();
|
||||
_setRoomStartupFailed(RoomStartupFailureType.im);
|
||||
return;
|
||||
}
|
||||
@ -2693,6 +2724,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
rtmProvider?.loadRoomHistoryMessages(groupId, count: 20) ??
|
||||
Future<void>.value(),
|
||||
);
|
||||
_refreshRoomRedPacketListAfterEntry(roomId: roomId);
|
||||
|
||||
await Future.wait([microphoneListFuture, onlineUsersFuture]);
|
||||
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
|
||||
@ -2748,6 +2780,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
debugPrint('[RoomStartup] bootstrap failed: $error\n$stackTrace');
|
||||
_stopVoiceRoomForegroundService();
|
||||
_setRoomStartupFailed(failureType);
|
||||
}
|
||||
}
|
||||
@ -2988,11 +3021,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Future<List<SCRoomRedPacketListRes>> loadRoomRedPacketList(
|
||||
int current,
|
||||
) async {
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
|
||||
if (roomId.isEmpty) {
|
||||
return const <SCRoomRedPacketListRes>[];
|
||||
}
|
||||
var result = await SCChatRoomRepository().roomRedPacketList(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
roomId,
|
||||
current,
|
||||
);
|
||||
if (current == 1) {
|
||||
final currentRoomId =
|
||||
currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
|
||||
if (current == 1 && currentRoomId == roomId) {
|
||||
redPacketList = result;
|
||||
notifyListeners();
|
||||
}
|
||||
@ -3055,19 +3094,18 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void _refreshRoomRedPacketListAfterEntry() {
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
|
||||
if (roomId.isEmpty) {
|
||||
void _refreshRoomRedPacketListAfterEntry({String? roomId}) {
|
||||
final normalizedRoomId =
|
||||
roomId?.trim() ??
|
||||
currenRoom?.roomProfile?.roomProfile?.id?.trim() ??
|
||||
"";
|
||||
if (normalizedRoomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
for (final delay in const [
|
||||
Duration.zero,
|
||||
Duration(seconds: 2),
|
||||
Duration(seconds: 6),
|
||||
]) {
|
||||
for (final delay in _roomRedPacketEntryRefreshDelays) {
|
||||
unawaited(
|
||||
Future<void>.delayed(delay).then((_) {
|
||||
return _refreshRoomRedPacketListIfCurrent(roomId);
|
||||
return _refreshRoomRedPacketListIfCurrent(normalizedRoomId);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@ -3381,6 +3419,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
///清空列表数据
|
||||
void _clearData() {
|
||||
_roomEntryRequestSerial += 1;
|
||||
_stopVoiceRoomForegroundService();
|
||||
_roomEntryEffectTimer?.cancel();
|
||||
_roomEntryEffectTimer = null;
|
||||
_stopRoomRedPacketPresenceHeartbeat();
|
||||
|
||||
@ -571,6 +571,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
logined = true;
|
||||
print('tim 登录成功');
|
||||
await initConversation();
|
||||
unawaited(syncRoomRedPacketBroadcastGroup());
|
||||
} else {
|
||||
// 登录失败逻辑
|
||||
//print('timm 需要重新登录2');
|
||||
@ -591,16 +592,26 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
|
||||
_onNewMessage(V2TimMessage message) {
|
||||
if (message.groupID != null) {
|
||||
final groupId = message.groupID!;
|
||||
|
||||
///全服通知 / 语区广播群
|
||||
if (message.groupID == SCGlobalConfig.bigBroadcastGroup ||
|
||||
message.groupID == roomRedPacketBroadcastGroupId) {
|
||||
_newBroadCastMsgRecv(message.groupID!, message);
|
||||
if (groupId == SCGlobalConfig.bigBroadcastGroup ||
|
||||
groupId == roomRedPacketBroadcastGroupId) {
|
||||
_newBroadCastMsgRecv(groupId, message);
|
||||
return;
|
||||
}
|
||||
if (_shouldRouteUntrackedRoomRedPacketBroadcast(groupId, message)) {
|
||||
debugPrint(
|
||||
'[RoomRedPacket][IM] route untracked group as region broadcast '
|
||||
'group=$groupId cachedRegionGroup=$roomRedPacketBroadcastGroupId',
|
||||
);
|
||||
_newBroadCastMsgRecv(groupId, message, forceRegionBroadcast: true);
|
||||
return;
|
||||
}
|
||||
|
||||
///群消息
|
||||
for (var element in onNewMessageListenerGroupMap.values) {
|
||||
element?.call(message.groupID!, message);
|
||||
element?.call(groupId, message);
|
||||
}
|
||||
} else {
|
||||
///单聊消息
|
||||
@ -1780,6 +1791,38 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
'';
|
||||
}
|
||||
|
||||
String _currentVoiceRoomGroupId() {
|
||||
final currentContext = context;
|
||||
if (currentContext == null || !currentContext.mounted) {
|
||||
return '';
|
||||
}
|
||||
return Provider.of<RealTimeCommunicationManager>(
|
||||
currentContext,
|
||||
listen: false,
|
||||
).currenRoom?.roomProfile?.roomProfile?.roomAccount?.trim() ??
|
||||
'';
|
||||
}
|
||||
|
||||
bool _shouldRouteUntrackedRoomRedPacketBroadcast(
|
||||
String groupId,
|
||||
V2TimMessage message,
|
||||
) {
|
||||
if (groupId == _currentVoiceRoomGroupId()) {
|
||||
return false;
|
||||
}
|
||||
final customData = message.customElem?.data;
|
||||
if (customData == null || customData.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
final decoded = json.decode(customData);
|
||||
final payload = _broadcastPayloadMap(decoded);
|
||||
return payload['type'] == SCRoomMsgType.roomRedPacket;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _broadcastPayloadMap(dynamic data) {
|
||||
if (data is Map<String, dynamic>) {
|
||||
return data;
|
||||
@ -1902,11 +1945,16 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
///全服广播消息
|
||||
_newBroadCastMsgRecv(String groupID, V2TimMessage message) async {
|
||||
_newBroadCastMsgRecv(
|
||||
String groupID,
|
||||
V2TimMessage message, {
|
||||
bool forceRegionBroadcast = false,
|
||||
}) async {
|
||||
try {
|
||||
String? customData = message.customElem?.data;
|
||||
if (customData != null && customData.isNotEmpty) {
|
||||
final isRegionBroadcastGroup = groupID == roomRedPacketBroadcastGroupId;
|
||||
final isRegionBroadcastGroup =
|
||||
forceRegionBroadcast || groupID == roomRedPacketBroadcastGroupId;
|
||||
final data = json.decode(customData);
|
||||
var type = data["type"];
|
||||
if (type == "SYS_ACTIVITY") {
|
||||
@ -1988,7 +2036,25 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
} else if (type == SCRoomMsgType.roomRedPacket) {
|
||||
///红包触发飘屏
|
||||
var fData = data["data"];
|
||||
final payload = _broadcastPayloadMap(fData);
|
||||
final packetId = _payloadText(payload['packetId']);
|
||||
final packetRoomId = _payloadText(payload["roomId"]);
|
||||
debugPrint(
|
||||
'[RoomRedPacket][IM] recv broadcast packetId=$packetId '
|
||||
'roomId=$packetRoomId group=$groupID '
|
||||
'regionGroup=$roomRedPacketBroadcastGroupId '
|
||||
'isRegion=$isRegionBroadcastGroup '
|
||||
'payloadRegion=${_payloadText(payload['regionCode'])} '
|
||||
'currentRegion=${roomRedPacketBroadcastRegionCode ?? ''} '
|
||||
'currentRoom=${_currentVoiceRoomId()}',
|
||||
);
|
||||
if (!_isSameRoomRedPacketRegion(fData)) {
|
||||
debugPrint(
|
||||
'[RoomRedPacket][IM] skip broadcast region mismatch '
|
||||
'packetId=$packetId '
|
||||
'payloadRegion=${_payloadText(payload['regionCode'])} '
|
||||
'currentRegion=${roomRedPacketBroadcastRegionCode ?? ''}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
RoomRedPacketPendingCache.instance.upsertPayload(fData);
|
||||
@ -2007,7 +2073,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
: '',
|
||||
priority: 1000,
|
||||
);
|
||||
final packetRoomId = _payloadText(fData["roomId"]);
|
||||
if (_isCurrentVisibleVoiceRoom(packetRoomId)) {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
@ -2616,6 +2681,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[RoomRedPacket][IM] send broadcast packetId=${payload['packetId']} '
|
||||
'roomId=${payload['roomId']} mode=${payload['packetMode']} '
|
||||
'targets=${targetGroupIds.join(',')} regionGroup=$regionGroupId '
|
||||
'region=${payload['regionCode'] ?? roomRedPacketBroadcastRegionCode ?? ''}',
|
||||
);
|
||||
final message = <String, dynamic>{
|
||||
'type': SCRoomMsgType.roomRedPacket,
|
||||
'data': payload,
|
||||
|
||||
38
lib/services/audio/voice_room_foreground_service.dart
Normal file
38
lib/services/audio/voice_room_foreground_service.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class SCVoiceRoomForegroundService {
|
||||
static const MethodChannel _channel = MethodChannel(
|
||||
'com.org.yumiparty/voice_room_foreground_service',
|
||||
);
|
||||
|
||||
static Future<void> start({String? roomName}) async {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await _channel.invokeMethod<void>('start', {
|
||||
'roomName': roomName?.trim(),
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('[VoiceRoomForeground] start failed: ${e.message}');
|
||||
} catch (e) {
|
||||
debugPrint('[VoiceRoomForeground] start failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> stop() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await _channel.invokeMethod<void>('stop');
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('[VoiceRoomForeground] stop failed: ${e.message}');
|
||||
} catch (e) {
|
||||
debugPrint('[VoiceRoomForeground] stop failed: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,12 +24,14 @@ typedef FloatingScreenManager = OverlayManager;
|
||||
|
||||
class OverlayManager {
|
||||
static const int _maxLowPerformanceCompensationQueueLength = 8;
|
||||
static const int _maxDeferredRegionRedPacketCount = 12;
|
||||
static const Duration _compensationDedupWindow = Duration(seconds: 3);
|
||||
static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30);
|
||||
|
||||
final SCPriorityQueue<SCFloatingMessage> _messageQueue = SCPriorityQueue(
|
||||
(a, b) => b.priority.compareTo(a.priority),
|
||||
);
|
||||
final List<SCFloatingMessage> _deferredRegionRedPacketMessages = [];
|
||||
final Map<String, DateTime> _recentCompensationKeys = {};
|
||||
final Map<String, DateTime> _recentFloatingKeys = {};
|
||||
bool _isPlaying = false;
|
||||
@ -53,6 +55,12 @@ class OverlayManager {
|
||||
if (_isRecentFloatingDuplicate(message)) {
|
||||
return;
|
||||
}
|
||||
if (message.type == 4) {
|
||||
debugPrint(
|
||||
'[RoomRedPacket][Floating] enqueue packetId=${message.toUserId} '
|
||||
'roomId=${message.roomId} region=${message.isRegionBroadcast}',
|
||||
);
|
||||
}
|
||||
_rememberFloatingMessage(message);
|
||||
_enqueueMessage(
|
||||
message,
|
||||
@ -61,6 +69,7 @@ class OverlayManager {
|
||||
} else {
|
||||
_removeActiveMessage(scheduleNext: false);
|
||||
_messageQueue.clear();
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
_isPlaying = false;
|
||||
_isProcessing = false;
|
||||
_isDisposed = false;
|
||||
@ -256,9 +265,20 @@ class OverlayManager {
|
||||
// 安全地获取第一个消息
|
||||
if (_messageQueue.isEmpty) return;
|
||||
final messageToProcess = _messageQueue.first;
|
||||
if (messageToProcess == null) {
|
||||
_messageQueue.removeFirst();
|
||||
_safeScheduleNext();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_shouldDisplayMessage(context, messageToProcess)) {
|
||||
_messageQueue.removeFirst();
|
||||
if (_shouldDeferRegionRedPacketMessage(context, messageToProcess)) {
|
||||
_deferRegionRedPacketMessage(context, messageToProcess);
|
||||
_safeScheduleNext();
|
||||
return;
|
||||
}
|
||||
_logRegionRedPacketDrop(context, messageToProcess);
|
||||
_safeScheduleNext();
|
||||
return;
|
||||
}
|
||||
@ -401,6 +421,7 @@ class OverlayManager {
|
||||
_removeRegionBroadcastMessages();
|
||||
return;
|
||||
}
|
||||
_restoreDeferredRegionRedPacketMessages();
|
||||
_safeScheduleNext();
|
||||
}
|
||||
|
||||
@ -434,8 +455,10 @@ class OverlayManager {
|
||||
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
_removeActiveMessage(scheduleNext: false);
|
||||
_messageQueue.clear();
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
return;
|
||||
}
|
||||
_restoreDeferredRegionRedPacketMessages();
|
||||
final context = navigatorKey.currentState?.context;
|
||||
final activeMessage = _currentMessage;
|
||||
if (context != null &&
|
||||
@ -456,6 +479,7 @@ class OverlayManager {
|
||||
_currentOverlayEntry = null;
|
||||
_currentMessage = null;
|
||||
_messageQueue.clear();
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
_recentCompensationKeys.clear();
|
||||
_isPlaying = false;
|
||||
_isProcessing = false;
|
||||
@ -497,6 +521,86 @@ class OverlayManager {
|
||||
|
||||
bool get _isSuppressed => _suppressedCount > 0;
|
||||
|
||||
bool _isRegionRedPacketMessage(SCFloatingMessage message) {
|
||||
return message.type == 4 && message.isRegionBroadcast;
|
||||
}
|
||||
|
||||
bool _shouldDeferRegionRedPacketMessage(
|
||||
BuildContext context,
|
||||
SCFloatingMessage message,
|
||||
) {
|
||||
if (!_isRegionRedPacketMessage(message) ||
|
||||
SCGlobalConfig.isFloatingBroadcastClosed) {
|
||||
return false;
|
||||
}
|
||||
return !_shouldDisplayRegionBroadcastMessage(context, message);
|
||||
}
|
||||
|
||||
void _deferRegionRedPacketMessage(
|
||||
BuildContext context,
|
||||
SCFloatingMessage message,
|
||||
) {
|
||||
final key = _floatingDedupKeyFor(message);
|
||||
if (key != null &&
|
||||
_deferredRegionRedPacketMessages.any(
|
||||
(item) => _floatingDedupKeyFor(item) == key,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
while (_deferredRegionRedPacketMessages.length >=
|
||||
_maxDeferredRegionRedPacketCount) {
|
||||
_deferredRegionRedPacketMessages.removeAt(0);
|
||||
}
|
||||
_deferredRegionRedPacketMessages.add(message);
|
||||
debugPrint(
|
||||
'[RoomRedPacket][Floating] defer packetId=${message.toUserId} '
|
||||
'roomId=${message.roomId} ${_displayStateForLog(context)}',
|
||||
);
|
||||
}
|
||||
|
||||
void _restoreDeferredRegionRedPacketMessages() {
|
||||
if (_deferredRegionRedPacketMessages.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final messages = List<SCFloatingMessage>.from(
|
||||
_deferredRegionRedPacketMessages,
|
||||
);
|
||||
_deferredRegionRedPacketMessages.clear();
|
||||
for (final message in messages) {
|
||||
_messageQueue.add(message);
|
||||
}
|
||||
debugPrint(
|
||||
'[RoomRedPacket][Floating] restore deferred count=${messages.length}',
|
||||
);
|
||||
}
|
||||
|
||||
void _logRegionRedPacketDrop(
|
||||
BuildContext context,
|
||||
SCFloatingMessage message,
|
||||
) {
|
||||
if (message.type != 4) {
|
||||
return;
|
||||
}
|
||||
debugPrint(
|
||||
'[RoomRedPacket][Floating] drop packetId=${message.toUserId} '
|
||||
'roomId=${message.roomId} region=${message.isRegionBroadcast} '
|
||||
'${_displayStateForLog(context)}',
|
||||
);
|
||||
}
|
||||
|
||||
String _displayStateForLog(BuildContext context) {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
|
||||
return 'homeVisible=$_homeRootTabsVisible '
|
||||
'roomVisible=${rtcProvider.shouldShowRoomVisualEffects} '
|
||||
'currentRoomId=$currentRoomId '
|
||||
'scope=${SCGlobalConfig.floatingBroadcastScope}';
|
||||
}
|
||||
|
||||
bool _shouldDisplayMessage(BuildContext context, SCFloatingMessage? message) {
|
||||
if (message == null) {
|
||||
return false;
|
||||
@ -582,7 +686,7 @@ class OverlayManager {
|
||||
|
||||
while (_messageQueue.isNotEmpty) {
|
||||
final message = _messageQueue.removeFirst();
|
||||
if (!message.isRegionBroadcast) {
|
||||
if (!message.isRegionBroadcast || _isRegionRedPacketMessage(message)) {
|
||||
newQueue.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -607,6 +607,11 @@ class _RoomRedPacketSendPanelState extends State<RoomRedPacketSendPanel> {
|
||||
(_selectedPickupMinutes > 0 ? _selectedPickupMinutes * 60 : 0)
|
||||
..['claimStartTime'] = claimStartTime
|
||||
..['grabbed'] = result.grabbed ?? false;
|
||||
final roomRegionCode = rtcProvider.currenRoom?.roomProfile?.regionCode;
|
||||
if ((payload['regionCode']?.toString().trim() ?? '').isEmpty &&
|
||||
(roomRegionCode?.trim().isNotEmpty ?? false)) {
|
||||
payload['regionCode'] = roomRegionCode!.trim();
|
||||
}
|
||||
|
||||
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||
rtmProvider.addMsg(
|
||||
|
||||
@ -68,6 +68,7 @@ class _RoomRedPacketFloatingEntryState
|
||||
Timer? _entryRefreshTimer;
|
||||
bool _refreshingList = false;
|
||||
int _elapsedSeconds = 0;
|
||||
String _observedRoomId = '';
|
||||
String _verifiedRoomId = '';
|
||||
int _verifiedRoomRefreshCount = 0;
|
||||
Set<String> _verifiedKnownPacketIds = const <String>{};
|
||||
@ -80,6 +81,11 @@ class _RoomRedPacketFloatingEntryState
|
||||
const Duration(seconds: 1),
|
||||
(_) => _onEntryTimerTick(),
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_refreshRoomRedPacketList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -93,6 +99,7 @@ class _RoomRedPacketFloatingEntryState
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_refreshRoomRedPacketListIfRoomChanged();
|
||||
setState(() {});
|
||||
_elapsedSeconds++;
|
||||
if (_elapsedSeconds % _listRefreshIntervalSeconds == 0) {
|
||||
@ -100,6 +107,24 @@ class _RoomRedPacketFloatingEntryState
|
||||
}
|
||||
}
|
||||
|
||||
void _refreshRoomRedPacketListIfRoomChanged() {
|
||||
try {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
|
||||
if (currentRoomId.isEmpty || currentRoomId == _observedRoomId) {
|
||||
return;
|
||||
}
|
||||
_observedRoomId = currentRoomId;
|
||||
_elapsedSeconds = 0;
|
||||
_verifiedRoomId = '';
|
||||
_verifiedRoomRefreshCount = 0;
|
||||
_verifiedKnownPacketIds = const <String>{};
|
||||
_verifiedVisiblePacketIds = const <String>{};
|
||||
_refreshRoomRedPacketList();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _refreshRoomRedPacketList() {
|
||||
if (_refreshingList || !mounted) {
|
||||
return;
|
||||
@ -116,6 +141,13 @@ class _RoomRedPacketFloatingEntryState
|
||||
rtcProvider
|
||||
.loadRoomRedPacketList(1)
|
||||
.then((result) {
|
||||
final activeRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id
|
||||
?.trim() ??
|
||||
'';
|
||||
if (activeRoomId != currentRoomId) {
|
||||
return result;
|
||||
}
|
||||
final serverPackets =
|
||||
result.map(RoomRedPacketUiData.fromListRes).toList();
|
||||
final serverPacketIds =
|
||||
|
||||
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.3.0+10
|
||||
version: 1.3.0+11
|
||||
|
||||
|
||||
environment:
|
||||
|
||||
1
需求进度.md
1
需求进度.md
@ -562,6 +562,7 @@
|
||||
- 已修正红包领取成功公屏消息的多语言问题:领取后不再发送已翻译好的普通文本,改为 `ROOM_RED_PACKET_CLAIM` 结构化房间 IM,只传领取金额、红包发送人名和 `packetId`,公屏渲染时按当前客户端语言生成文案,避免英文用户看到阿语消息。
|
||||
- 已补强房外延时红包飘屏和领取通知去重:首页四个一级 Tab 可见/回到前台时会重新同步当前语区红包 IM 群;语区红包飘屏在首页 Tab 中不再受“只看房间飘屏”设置影响;领取成功公屏通知按 `packetId + 领取人ID` 生成稳定去重键,避免本地插入和 IM 回包/历史拉取造成两条一模一样的领取消息。
|
||||
- 已定位并修复公屏上滑历史反复弹 `user info not found`:历史消息 item 重建时会重复调用用户资料接口,缺失用户返回错误后被网络层 toast;现在房间公屏补拉用户资料改为静默请求,并对同一 userId 的请求/失败结果做缓存,5 分钟内不再反复请求同一个不存在用户。
|
||||
- 已继续排查房外/跨房红包飘屏缺失:IM 重登成功后会立即重新同步当前语区红包群;收到未登记但内容为 `ROOM_RED_PACKET` 的群消息时会按语区红包飘屏兜底处理,避免语区群 id 尚未写入时被普通群消息分支吞掉;区域红包在首页 Tab 暂不可见时不再直接从飘屏队列丢弃,而是挂起后在回到首页 Tab 时恢复播放;发送红包 payload 也会补齐当前房间 `regionCode`,便于日志核对和前端兜底过滤。
|
||||
|
||||
## 已知问题
|
||||
- 已接入语音房表情包素材第一版:桌面 `表情素材` 中的 `fluent_emoji` 与 `monkey` gif 已纳入项目资源,底部聊天入口改为图稿里的短白色胶囊并移除内部输入图标,右侧新增独立表情按钮;同一个房间输入弹层现在可从表情按钮直接打开表情包面板,原 unicode emoji 面板已替换为本地 gif 表情。发送逻辑同步按最新规则分流:用户在麦上发表情时只在自己麦位播放,不进入聊天列表;用户未上麦发表情时进入房间聊天框展示,不触发麦位动画。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user