improvements

ui_ux_rollout_merge_audio_video_call
WaseemAbbasi22 1 month ago
parent 8aa40f7b8a
commit 79b6d4fa2b

@ -18,9 +18,30 @@ import 'package:test_sa/modules/tm_module/tasks/task_request_detail_view.dart';
import 'package:test_sa/modules/tm_module/gas_refill/gas_refill_details.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
import 'package:test_sa/modules/cx_module/chat/call/services/call_notification_service.dart';
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {}
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
try {
log('📱 [BACKGROUND] Push notification received', name: 'FirebaseNotificationManger');
log('📱 [BACKGROUND] Data: ${message.data}', name: 'FirebaseNotificationManger');
final notificationType = message.data['notificationType'] as String?;
final transactionType = message.data['transactionType'] as String?;
// Handle incoming call notifications
if (notificationType == 'incoming_call' || transactionType == 'call') {
log('📞 [BACKGROUND] Incoming call notification detected', name: 'FirebaseNotificationManger');
await CallNotificationService().handleIncomingCallNotification(
notificationData: message.data,
source: 'push',
);
}
} catch (e, stackTrace) {
log('❌ [BACKGROUND] Error handling background message: $e',
name: 'FirebaseNotificationManger', error: e, stackTrace: stackTrace);
}
}
class FirebaseNotificationManger {
static FirebaseMessaging messaging = FirebaseMessaging.instance;
@ -86,6 +107,21 @@ class FirebaseNotificationManger {
}
static void handleMessage(context, Map<String, dynamic> messageData) {
// NEW: Check if this is a call notification first
final notificationType = messageData['notificationType'] as String?;
final transactionType = messageData['transactionType'] as String?;
if (notificationType == 'incoming_call' || transactionType == 'call') {
log('📞 [FOREGROUND] Incoming call notification detected', name: 'FirebaseNotificationManger');
// Let CallNotificationService handle it
CallNotificationService().handleIncomingCallNotification(
notificationData: messageData,
source: 'push',
);
return;
}
if (messageData["requestType"] != null && messageData["requestNumber"] != null) {
Widget? serviceClass;
@ -199,10 +235,21 @@ class FirebaseNotificationManger {
static initialized(BuildContext context) async {
//TOD0 add platform check here also
if (!(await isGoogleServicesAvailable()) && Platform.isAndroid) {
// NEW: Handle Huawei initial notification
var initialNotification = await h_push.Push.getInitialNotification();
if (initialNotification != null) {
Map<String, dynamic> remoteData = Map<String, dynamic>.from(initialNotification["extras"] as Map);
handleMessage(context, remoteData);
// Check if it's a call notification
final notificationType = remoteData['notificationType'] as String?;
if (notificationType == 'incoming_call') {
await CallNotificationService().handleIncomingCallNotification(
notificationData: remoteData,
source: 'push',
);
} else {
handleMessage(context, remoteData);
}
}
h_push.Push.onNotificationOpenedApp.listen((message) {
@ -211,7 +258,16 @@ class FirebaseNotificationManger {
Map<String, dynamic> remoteData = message;
remoteData = remoteData["extras"];
handleMessage(context, remoteData);
// Check if it's a call notification
final notificationType = remoteData['notificationType'] as String?;
if (notificationType == 'incoming_call') {
CallNotificationService().handleIncomingCallNotification(
notificationData: remoteData,
source: 'push',
);
} else {
handleMessage(context, remoteData);
}
}
} catch (ex) {
print("parsingError:$ex");
@ -245,22 +301,61 @@ class FirebaseNotificationManger {
FirebaseMessaging.instance.getInitialMessage().then((initialMessage) {
if (initialMessage != null) {
handleMessage(context, initialMessage.data);
// NEW: Check if it's a call notification
final notificationType = initialMessage.data['notificationType'] as String?;
if (notificationType == 'incoming_call') {
CallNotificationService().handleIncomingCallNotification(
notificationData: initialMessage.data,
source: 'push',
);
} else {
handleMessage(context, initialMessage.data);
}
}
});
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// NEW: Check if it's a call notification
final notificationType = message.data['notificationType'] as String?;
if (notificationType == 'incoming_call') {
log('📞 [FOREGROUND] Incoming call via FCM', name: 'FirebaseNotificationManger');
CallNotificationService().handleIncomingCallNotification(
notificationData: message.data,
source: 'push',
);
return;
}
// ...existing code...
if (Platform.isAndroid) {
if (message.data["notificationType"] != 'NurseConfirmArrive') {
NotificationManger.showNotification(
title: message.notification?.title ?? "", subtext: message.notification?.body ?? "", hashcode: int.tryParse("1234" ?? "") ?? 1, payload: json.encode(message.data), context: context);
title: message.notification?.title ?? "",
subtext: message.notification?.body ?? "",
hashcode: int.tryParse("1234" ?? "") ?? 1,
payload: json.encode(message.data),
context: context);
}
}
return;
});
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
handleMessage(context, message.data);
// NEW: Check if it's a call notification
final notificationType = message.data['notificationType'] as String?;
if (notificationType == 'incoming_call') {
log('📞 [NOTIFICATION TAP] Incoming call notification tapped', name: 'FirebaseNotificationManger');
CallNotificationService().handleIncomingCallNotification(
notificationData: message.data,
source: 'push',
);
} else {
handleMessage(context, message.data);
}
});
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
}
}

@ -0,0 +1,202 @@
import 'dart:developer';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:flutter_callkit_incoming/entities/entities.dart';
import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart';
/// Service to handle incoming call notifications across all app states
/// Integrates with existing Firebase notification system
class CallNotificationService {
static final CallNotificationService _instance = CallNotificationService._internal();
factory CallNotificationService() => _instance;
CallNotificationService._internal();
// Track processed call IDs to prevent duplicates
final Set<String> _processedCallIds = {};
// Store pending call data for restoration after app wake
Map<String, dynamic>? _pendingCallData;
/// Handle incoming call notification from push (FCM/Huawei) or SignalR
/// This is the SINGLE entry point for all incoming calls regardless of app state
Future<void> handleIncomingCallNotification({
required Map<String, dynamic> notificationData,
required String source, // 'push' or 'signalr'
}) async {
try {
log('═══════════════════════════════════════════', name: 'CallNotificationService');
log('📞 [INCOMING CALL] Notification received from: $source', name: 'CallNotificationService');
log('📞 [INCOMING CALL] Data: $notificationData', name: 'CallNotificationService');
// Extract call information
final notificationType = notificationData['notificationType'] as String?;
final transactionType = notificationData['transactionType'] as String?;
// Verify this is a call notification
if (notificationType != 'incoming_call' && transactionType != 'call') {
log('⚠️ [INCOMING CALL] Not a call notification, ignoring', name: 'CallNotificationService');
return;
}
final callId = notificationData['callId'] as String? ??
notificationData['sessionId'] as String?;
final callerId = notificationData['callerId'] as String? ??
notificationData['sourceUserId'] as String?;
final callerName = notificationData['callerName'] as String? ??
notificationData['userName'] as String? ??
'Unknown Caller';
final isVideoCall = notificationData['isVideoCall'] as bool? ??
(notificationData['callType'] == 'video');
final moduleId = notificationData['moduleId'] as String?;
final referenceId = notificationData['referenceId'] as String?;
final conversationId = notificationData['conversationId'] as String?;
if (callId == null || callerId == null) {
log('❌ [INCOMING CALL] Missing required data (callId or callerId)', name: 'CallNotificationService');
return;
}
// CRITICAL: Prevent duplicate processing
if (_processedCallIds.contains(callId)) {
log('⚠️ [INCOMING CALL] Already processed call $callId, ignoring duplicate', name: 'CallNotificationService');
return;
}
_processedCallIds.add(callId);
// Clean up old processed IDs (keep last 50)
if (_processedCallIds.length > 50) {
final oldIds = _processedCallIds.take(_processedCallIds.length - 50).toList();
_processedCallIds.removeAll(oldIds);
}
log('✅ [INCOMING CALL] Valid call notification', name: 'CallNotificationService');
log(' Call ID: $callId', name: 'CallNotificationService');
log(' Caller: $callerName ($callerId)', name: 'CallNotificationService');
log(' Video: $isVideoCall', name: 'CallNotificationService');
// Store pending call data
_pendingCallData = {
'callId': callId,
'callerId': callerId,
'callerName': callerName,
'isVideoCall': isVideoCall,
'moduleId': moduleId,
'referenceId': referenceId,
'conversationId': conversationId,
'timestamp': DateTime.now().toIso8601String(),
};
// Show CallKit/ConnectionService immediately
await _showNativeIncomingCallUI(
callId: callId,
callerName: callerName,
callerNumber: callerId,
isVideo: isVideoCall,
extra: _pendingCallData!,
);
log('✅ [INCOMING CALL] CallKit UI displayed', name: 'CallNotificationService');
// Use CallManager to handle the incoming call
log('📞 [INCOMING CALL] Delegating to CallManager...', name: 'CallNotificationService');
await CallManager().handleIncomingCallNotification(
callId: callId,
callerId: callerId,
callerName: callerName,
isVideoCall: isVideoCall,
extraData: _pendingCallData,
);
log('✅ [INCOMING CALL] Call handled successfully', name: 'CallNotificationService');
log('═══════════════════════════════════════════', name: 'CallNotificationService');
} catch (e, stackTrace) {
log('❌ [INCOMING CALL] Error handling notification: $e',
name: 'CallNotificationService', error: e, stackTrace: stackTrace);
}
}
/// Show native incoming call UI using CallKit/ConnectionService
Future<void> _showNativeIncomingCallUI({
required String callId,
required String callerName,
required String callerNumber,
required bool isVideo,
required Map<String, dynamic> extra,
}) async {
try {
log('📱 [CALLKIT] Showing native incoming call UI...', name: 'CallNotificationService');
log(' Call ID: $callId', name: 'CallNotificationService');
log(' Caller: $callerName', name: 'CallNotificationService');
log(' Video: $isVideo', name: 'CallNotificationService');
final params = CallKitParams(
id: callId,
nameCaller: callerName,
appName: 'Atoms SA',
handle: callerNumber,
type: isVideo ? 1 : 0, // 0 = audio, 1 = video
duration: 30000, // 30 seconds timeout
extra: extra,
headers: <String, dynamic>{'platform': 'flutter'},
android: AndroidParams(
isCustomNotification: true,
isShowLogo: false,
ringtonePath: 'system_ringtone_default',
backgroundColor: '#0955fa',
backgroundUrl: '',
actionColor: '#4CAF50',
incomingCallNotificationChannelName: 'Incoming Calls',
missedCallNotificationChannelName: 'Missed Calls',
),
ios: IOSParams(
iconName: 'CallKitLogo',
handleType: 'generic',
supportsVideo: true,
maximumCallGroups: 2,
maximumCallsPerCallGroup: 1,
audioSessionMode: 'videoChat',
audioSessionActive: true,
audioSessionPreferredSampleRate: 44100.0,
audioSessionPreferredIOBufferDuration: 0.005,
supportsDTMF: true,
supportsHolding: true,
supportsGrouping: false,
supportsUngrouping: false,
ringtonePath: 'system_ringtone_default',
),
);
await FlutterCallkitIncoming.showCallkitIncoming(params);
log('✅ [CALLKIT] Native UI displayed successfully', name: 'CallNotificationService');
} catch (e, stackTrace) {
log('❌ [CALLKIT] Error showing native UI: $e',
name: 'CallNotificationService', error: e, stackTrace: stackTrace);
rethrow;
}
}
/// Get pending call data (used when app wakes up)
Map<String, dynamic>? get pendingCallData => _pendingCallData;
/// Clear pending call data
void clearPendingCallData() {
_pendingCallData = null;
}
/// Mark a call as processed (for external use)
void markCallAsProcessed(String callId) {
_processedCallIds.add(callId);
}
/// Check if a call has been processed
bool isCallProcessed(String callId) {
return _processedCallIds.contains(callId);
}
/// Reset the service (for testing or logout)
void reset() {
_processedCallIds.clear();
_pendingCallData = null;
log('🔄 [RESET] CallNotificationService reset', name: 'CallNotificationService');
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,995 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:test_sa/main.dart';
import 'package:test_sa/modules/cx_module/chat/call/audio_call_page.dart';
import 'package:test_sa/modules/cx_module/chat/call/video_call_page.dart';
import 'package:test_sa/modules/cx_module/chat/call/services/webrtc_service.dart';
import 'package:test_sa/modules/cx_module/chat/call/services/callkit_service.dart';
import 'package:test_sa/modules/cx_module/chat/call/call_error_handler.dart';
import 'package:test_sa/modules/cx_module/chat/model/call_session.dart';
import 'package:test_sa/modules/cx_module/chat/services/signalr_service.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:uuid/uuid.dart';
/// CallManager - Singleton service that manages all call responsibilities
/// Works independently of ChatProvider
/// Uses SignalRService for SignalR communication
class CallManager extends ChangeNotifier {
static final CallManager _instance = CallManager._internal();
factory CallManager() => _instance;
CallManager._internal();
// Services
final SignalRService _signalRService = SignalRService();
final CallKitService _callKitService = CallKitService();
WebRTCService? _webrtcService;
// Call state
CallSession? _currentCall;
CallStatus _callStatus = CallStatus.idle;
Duration _callDuration = Duration.zero;
Timer? _callDurationTimer;
Timer? _callTimeoutTimer;
// Call controls
bool _isMuted = false;
bool _isSpeakerOn = false;
bool _isCameraOn = true;
bool _isPeerMuted = false;
bool _isPeerCameraOn = true;
// Initialization state
bool _callKitInitialized = false;
bool _handlersRegistered = false;
// Module context (for SignalR invocations)
String? _moduleId;
String? _referenceId;
String? _conversationId;
String? _myEmployeeNumber;
// Getters
CallSession? get currentCall => _currentCall;
CallStatus get callStatus => _callStatus;
Duration get callDuration => _callDuration;
bool get isMuted => _isMuted;
bool get isSpeakerOn => _isSpeakerOn;
bool get isCameraOn => _isCameraOn;
bool get isPeerMuted => _isPeerMuted;
bool get isPeerCameraOn => _isPeerCameraOn;
WebRTCService? get webrtcService => _webrtcService;
bool get isCallInProgress => _callStatus != CallStatus.idle;
/// Initialize CallManager with user credentials
Future<void> initialize({
required String userId,
required String authToken,
String? conversationId,
String? moduleId,
String? referenceId,
String? employeeNumber,
}) async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('🔧 [INIT] Initializing CallManager...', name: 'CallManager');
log(' User ID: $userId', name: 'CallManager');
log(' Conversation ID: ${conversationId ?? "none"}', name: 'CallManager');
// Store module context
_moduleId = moduleId;
_referenceId = referenceId;
_conversationId = conversationId;
_myEmployeeNumber = employeeNumber;
// Initialize SignalR connection
log('🔌 [INIT] Initializing SignalR connection...', name: 'CallManager');
final connected = await _signalRService.initialize(
userId: userId,
authToken: authToken,
conversationId: conversationId,
);
if (!connected) {
throw Exception('Failed to initialize SignalR connection');
}
log('✅ [INIT] SignalR connected', name: 'CallManager');
// Initialize CallKit
if (!_callKitInitialized) {
await _initializeCallKit();
}
// Register call event handlers
if (!_handlersRegistered) {
_registerCallHandlers();
}
log('✅ [INIT] CallManager initialized successfully', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [INIT] Error initializing CallManager: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
rethrow;
}
}
/// Initialize CallKit service
Future<void> _initializeCallKit() async {
try {
log('📞 [CALLKIT] Initializing CallKit...', name: 'CallManager');
await _callKitService.initialize();
// Setup CallKit callbacks
_callKitService.onCallAccepted = _handleCallKitAccepted;
_callKitService.onCallDeclined = _handleCallKitDeclined;
_callKitService.onCallEnded = _handleCallKitEnded;
_callKitService.onCallTimeout = _handleCallKitTimeout;
_callKitInitialized = true;
log('✅ [CALLKIT] CallKit initialized', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [CALLKIT] Error initializing: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
}
}
/// Register SignalR call event handlers
void _registerCallHandlers() {
log('🔧 [HANDLERS] Registering call event handlers...', name: 'CallManager');
_signalRService.on('OnIncomingCallAsync', _handleIncomingCall);
_signalRService.on('OnCallAcceptedAsync', _handleCallAccepted);
_signalRService.on('OnCallDeclinedAsync', _handleCallDeclined);
_signalRService.on('OnHangUpAsync', _handleHangUp);
_signalRService.on('OnOfferAsync', _handleOffer);
_signalRService.on('OnAnswerOfferAsync', _handleAnswer);
_signalRService.on('OnIceCandidateAsync', _handleIceCandidate);
_signalRService.on('OnAudioToggle', _handleAudioToggle);
_signalRService.on('OnCameraToggle', _handleCameraToggle);
_handlersRegistered = true;
log('✅ [HANDLERS] All call handlers registered', name: 'CallManager');
}
/// Start outgoing call
Future<void> startCall({
required String peerId,
required String peerName,
required CallType callType,
String? peerAvatar,
}) async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [OUTGOING] Starting ${callType.name} call to $peerName ($peerId)', name: 'CallManager');
// Check if already in call
if (_callStatus != CallStatus.idle) {
log('⚠️ [OUTGOING] Already in a call - CALLER_BUSY', name: 'CallManager');
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showCallAlreadyInProgress(context);
}
return;
}
// Update status
_updateCallStatus(CallStatus.checkingPermissions);
log('🔍 [OUTGOING] Checking permissions...', name: 'CallManager');
// Check permissions
if (!await _checkPermissions(callType)) {
log('❌ [OUTGOING] Permissions denied', name: 'CallManager');
_updateCallStatus(CallStatus.idle);
return;
}
log('✅ [OUTGOING] Permissions granted', name: 'CallManager');
// CRITICAL: Ensure SignalR is connected before every invocation
_updateCallStatus(CallStatus.connecting);
log('🔌 [OUTGOING] Ensuring SignalR connection...', name: 'CallManager');
if (!await _signalRService.ensureConnected()) {
log('❌ [OUTGOING] SignalR connection failed', name: 'CallManager');
_updateCallStatus(CallStatus.idle);
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showSignalRNotConnected(context);
}
return;
}
log('✅ [OUTGOING] SignalR connected - state: ${_signalRService.connectionState}', name: 'CallManager');
// Generate call ID
final callId = const Uuid().v4();
log('🆔 [OUTGOING] Generated call ID: $callId', name: 'CallManager');
// Create call session
_currentCall = CallSession(
callId: callId,
type: callType,
direction: CallDirection.outgoing,
peerId: peerId,
peerName: peerName,
peerAvatar: peerAvatar,
startTime: DateTime.now(),
);
// Invoke CallUserAsync
log('📤 [OUTGOING] Invoking CallUserAsync...', name: 'CallManager');
log(' From: ${_myEmployeeNumber ?? ""}', name: 'CallManager');
log(' To: $peerId', name: 'CallManager');
log(' Video: ${callType == CallType.video}', name: 'CallManager');
await _signalRService.invoke(
'CallUserAsync',
args: [
_myEmployeeNumber ?? '',
peerId,
callType == CallType.video,
],
);
log('✅ [OUTGOING] CallUserAsync invoked successfully', name: 'CallManager');
_updateCallStatus(CallStatus.outgoingRinging);
// Initialize WebRTC
log('🔧 [OUTGOING] Initializing WebRTC...', name: 'CallManager');
await _initializeWebRTC();
log('✅ [OUTGOING] WebRTC initialized', name: 'CallManager');
// Start timeout timer (60 seconds for outgoing calls)
_callTimeoutTimer = Timer(const Duration(seconds: 60), () {
if (_callStatus == CallStatus.outgoingRinging) {
log('⏱️ [OUTGOING] Call timeout - no answer after 60s', name: 'CallManager');
_cleanup();
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(context, 'Call timeout - no answer');
}
}
});
log('⏱️ [OUTGOING] Timeout timer started (60s)', name: 'CallManager');
// Navigate to call screen
_navigateToCallScreen();
log('✅ [OUTGOING] Navigated to call screen', name: 'CallManager');
log('✅ [OUTGOING] Call started successfully', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [OUTGOING] Error starting call: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(context, 'Failed to start call: $e');
}
}
}
/// Handle incoming call from notification
Future<void> handleIncomingCallNotification({
required String callId,
required String callerId,
required String callerName,
required bool isVideoCall,
Map<String, dynamic>? extraData,
}) async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [INCOMING] Handling incoming call notification', name: 'CallManager');
log(' Call ID: $callId', name: 'CallManager');
log(' Caller: $callerName ($callerId)', name: 'CallManager');
log(' Video: $isVideoCall', name: 'CallManager');
// Check if already in call
if (_callStatus != CallStatus.idle) {
log('⚠️ [INCOMING] Already in a call - declining with CALLER_BUSY', name: 'CallManager');
// Ensure SignalR is connected before declining
if (!await _signalRService.ensureConnected()) {
log('❌ [INCOMING] Cannot decline - SignalR not connected', name: 'CallManager');
return;
}
await _signalRService.invoke('CallDeclinedAsync', args: [
_myEmployeeNumber ?? '',
callerId,
'CALLER_BUSY',
]);
log('✅ [INCOMING] Declined with CALLER_BUSY', name: 'CallManager');
return;
}
// Ensure SignalR is connected
log('🔌 [INCOMING] Ensuring SignalR connection...', name: 'CallManager');
if (!await _signalRService.ensureConnected()) {
log('❌ [INCOMING] SignalR connection failed - cannot handle call', name: 'CallManager');
return;
}
log('✅ [INCOMING] SignalR connected', name: 'CallManager');
// Create call session
_currentCall = CallSession(
callId: callId,
type: isVideoCall ? CallType.video : CallType.audio,
direction: CallDirection.incoming,
peerId: callerId,
peerName: callerName,
peerAvatar: null,
startTime: DateTime.now(),
);
_updateCallStatus(CallStatus.incomingRinging);
log('✅ [INCOMING] Call session created', name: 'CallManager');
// Initialize CallKit if not already initialized
if (!_callKitInitialized) {
await _initializeCallKit();
}
// Show CallKit UI
log('📱 [INCOMING] Showing CallKit UI...', name: 'CallManager');
await _callKitService.showIncomingCall(
callId: callId,
callerName: callerName,
callerNumber: callerId,
isVideo: isVideoCall,
extra: extraData,
);
log('✅ [INCOMING] CallKit UI displayed', name: 'CallManager');
// Start timeout timer (30 seconds for incoming calls)
_callTimeoutTimer = Timer(const Duration(seconds: 30), () {
if (_callStatus == CallStatus.incomingRinging) {
log('⏱️ [INCOMING] Call timeout - no answer after 30s', name: 'CallManager');
_handleCallTimeout();
}
});
log('⏱️ [INCOMING] Timeout timer started (30s)', name: 'CallManager');
log('✅ [INCOMING] Incoming call handled successfully', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [INCOMING] Error handling incoming call: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
}
}
/// Handle call timeout (for incoming calls)
Future<void> _handleCallTimeout() async {
try {
log('⏱️ [TIMEOUT] Handling call timeout...', name: 'CallManager');
if (_currentCall == null) return;
// Invoke CallMissedAsync
if (await _signalRService.ensureConnected()) {
await _signalRService.invoke('CallMissedAsync', args: [
_myEmployeeNumber ?? '',
_currentCall!.peerId,
]);
log('✅ [TIMEOUT] CallMissedAsync invoked', name: 'CallManager');
}
_cleanup();
} catch (e, stackTrace) {
log('❌ [TIMEOUT] Error: $e', name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
}
}
/// Accept incoming call
Future<void> acceptCall() async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('✅ [ACCEPT] Accepting call...', name: 'CallManager');
if (_currentCall == null || _callStatus != CallStatus.incomingRinging) {
log('⚠️ [ACCEPT] No incoming call to accept', name: 'CallManager');
return;
}
// Check permissions
log('🔍 [ACCEPT] Checking permissions...', name: 'CallManager');
if (!await _checkPermissions(_currentCall!.type)) {
log('❌ [ACCEPT] Permissions denied', name: 'CallManager');
await declineCall('permission_denied');
return;
}
log('✅ [ACCEPT] Permissions granted', name: 'CallManager');
// Ensure SignalR connected
log('🔌 [ACCEPT] Ensuring SignalR connection...', name: 'CallManager');
if (!await _signalRService.ensureConnected()) {
log('❌ [ACCEPT] SignalR not connected', name: 'CallManager');
_cleanup();
return;
}
log('✅ [ACCEPT] SignalR connected', name: 'CallManager');
_updateCallStatus(CallStatus.connecting);
// Cancel timeout timer
_callTimeoutTimer?.cancel();
// Invoke AnswerCallAsync
log('📤 [ACCEPT] Invoking AnswerCallAsync...', name: 'CallManager');
await _signalRService.invoke('AnswerCallAsync', args: [
_myEmployeeNumber ?? '',
_currentCall!.peerId,
_moduleId ?? '0',
_referenceId ?? '0',
_conversationId ?? '',
]);
log('✅ [ACCEPT] AnswerCallAsync invoked', name: 'CallManager');
// Initialize WebRTC
log('🔧 [ACCEPT] Initializing WebRTC...', name: 'CallManager');
await _initializeWebRTC();
log('✅ [ACCEPT] WebRTC initialized', name: 'CallManager');
// Navigate to call screen
_navigateToCallScreen();
log('✅ [ACCEPT] Navigated to call screen', name: 'CallManager');
log('✅ [ACCEPT] Call accepted successfully', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [ACCEPT] Error accepting call: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
}
}
/// Decline incoming call
Future<void> declineCall(String reason) async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('❌ [DECLINE] Declining call - reason: $reason', name: 'CallManager');
if (_currentCall == null) {
log('⚠️ [DECLINE] No call to decline', name: 'CallManager');
return;
}
// Cancel timeout timer
_callTimeoutTimer?.cancel();
// Ensure SignalR connected
if (await _signalRService.ensureConnected()) {
log('📤 [DECLINE] Invoking CallDeclinedAsync...', name: 'CallManager');
await _signalRService.invoke('CallDeclinedAsync', args: [
_myEmployeeNumber ?? '',
_currentCall!.peerId,
reason,
]);
log('✅ [DECLINE] CallDeclinedAsync invoked', name: 'CallManager');
} else {
log('⚠️ [DECLINE] SignalR not connected, skipping backend call', name: 'CallManager');
}
_cleanup();
log('✅ [DECLINE] Call declined successfully', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [DECLINE] Error declining call: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
}
}
/// Hang up call
Future<void> hangUp() async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [HANGUP] Hanging up...', name: 'CallManager');
if (_currentCall == null) {
log('⚠️ [HANGUP] No call to hang up', name: 'CallManager');
return;
}
// Ensure SignalR connected
if (await _signalRService.ensureConnected()) {
log('📤 [HANGUP] Invoking HangUpAsync...', name: 'CallManager');
await _signalRService.invoke('HangUpAsync', args: [
_myEmployeeNumber ?? '',
_currentCall!.peerId,
_moduleId ?? '0',
_referenceId ?? '0',
_conversationId ?? '',
]);
log('✅ [HANGUP] HangUpAsync invoked', name: 'CallManager');
} else {
log('⚠️ [HANGUP] SignalR not connected, skipping backend call', name: 'CallManager');
}
_cleanup();
log('✅ [HANGUP] Call ended successfully', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [HANGUP] Error hanging up: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
}
}
/// Toggle mute
Future<void> toggleMute() async {
log('🎤 [CONTROL] Toggling mute: $_isMuted -> ${!_isMuted}', name: 'CallManager');
_isMuted = !_isMuted;
_webrtcService?.setMicrophoneMuted(_isMuted);
if (await _signalRService.ensureConnected()) {
await _signalRService.invoke('AudioToggle', args: [
_myEmployeeNumber ?? '',
_currentCall?.peerId ?? '',
]);
}
notifyListeners();
log('✅ [CONTROL] Mute toggled: $_isMuted', name: 'CallManager');
}
/// Toggle speaker
Future<void> toggleSpeaker() async {
log('🔊 [CONTROL] Toggling speaker: $_isSpeakerOn -> ${!_isSpeakerOn}', name: 'CallManager');
_isSpeakerOn = !_isSpeakerOn;
await _webrtcService?.setSpeakerphoneEnabled(_isSpeakerOn);
notifyListeners();
log('✅ [CONTROL] Speaker toggled: $_isSpeakerOn', name: 'CallManager');
}
/// Toggle camera
Future<void> toggleCamera() async {
if (_currentCall?.type != CallType.video) return;
log('📹 [CONTROL] Toggling camera: $_isCameraOn -> ${!_isCameraOn}', name: 'CallManager');
_isCameraOn = !_isCameraOn;
_webrtcService?.setCameraEnabled(_isCameraOn);
if (await _signalRService.ensureConnected()) {
await _signalRService.invoke('CameraToggle', args: [
_myEmployeeNumber ?? '',
_currentCall?.peerId ?? '',
]);
}
notifyListeners();
log('✅ [CONTROL] Camera toggled: $_isCameraOn', name: 'CallManager');
}
/// Switch camera
Future<void> switchCamera() async {
if (_currentCall?.type != CallType.video || !_isCameraOn) return;
log('🔄 [CONTROL] Switching camera...', name: 'CallManager');
await _webrtcService?.switchCamera();
log('✅ [CONTROL] Camera switched', name: 'CallManager');
}
// ==================== Private Methods ====================
Future<bool> _checkPermissions(CallType callType) async {
log('🔍 [PERMISSIONS] Checking permissions for ${callType.name} call...', name: 'CallManager');
final micStatus = await Permission.microphone.request();
if (!micStatus.isGranted) {
log('❌ [PERMISSIONS] Microphone permission denied', name: 'CallManager');
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showMicrophonePermissionDenied(context);
}
return false;
}
log('✅ [PERMISSIONS] Microphone permission granted', name: 'CallManager');
if (callType == CallType.video) {
final cameraStatus = await Permission.camera.request();
if (!cameraStatus.isGranted) {
log('❌ [PERMISSIONS] Camera permission denied', name: 'CallManager');
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showCameraPermissionDenied(context);
}
return false;
}
log('✅ [PERMISSIONS] Camera permission granted', name: 'CallManager');
}
return true;
}
Future<void> _initializeWebRTC() async {
log('🔧 [WEBRTC] Initializing WebRTC...', name: 'CallManager');
_webrtcService = WebRTCService();
_setupWebRTCCallbacks();
if (_currentCall!.type == CallType.audio) {
log('🎵 [WEBRTC] Initializing for audio call', name: 'CallManager');
await _webrtcService!.initializeForAudioCall();
} else {
log('📹 [WEBRTC] Initializing for video call', name: 'CallManager');
await _webrtcService!.initializeForVideoCall();
}
log('✅ [WEBRTC] WebRTC initialized', name: 'CallManager');
}
void _setupWebRTCCallbacks() {
log('🔧 [WEBRTC] Setting up callbacks...', name: 'CallManager');
_webrtcService!.onIceCandidate = (RTCIceCandidate candidate) {
log('🧊 [WEBRTC] ICE candidate generated', name: 'CallManager');
log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', name: 'CallManager');
final candidateJson = jsonEncode({
'candidate': candidate.candidate,
'sdpMid': candidate.sdpMid,
'sdpMLineIndex': candidate.sdpMLineIndex,
});
_signalRService.invoke('IceCandidateAsync', args: [
_currentCall!.peerId,
candidateJson,
_currentCall!.callId,
]);
};
_webrtcService!.onRemoteStream = (MediaStream stream) {
log('📡 [WEBRTC] Remote stream received', name: 'CallManager');
log(' Audio tracks: ${stream.getAudioTracks().length}', name: 'CallManager');
log(' Video tracks: ${stream.getVideoTracks().length}', name: 'CallManager');
notifyListeners();
};
_webrtcService!.onIceConnectionStateChange = (RTCIceConnectionState state) {
log('🔗 [WEBRTC] ICE state: ${state.toString()}', name: 'CallManager');
if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) {
log('✅ [WEBRTC] ICE connection established', name: 'CallManager');
_updateCallStatus(CallStatus.connected);
_startCallDurationTimer();
} else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) {
log('❌ [WEBRTC] ICE connection failed', name: 'CallManager');
_cleanup();
} else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
log('⚠️ [WEBRTC] ICE connection disconnected', name: 'CallManager');
}
};
log('✅ [WEBRTC] Callbacks set up', name: 'CallManager');
}
void _navigateToCallScreen() {
final context = navigatorKey.currentContext;
if (context == null || _currentCall == null) {
log('⚠️ [NAV] Cannot navigate - context or call is null', name: 'CallManager');
return;
}
log('🧭 [NAV] Navigating to ${_currentCall!.type.name} call screen', name: 'CallManager');
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _currentCall!.type == CallType.audio
? const AudioCallPage()
: const VideoCallPage(),
),
);
}
void _updateCallStatus(CallStatus newStatus) {
if (_callStatus == newStatus) return;
log('🔄 [STATUS] ${_callStatus.name} -> ${newStatus.name}', name: 'CallManager');
_callStatus = newStatus;
notifyListeners();
}
void _startCallDurationTimer() {
_callDurationTimer?.cancel();
_callDuration = Duration.zero;
log('⏱️ [TIMER] Starting call duration timer', name: 'CallManager');
_callDurationTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_callDuration = Duration(seconds: _callDuration.inSeconds + 1);
notifyListeners();
});
}
void _cleanup() {
log('🧹 [CLEANUP] Starting cleanup...', name: 'CallManager');
_callTimeoutTimer?.cancel();
_callDurationTimer?.cancel();
_webrtcService?.dispose();
_webrtcService = null;
if (_currentCall != null) {
_callKitService.endCall(_currentCall!.callId);
}
_currentCall = null;
_updateCallStatus(CallStatus.idle);
_callDuration = Duration.zero;
_isMuted = false;
_isSpeakerOn = false;
_isCameraOn = true;
_isPeerMuted = false;
_isPeerCameraOn = true;
log('✅ [CLEANUP] Cleanup complete', name: 'CallManager');
}
/// Reset service (for logout or testing)
Future<void> reset() async {
log('🔄 [RESET] Resetting CallManager...', name: 'CallManager');
_cleanup();
_callKitInitialized = false;
_handlersRegistered = false;
_moduleId = null;
_referenceId = null;
_conversationId = null;
_myEmployeeNumber = null;
log('✅ [RESET] CallManager reset complete', name: 'CallManager');
}
// ==================== SignalR Event Handlers ====================
void _handleIncomingCall(List<Object?>? args) {
log('📞 [EVENT] OnIncomingCallAsync received', name: 'CallManager');
log(' Args: $args', name: 'CallManager');
// Note: Incoming calls are now handled by CallNotificationService
// This event is kept for chat screen integration
log(' [EVENT] Incoming call handled by notification service', name: 'CallManager');
}
void _handleCallAccepted(List<Object?>? args) async {
log('📞 [EVENT] OnCallAcceptedAsync received', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
if (_callStatus != CallStatus.outgoingRinging) {
log('⚠️ [EVENT] Not in outgoing ringing state, ignoring', name: 'CallManager');
return;
}
_callTimeoutTimer?.cancel();
_updateCallStatus(CallStatus.connecting);
try {
// Create and send offer
log('🔧 [EVENT] Creating SDP offer...', name: 'CallManager');
final offer = await _webrtcService!.createOffer();
log('✅ [EVENT] SDP offer created', name: 'CallManager');
log('📤 [EVENT] Sending offer via OfferAsync...', name: 'CallManager');
await _signalRService.invoke('OfferAsync', args: [
_currentCall!.peerId,
offer.sdp ?? '',
_currentCall!.callId,
]);
log('✅ [EVENT] Offer sent successfully', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [EVENT] Error handling call accepted: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
}
}
void _handleCallDeclined(List<Object?>? args) {
log('📞 [EVENT] OnCallDeclinedAsync received', name: 'CallManager');
log(' Args: $args', name: 'CallManager');
// Extract decline reason if available
String? reason;
if (args != null && args.isNotEmpty) {
reason = args[0]?.toString();
log(' Decline reason: $reason', name: 'CallManager');
}
// Show appropriate message
final context = navigatorKey.currentContext;
if (context != null && reason != null) {
if (reason == 'USER_BUSY') {
CallErrorHandler.showGenericError(context, 'The user is currently busy on another call');
} else if (reason == 'USER_OFFLINE') {
CallErrorHandler.showGenericError(context, 'The user is currently offline');
}
}
_cleanup();
}
void _handleHangUp(List<Object?>? args) {
log('📞 [EVENT] OnHangUpAsync received', name: 'CallManager');
log(' Args: $args', name: 'CallManager');
_cleanup();
}
void _handleOffer(List<Object?>? args) async {
log('📞 [EVENT] OnOfferAsync received', name: 'CallManager');
if (args == null || args.isEmpty) {
log('⚠️ [EVENT] No offer data received', name: 'CallManager');
return;
}
try {
final offerSdp = args[0] as String?;
if (offerSdp == null) {
log('⚠️ [EVENT] Offer SDP is null', name: 'CallManager');
return;
}
if (_webrtcService == null) {
log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager');
return;
}
log('🔧 [EVENT] Creating SDP answer...', name: 'CallManager');
final answer = await _webrtcService!.createAnswer(offerSdp);
log('✅ [EVENT] SDP answer created', name: 'CallManager');
log('📤 [EVENT] Sending answer via AnswerOfferAsync...', name: 'CallManager');
await _signalRService.invoke('AnswerOfferAsync', args: [
_currentCall!.peerId,
answer.sdp ?? '',
_currentCall!.callId,
]);
log('✅ [EVENT] Answer sent successfully', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [EVENT] Error handling offer: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
}
}
void _handleAnswer(List<Object?>? args) async {
log('📞 [EVENT] OnAnswerOfferAsync received', name: 'CallManager');
if (args == null || args.isEmpty) {
log('⚠️ [EVENT] No answer data received', name: 'CallManager');
return;
}
try {
final answerSdp = args[0] as String?;
if (answerSdp == null) {
log('⚠️ [EVENT] Answer SDP is null', name: 'CallManager');
return;
}
if (_webrtcService == null) {
log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager');
return;
}
log('🔧 [EVENT] Setting remote answer...', name: 'CallManager');
await _webrtcService!.setRemoteAnswer(answerSdp);
log('✅ [EVENT] Remote answer set successfully', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [EVENT] Error handling answer: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
}
}
void _handleIceCandidate(List<Object?>? args) async {
log('📞 [EVENT] OnIceCandidateAsync received', name: 'CallManager');
if (args == null || args.isEmpty) {
log('⚠️ [EVENT] No ICE candidate data received', name: 'CallManager');
return;
}
try {
final candidateJson = args[0] as String?;
if (candidateJson == null) {
log('⚠️ [EVENT] Candidate JSON is null', name: 'CallManager');
return;
}
if (_webrtcService == null) {
log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager');
return;
}
final candidateData = jsonDecode(candidateJson) as Map<String, dynamic>;
final candidate = RTCIceCandidate(
candidateData['candidate'] as String?,
candidateData['sdpMid'] as String?,
candidateData['sdpMLineIndex'] as int?,
);
log('🧊 [EVENT] Adding remote ICE candidate...', name: 'CallManager');
await _webrtcService!.addIceCandidate(candidate);
log('✅ [EVENT] Remote ICE candidate added', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [EVENT] Error handling ICE candidate: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
}
}
void _handleAudioToggle(List<Object?>? args) {
log('📞 [EVENT] OnAudioToggle received', name: 'CallManager');
log(' Peer muted: $_isPeerMuted -> ${!_isPeerMuted}', name: 'CallManager');
_isPeerMuted = !_isPeerMuted;
notifyListeners();
}
void _handleCameraToggle(List<Object?>? args) {
log('📞 [EVENT] OnCameraToggle received', name: 'CallManager');
log(' Peer camera: $_isPeerCameraOn -> ${!_isPeerCameraOn}', name: 'CallManager');
_isPeerCameraOn = !_isPeerCameraOn;
notifyListeners();
}
// ==================== CallKit Event Handlers ====================
void _handleCallKitAccepted(String callId) {
log('✅ [CALLKIT] Call accepted: $callId', name: 'CallManager');
if (_currentCall?.callId == callId) {
acceptCall();
} else {
log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager');
}
}
void _handleCallKitDeclined(String callId) {
log('❌ [CALLKIT] Call declined: $callId', name: 'CallManager');
if (_currentCall?.callId == callId) {
declineCall('user_declined');
} else {
log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager');
}
}
void _handleCallKitEnded(String callId) {
log('🔴 [CALLKIT] Call ended: $callId', name: 'CallManager');
if (_currentCall?.callId == callId) {
hangUp();
} else {
log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager');
}
}
void _handleCallKitTimeout(String callId) {
log('⏱️ [CALLKIT] Call timeout: $callId', name: 'CallManager');
if (_currentCall?.callId == callId) {
_handleCallTimeout();
} else {
log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager');
}
}
}

@ -0,0 +1,277 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:signalr_netcore/hub_connection.dart';
import 'package:signalr_netcore/signalr_client.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
/// Singleton SignalR service that manages the single HubConnection
/// Both ChatProvider and CallManager use this service
class SignalRService {
static final SignalRService _instance = SignalRService._internal();
factory SignalRService() => _instance;
SignalRService._internal();
// Single HubConnection instance
HubConnection? _hubConnection;
// Connection state
bool _isInitializing = false;
// Authentication data
String? _userId;
String? _authToken;
String? _currentConversationId;
// Event handler registrations
final Map<String, List<Function(List<Object?>?)>> _eventHandlers = {};
/// Get the current HubConnection
HubConnection? get hubConnection => _hubConnection;
/// Check if connected
bool get isConnected => _hubConnection?.state == HubConnectionState.Connected;
/// Get current connection state
HubConnectionState? get connectionState => _hubConnection?.state;
/// Initialize SignalR connection with authentication
Future<bool> initialize({
required String userId,
required String authToken,
String? conversationId,
}) async {
try {
log('═══════════════════════════════════════════', name: 'SignalRService');
log('🔌 [SIGNALR] Initializing SignalR connection...', name: 'SignalRService');
log(' User ID: $userId', name: 'SignalRService');
log(' Conversation ID: ${conversationId ?? "none"}', name: 'SignalRService');
// Prevent multiple simultaneous initializations
if (_isInitializing) {
log('⏳ [SIGNALR] Already initializing, waiting...', name: 'SignalRService');
// Wait for current initialization to complete
final timeout = DateTime.now().add(const Duration(seconds: 5));
while (_isInitializing && DateTime.now().isBefore(timeout)) {
await Future.delayed(const Duration(milliseconds: 100));
}
return isConnected;
}
// If already connected with same credentials, reuse connection
if (isConnected && _userId == userId && _authToken == authToken) {
log('✅ [SIGNALR] Already connected with same credentials, reusing connection', name: 'SignalRService');
// Join new conversation if provided and different
if (conversationId != null && conversationId != _currentConversationId) {
await _joinConversation(conversationId);
}
return true;
}
_isInitializing = true;
// Store credentials
_userId = userId;
_authToken = authToken;
_currentConversationId = conversationId;
// Dispose existing connection if any
await _disposeConnection();
// Create new connection
final httpOp = HttpConnectionOptions(
skipNegotiation: false,
logMessageContent: kDebugMode,
);
_hubConnection = HubConnectionBuilder()
.withUrl(
"${URLs.chatHubUrlChat}?UserId=$userId&source=Desktop&access_token=$authToken",
options: httpOp,
)
.withAutomaticReconnect(retryDelays: <int>[2000, 5000, 10000, 20000])
.build();
// Setup reconnection handlers
_setupReconnectionHandlers();
// Start connection
await _hubConnection!.start();
log('✅ [SIGNALR] Connection established', name: 'SignalRService');
log(' Connection ID: ${_hubConnection!.connectionId}', name: 'SignalRService');
// Join conversation if provided
if (conversationId != null) {
await _joinConversation(conversationId);
}
// Re-register all event handlers
_reregisterAllHandlers();
_isInitializing = false;
log('═══════════════════════════════════════════', name: 'SignalRService');
return true;
} catch (e, stackTrace) {
log('❌ [SIGNALR] Error initializing connection: $e',
name: 'SignalRService', error: e, stackTrace: stackTrace);
_isInitializing = false;
return false;
}
}
/// Setup reconnection handlers
void _setupReconnectionHandlers() {
if (_hubConnection == null) return;
_hubConnection!.onclose(({Exception? error}) {
log('🔴 [SIGNALR] Connection closed: $error', name: 'SignalRService');
});
_hubConnection!.onreconnecting(({Exception? error}) {
log('🟡 [SIGNALR] Reconnecting: $error', name: 'SignalRService');
});
_hubConnection!.onreconnected(({String? connectionId}) async {
log('🟢 [SIGNALR] Reconnected: $connectionId', name: 'SignalRService');
// Rejoin conversation if we had one
if (_currentConversationId != null) {
await _joinConversation(_currentConversationId!);
}
// Re-register all event handlers
_reregisterAllHandlers();
});
}
/// Join a conversation
Future<void> _joinConversation(String conversationId) async {
try {
if (_hubConnection?.state != HubConnectionState.Connected) {
log('⚠️ [SIGNALR] Cannot join conversation - not connected', name: 'SignalRService');
return;
}
await _hubConnection!.invoke("JoinConversation", args: [conversationId]);
_currentConversationId = conversationId;
log('✅ [SIGNALR] Joined conversation: $conversationId', name: 'SignalRService');
} catch (e) {
log('❌ [SIGNALR] Error joining conversation: $e', name: 'SignalRService');
}
}
/// Register an event handler
void on(String eventName, Function(List<Object?>?) handler) {
log('🔧 [SIGNALR] Registering handler for: $eventName', name: 'SignalRService');
// Store handler for re-registration after reconnect
if (!_eventHandlers.containsKey(eventName)) {
_eventHandlers[eventName] = [];
}
_eventHandlers[eventName]!.add(handler);
// Register with SignalR if connected
if (_hubConnection != null) {
_hubConnection!.on(eventName, handler);
}
}
/// Unregister an event handler
void off(String eventName, [Function(List<Object?>?)? handler]) {
log('🔧 [SIGNALR] Unregistering handler for: $eventName', name: 'SignalRService');
if (handler != null) {
_eventHandlers[eventName]?.remove(handler);
if (_eventHandlers[eventName]?.isEmpty ?? false) {
_eventHandlers.remove(eventName);
}
} else {
_eventHandlers.remove(eventName);
}
// Unregister from SignalR if connected
if (_hubConnection != null) {
_hubConnection!.off(eventName, method: handler);
}
}
/// Re-register all event handlers (after reconnection)
void _reregisterAllHandlers() {
if (_hubConnection == null) return;
log('🔄 [SIGNALR] Re-registering ${_eventHandlers.length} event handlers...', name: 'SignalRService');
for (final entry in _eventHandlers.entries) {
final eventName = entry.key;
final handlers = entry.value;
for (final handler in handlers) {
_hubConnection!.on(eventName, handler);
}
}
log('✅ [SIGNALR] All event handlers re-registered', name: 'SignalRService');
}
/// Invoke a SignalR method
Future<Object?> invoke(String methodName, {List<Object>? args}) async {
if (_hubConnection?.state != HubConnectionState.Connected) {
throw Exception('SignalR not connected. Current state: ${_hubConnection?.state}');
}
log('📤 [SIGNALR] Invoking: $methodName', name: 'SignalRService');
return await _hubConnection!.invoke(methodName, args: args);
}
/// Ensure connection is ready (connect if needed)
Future<bool> ensureConnected() async {
if (isConnected) {
return true;
}
if (_userId != null && _authToken != null) {
return await initialize(
userId: _userId!,
authToken: _authToken!,
conversationId: _currentConversationId,
);
}
log('❌ [SIGNALR] Cannot reconnect - no credentials stored', name: 'SignalRService');
return false;
}
/// Dispose connection
Future<void> _disposeConnection() async {
try {
if (_hubConnection != null) {
await _hubConnection!.stop();
_hubConnection = null;
log('✅ [SIGNALR] Connection disposed', name: 'SignalRService');
}
} catch (e) {
log('⚠️ [SIGNALR] Error disposing connection: $e', name: 'SignalRService');
}
}
/// Reset service (for logout)
Future<void> reset() async {
log('🔄 [SIGNALR] Resetting service...', name: 'SignalRService');
await _disposeConnection();
_eventHandlers.clear();
_userId = null;
_authToken = null;
_currentConversationId = null;
_isInitializing = false;
log('✅ [SIGNALR] Service reset complete', name: 'SignalRService');
}
}
Loading…
Cancel
Save