You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cloudsolutions-atoms/lib/modules/cx_module/chat/services/call_manager.dart

1626 lines
68 KiB
Dart

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.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:test_sa/modules/cx_module/chat/chat_provider.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:uuid/uuid.dart';
import 'package:test_sa/core/di/service_locator.dart';
class CallManager extends ChangeNotifier {
static final CallManager _instance = CallManager._internal();
factory CallManager() => _instance;
CallManager._internal() {
log('🏗️ [CallManager] Singleton instance created (hashCode: $hashCode)', name: 'CallManager');
}
// Services - Retrieved from DI
SignalRService get _signalRService => getIt<SignalRService>();
WebRTCService get _webrtcService => getIt<WebRTCService>();
final CallKitService _callKitService = CallKitService();
// Public getter for WebRTC service (used by call pages and ChatProvider)
WebRTCService get 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? _userId;
String? _authToken;
String? _moduleId;
String? _referenceId;
String? _conversationId;
String? _myEmployeeNumber;
// CRITICAL FIX: Static credential cache for background calls
// This allows incoming calls to work even when app is in background
static String? _cachedUserId;
static String? _cachedAuthToken;
static String? _cachedEmployeeNumber;
// CRITICAL: Pending call data for background acceptance
Map<String, dynamic>? _pendingCallData;
// CRITICAL FIX: WebRTC initialization synchronization
Completer<void>? _webrtcInitCompleter;
String? _pendingOfferSdp; // Queue for offer that arrives during initialization
// 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;
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('🔧 [CallManager] Initializing...', name: 'CallManager');
// CRITICAL FIX: Cache credentials for background call handling
_cachedUserId = userId;
_cachedAuthToken = authToken;
_cachedEmployeeNumber = employeeNumber;
// CRITICAL FIX: Don't re-initialize if already initialized with same USER credentials
// conversationId can change (different chats) - that's OK, just update it
// Only re-initialize if USER changes (userId, authToken, employeeNumber)
// Compare employee numbers case-insitively (backend sometimes changes case)
if (_userId == userId &&
_authToken == authToken &&
_myEmployeeNumber?.toLowerCase() == employeeNumber?.toLowerCase() &&
_handlersRegistered) {
// User is the same, just update conversation context without reconnecting
final conversationChanged = _conversationId != conversationId;
final moduleChanged = _moduleId != moduleId;
final referenceChanged = _referenceId != referenceId;
if (conversationChanged || moduleChanged || referenceChanged) {
log('🔄 [CallManager] Updating context (no reconnection needed)', name: 'CallManager');
log(' Conversation: $_conversationId -> $conversationId', name: 'CallManager');
log(' Module: $_moduleId -> $moduleId', name: 'CallManager');
log(' Reference: $_referenceId -> $referenceId', name: 'CallManager');
// Update context
_conversationId = conversationId;
_moduleId = moduleId;
_referenceId = referenceId;
// Update SignalR conversation only if needed
if (conversationChanged && conversationId != null) {
await _signalRService.initialize(
userId: userId,
authToken: authToken,
conversationId: conversationId,
);
}
} else {
log('✅ [CallManager] Already initialized with same credentials - skipping', name: 'CallManager');
}
// Update employee number if case changed (keep latest version)
if (_myEmployeeNumber != employeeNumber) {
log('🔄 [CallManager] Updating employee number case: $_myEmployeeNumber -> $employeeNumber', name: 'CallManager');
_myEmployeeNumber = employeeNumber;
}
log(' User ID: $userId', name: 'CallManager');
log(' Employee Number: $employeeNumber', name: 'CallManager');
log(' Conversation ID: $conversationId', name: 'CallManager');
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
return;
}
log('🔄 [CallManager] User changed - full re-initialization', name: 'CallManager');
log(' Previous User: $_userId -> New: $userId', name: 'CallManager');
log(' Previous Employee: $_myEmployeeNumber -> New: $employeeNumber', name: 'CallManager');
// Store module context
_userId = userId;
_authToken = authToken;
_moduleId = moduleId;
_referenceId = referenceId;
_conversationId = conversationId;
_myEmployeeNumber = employeeNumber;
// Initialize SignalR connection
final connected = await _signalRService.initialize(
userId: userId,
authToken: authToken,
conversationId: conversationId,
);
if (!connected) {
throw Exception('Failed to initialize SignalR connection');
}
// Initialize CallKit
if (!_callKitInitialized) {
await _initializeCallKit();
}
// Register call event handlers
if (!_handlersRegistered) {
_registerCallHandlers();
}
log('✅ [CallManager] Initialized successfully', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [CallManager] Initialization error: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
rethrow;
}
}
/// Initialize CallKit service
Future<void> _initializeCallKit() async {
try {
await _callKitService.initialize();
// Setup CallKit callbacks
_callKitService.onCallAccepted = _handleCallKitAccepted;
_callKitService.onCallDeclined = _handleCallKitDeclined;
_callKitService.onCallEnded = _handleCallKitEnded;
_callKitService.onCallTimeout = _handleCallKitTimeout;
_callKitInitialized = true;
} catch (e, stackTrace) {
log('❌ [CallKit] Initialization error: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
}
}
/// Register SignalR call event handlers
void _registerCallHandlers() {
_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);
_signalRService.on('OnUserOnlineAsync', _handleUserOnline);
_signalRService.on('OnUserOfflineAsync', _handleUserOffline);
_signalRService.on('OnCallHistoryUpdated', _handleCallHistoryUpdated);
_signalRService.on('OnError', _handleError);
_handlersRegistered = true;
log('✅ [CallManager] Event handlers registered (13 events)', 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');
// CRITICAL: Verify CallManager is initialized
if (_userId == null || _authToken == null) {
log('❌ [OUTGOING] CallManager not initialized!', name: 'CallManager');
log(' _userId: ${_userId ?? "NULL"}', name: 'CallManager');
log(' _authToken: ${_authToken != null ? "SET" : "NULL"}', name: 'CallManager');
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(context, 'Call system not ready. Please try again.');
}
return;
}
// CRITICAL: Verify handlers are registered
if (!_handlersRegistered) {
log('❌ [OUTGOING] Call handlers not registered!', name: 'CallManager');
log(' Attempting to register handlers now...', name: 'CallManager');
_registerCallHandlers();
}
log('✅ [OUTGOING] CallManager initialized', name: 'CallManager');
log(' User ID: $_userId', name: 'CallManager');
log(' Employee Number: $_myEmployeeNumber', name: 'CallManager');
log(' Handlers registered: $_handlersRegistered', name: 'CallManager');
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
// 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');
// CRITICAL FIX: Check if call was declined/cancelled while we were waiting
if (_currentCall?.callId != callId || _callStatus == CallStatus.idle) {
log('⚠️ [OUTGOING] Call was cancelled/declined during setup - aborting', name: 'CallManager');
log(' Current call ID: ${_currentCall?.callId}', name: 'CallManager');
log(' Expected call ID: $callId', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
return;
}
_updateCallStatus(CallStatus.outgoingRinging);
// Initialize WebRTC
log('🔧 [OUTGOING] Initializing WebRTC...', name: 'CallManager');
await _initializeWebRTC();
log('✅ [OUTGOING] WebRTC initialized', name: 'CallManager');
// CRITICAL FIX: Check again after WebRTC initialization
if (_currentCall?.callId != callId || _callStatus == CallStatus.idle) {
log('⚠️ [OUTGOING] Call was cancelled/declined after WebRTC init - aborting', name: 'CallManager');
return;
}
// 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');
// CRITICAL: Store pending call data for background acceptance
_pendingCallData = {
'callId': callId,
'callerId': callerId,
'callerName': callerName,
'isVideoCall': isVideoCall,
'conversationId': extraData?['conversationId'],
'moduleId': extraData?['moduleId'],
'referenceId': extraData?['referenceId'],
'callerEmployeeNumber': extraData?['callerEmployeeNumber'],
'timestamp': DateTime.now().toIso8601String(),
};
log('💾 [INCOMING] Pending call data stored for background acceptance', name: 'CallManager');
// CRITICAL FIX: Auto-initialize CallManager if not already initialized
// This handles the case when app is in background and context is null
if (_userId == null || _authToken == null) {
log('⚠️ [INCOMING] CallManager not initialized - attempting auto-initialization...', name: 'CallManager');
// CRITICAL FIX: Try cached credentials first (for background calls)
if (_cachedUserId != null && _cachedAuthToken != null && _cachedEmployeeNumber != null) {
log('🔧 [INCOMING] Using cached credentials (app in background)', name: 'CallManager');
log(' Cached User ID: $_cachedUserId', name: 'CallManager');
log(' Cached Employee Number: $_cachedEmployeeNumber', name: 'CallManager');
await initialize(
userId: _cachedUserId!,
authToken: _cachedAuthToken!,
conversationId: extraData?['conversationId'] as String?,
moduleId: extraData?['moduleId'] as String?,
referenceId: extraData?['referenceId'] as String?,
employeeNumber: _cachedEmployeeNumber,
);
log('✅ [INCOMING] Auto-initialization successful (from cache)', name: 'CallManager');
}
// Try to get credentials from ChatProvider (app in foreground)
else if (navigatorKey.currentContext != null) {
try {
final chatProvider = Provider.of<ChatProvider>(navigatorKey.currentContext!, listen: false);
final chatLoginResponse = chatProvider.chatLoginResponse;
final sender = chatProvider.sender; // Get sender for employee number
if (chatLoginResponse != null) {
log('🔧 [INCOMING] Found credentials in ChatProvider', name: 'CallManager');
log(' User ID: ${chatLoginResponse.userId}', name: 'CallManager');
log(' Employee Number: ${sender?.employeeNumber ?? "NOT AVAILABLE"}', name: 'CallManager');
await initialize(
userId: chatLoginResponse.userId.toString(),
authToken: chatLoginResponse.token ?? '',
conversationId: extraData?['conversationId'] as String?,
moduleId: extraData?['moduleId'] as String?,
referenceId: extraData?['referenceId'] as String?,
employeeNumber: sender?.employeeNumber, // Get from sender participant
);
log('✅ [INCOMING] Auto-initialization successful (from ChatProvider)', name: 'CallManager');
} else {
log('❌ [INCOMING] No credentials available', name: 'CallManager');
return;
}
} catch (e) {
log('❌ [INCOMING] Auto-initialization failed: $e', name: 'CallManager');
return;
}
} else {
log('❌ [INCOMING] No cached credentials and no context available', name: 'CallManager');
log(' This means user never logged into chat in this session', name: 'CallManager');
return;
}
}
// 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');
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showSignalRNotConnected(context);
}
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');
// CRITICAL: Handle background acceptance scenario
if (_currentCall == null && _pendingCallData != null) {
log('🔧 [ACCEPT] No current call but pending data exists - restoring from background', name: 'CallManager');
log(' Pending call ID: ${_pendingCallData!['callId']}', name: 'CallManager');
log(' Pending caller: ${_pendingCallData!['callerName']}', name: 'CallManager');
// Restore call session from pending data
_currentCall = CallSession(
callId: _pendingCallData!['callId'] as String,
type: (_pendingCallData!['isVideoCall'] as bool) ? CallType.video : CallType.audio,
direction: CallDirection.incoming,
peerId: _pendingCallData!['callerId'] as String,
peerName: _pendingCallData!['callerName'] as String,
peerAvatar: null,
startTime: DateTime.now(),
);
_updateCallStatus(CallStatus.incomingRinging);
log('✅ [ACCEPT] Call session restored from pending data', name: 'CallManager');
// Ensure CallManager is initialized with pending call context
if (_userId == null || _authToken == null) {
log('⚠️ [ACCEPT] CallManager not initialized, attempting auto-initialization...', name: 'CallManager');
if (_cachedUserId != null && _cachedAuthToken != null && _cachedEmployeeNumber != null) {
await initialize(
userId: _cachedUserId!,
authToken: _cachedAuthToken!,
conversationId: _pendingCallData!['conversationId'] as String?,
moduleId: _pendingCallData!['moduleId'] as String?,
referenceId: _pendingCallData!['referenceId'] as String?,
employeeNumber: _cachedEmployeeNumber,
);
log('✅ [ACCEPT] CallManager initialized for background call', name: 'CallManager');
} else {
log('❌ [ACCEPT] Cannot initialize - no cached credentials', name: 'CallManager');
_cleanup();
return;
}
}
}
if (_currentCall == null || _callStatus != CallStatus.incomingRinging) {
log('⚠️ [ACCEPT] No incoming call to accept', name: 'CallManager');
log(' Current call: ${_currentCall?.callId}', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
return;
}
log('📊 [ACCEPT] Current call state:', name: 'CallManager');
log(' Call ID: ${_currentCall!.callId}', name: 'CallManager');
log(' Peer ID: ${_currentCall!.peerId}', name: 'CallManager');
log(' Peer Name: ${_currentCall!.peerName}', name: 'CallManager');
log(' Call Type: ${_currentCall!.type.name}', name: 'CallManager');
log(' Direction: ${_currentCall!.direction.name}', name: 'CallManager');
// 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');
// CRITICAL: Ensure SignalR connected before accepting
log('🔌 [ACCEPT] Ensuring SignalR connection...', name: 'CallManager');
log(' Current SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
if (!await _signalRService.ensureConnected()) {
log('❌ [ACCEPT] SignalR connection failed', name: 'CallManager');
log(' Final state: ${_signalRService.connectionState}', name: 'CallManager');
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showSignalRNotConnected(context);
}
_cleanup();
return;
}
log('✅ [ACCEPT] SignalR connected', name: 'CallManager');
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
// CRITICAL: Ensure handlers are registered
if (!_handlersRegistered) {
log('⚠️ [ACCEPT] Call handlers not registered, registering now...', name: 'CallManager');
_registerCallHandlers();
}
_updateCallStatus(CallStatus.connecting);
// Cancel timeout timer
_callTimeoutTimer?.cancel();
log('✅ [ACCEPT] Timeout timer cancelled', name: 'CallManager');
// Invoke AnswerCallAsync
log('📤 [ACCEPT] Invoking AnswerCallAsync...', name: 'CallManager');
log(' My Employee Number: ${_myEmployeeNumber ?? "NULL"}', name: 'CallManager');
log(' Peer ID: ${_currentCall!.peerId}', name: 'CallManager');
log(' Module ID: ${_moduleId ?? "0"}', name: 'CallManager');
log(' Reference ID: ${_referenceId ?? "0"}', name: 'CallManager');
log(' Conversation ID: ${_conversationId ?? ""}', name: 'CallManager');
await _signalRService.invoke('AnswerCallAsync', args: [
_myEmployeeNumber ?? '',
_currentCall!.peerId,
_moduleId ?? '0',
_referenceId ?? '0',
_conversationId ?? '',
]);
log('✅ [ACCEPT] AnswerCallAsync invoked successfully', name: 'CallManager');
// Clear pending call data after successful accept
_pendingCallData = null;
// Initialize WebRTC
log('🔧 [ACCEPT] Initializing WebRTC...', name: 'CallManager');
log(' Call type: ${_currentCall!.type.name}', name: 'CallManager');
await _initializeWebRTC();
log('✅ [ACCEPT] WebRTC initialized', name: 'CallManager');
// Navigate to call screen
log('🧭 [ACCEPT] Navigating to call screen...', name: 'CallManager');
_navigateToCallScreen();
log('✅ [ACCEPT] Navigated to call screen', name: 'CallManager');
log('✅ [ACCEPT] Call accepted successfully', name: 'CallManager');
log(' [ACCEPT] Now waiting for caller to send offer via OnOfferAsync...', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [ACCEPT] Error accepting call: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(context, 'Failed to accept call: $e');
}
}
}
/// 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');
log(' Current call: ${_currentCall?.callId}', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
if (_currentCall == null) {
log('⚠️ [HANGUP] No call to hang up', name: 'CallManager');
return;
}
// Store call info before cleanup
final peerId = _currentCall!.peerId;
final callId = _currentCall!.callId;
// CRITICAL FIX: Verify SignalR connection state
log('🔍 [HANGUP] Verifying SignalR connection...', name: 'CallManager');
try {
// Check if connected, if not try to reconnect
if (!_signalRService.isConnected) {
log('⚠️ [HANGUP] SignalR not connected, attempting to reconnect...', name: 'CallManager');
final reconnected = await _signalRService.ensureConnected();
if (!reconnected) {
log('❌ [HANGUP] SignalR reconnection failed', name: 'CallManager');
} else {
log('✅ [HANGUP] SignalR reconnected successfully', name: 'CallManager');
}
}
// Try to invoke HangUpAsync if connected
if (_signalRService.isConnected) {
log('📤 [HANGUP] Invoking HangUpAsync...', name: 'CallManager');
log(' From: ${_myEmployeeNumber ?? ""}', name: 'CallManager');
log(' To: $peerId', name: 'CallManager');
log(' Module ID: ${_moduleId ?? "1"}', name: 'CallManager');
// CRITICAL FIX: Backend HangUpAsync expects 3 parameters:
// 1. fromEmployeeNumber (string)
// 2. toEmployeeNumber (string)
// 3. moduleId (int or string)
await _signalRService.invoke('HangUpAsync', args: [
_myEmployeeNumber ?? '',
peerId,
_moduleId ?? '1', // Add moduleId as 3rd parameter
]);
log('✅ [HANGUP] HangUpAsync invoked successfully', name: 'CallManager');
} else {
log('⚠️ [HANGUP] SignalR not connected after retry, skipping backend call', name: 'CallManager');
}
} catch (e) {
log('❌ [HANGUP] Error invoking HangUpAsync: $e', name: 'CallManager');
// Continue with cleanup even if SignalR call fails
}
// End CallKit call
log('📱 [HANGUP] Ending CallKit call...', name: 'CallManager');
try {
await _callKitService.endCall(callId);
log('✅ [HANGUP] CallKit call ended', name: 'CallManager');
} catch (e) {
log('⚠️ [HANGUP] Error ending CallKit: $e', name: 'CallManager');
}
// CRITICAL FIX: Don't manually navigate - let the call pages handle navigation
// Call pages listen to status changes and will pop when status becomes idle
log('🧭 [HANGUP] Navigation will be handled by call page listeners', name: 'CallManager');
// CRITICAL: Always cleanup to reset state for next call
log('🧹 [HANGUP] Starting cleanup...', name: 'CallManager');
_cleanup();
// CRITICAL FIX: Verify SignalR is still connected after cleanup
log('🔍 [HANGUP] Post-cleanup SignalR verification...', name: 'CallManager');
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
log(' SignalR connected: ${_signalRService.isConnected}', name: 'CallManager');
log(' CallManager status: ${_callStatus.name}', name: 'CallManager');
log(' Current call: ${_currentCall?.callId ?? "null"}', name: 'CallManager');
if (!_signalRService.isConnected) {
log('⚠️ [HANGUP] SignalR disconnected after cleanup, attempting reconnect...', name: 'CallManager');
await _signalRService.ensureConnected();
log(' Reconnect result: ${_signalRService.isConnected}', name: 'CallManager');
}
log('✅ [HANGUP] Call ended successfully', name: 'CallManager');
log(' SignalR ready for next call: ${_signalRService.isConnected}', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [HANGUP] Error hanging up: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
// CRITICAL FIX: Don't manually navigate in error case either
log('🧭 [HANGUP-ERROR] Navigation will be handled by call page listeners', name: 'CallManager');
_cleanup();
// Try to ensure SignalR is ready for next call
try {
await _signalRService.ensureConnected();
} catch (reconnectError) {
log('❌ [HANGUP] Failed to reconnect SignalR: $reconnectError', name: 'CallManager');
}
}
}
/// 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');
log(' Current call type: ${_currentCall!.type.name}', name: 'CallManager');
log(' WebRTC service hashCode: ${_webrtcService.hashCode}', name: 'CallManager');
// CRITICAL FIX: Create a completer to track initialization completion
_webrtcInitCompleter = Completer<void>();
try {
// CRITICAL FIX: Setup callbacks BEFORE initializing WebRTC
log('🔧 [WEBRTC] Setting up callbacks BEFORE initialization...', name: 'CallManager');
_setupWebRTCCallbacks();
log('✅ [WEBRTC] Callbacks set up', name: 'CallManager');
// Verify callback was actually set
log('🔍 [WEBRTC] Verifying callback assignment...', name: 'CallManager');
log(' onIceConnectionStateChange is null?: ${_webrtcService.onIceConnectionStateChange == null}', name: 'CallManager');
if (_currentCall!.type == CallType.audio) {
log('🎵 [WEBRTC] Initializing for audio call', name: 'CallManager');
await _webrtcService.initializeForAudioCall();
// CRITICAL FIX: Speaker is OFF by default in WebRTC service (earpiece mode)
// Update the CallManager state to reflect this
_isSpeakerOn = false;
log('✅ [WEBRTC] Speaker state synchronized: OFF (earpiece mode)', name: 'CallManager');
notifyListeners();
} else {
log('📹 [WEBRTC] Initializing for video call', name: 'CallManager');
await _webrtcService.initializeForVideoCall();
}
// Verify callback is still set after initialization
log('🔍 [WEBRTC] Post-initialization callback verification...', name: 'CallManager');
log(' onIceConnectionStateChange is null?: ${_webrtcService.onIceConnectionStateChange == null}', name: 'CallManager');
log(' Peer connection initialized?: ${_webrtcService.isPeerConnectionInitialized}', name: 'CallManager');
log('✅ [WEBRTC] WebRTC initialized', name: 'CallManager');
// CRITICAL FIX: Signal that WebRTC initialization is complete
if (!_webrtcInitCompleter!.isCompleted) {
_webrtcInitCompleter!.complete();
log('✅ [WEBRTC] Initialization completer signaled', name: 'CallManager');
}
// CRITICAL FIX: Process any pending offer that arrived during initialization
if (_pendingOfferSdp != null) {
log('📥 [WEBRTC] Processing pending offer that arrived during initialization...', name: 'CallManager');
final offerSdp = _pendingOfferSdp!;
_pendingOfferSdp = null;
// Process the offer now that WebRTC is ready
try {
log('🔧 [WEBRTC] Creating answer for pending offer...', name: 'CallManager');
final answer = await _webrtcService.createAnswer(offerSdp);
log('✅ [WEBRTC] Answer created for pending offer (length: ${answer.sdp?.length ?? 0})', name: 'CallManager');
log('📤 [WEBRTC] Sending pending answer via AnswerOfferAsync...', name: 'CallManager');
await _signalRService.invoke('AnswerOfferAsync', args: [
_currentCall!.peerId,
answer.sdp ?? '',
_currentCall!.callId,
]);
log('✅ [WEBRTC] Pending answer sent successfully', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [WEBRTC] Error processing pending offer: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
}
}
} catch (e, stackTrace) {
log('❌ [WEBRTC] Initialization error: $e', name: 'CallManager', error: e, stackTrace: stackTrace);
// Complete the completer with error
if (!_webrtcInitCompleter!.isCompleted) {
_webrtcInitCompleter!.completeError(e, stackTrace);
}
rethrow;
}
}
void _setupWebRTCCallbacks() {
log('🔧 [WEBRTC] Setting up callbacks...', name: 'CallManager');
log(' CallManager hashCode: $hashCode', name: 'CallManager');
log(' WebRTC service hashCode: ${_webrtcService.hashCode}', name: 'CallManager');
_webrtcService.onIceCandidate = (RTCIceCandidate candidate) {
log('🧊 [WEBRTC-CALLBACK] ICE candidate generated in CallManager', 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-CALLBACK] Remote stream received in CallManager', name: 'CallManager');
log(' Audio tracks: ${stream.getAudioTracks().length}', name: 'CallManager');
log(' Video tracks: ${stream.getVideoTracks().length}', name: 'CallManager');
notifyListeners();
};
_webrtcService.onIceConnectionStateChange = (RTCIceConnectionState state) {
log('═══════════════════════════════════════════', name: 'CallManager');
log('🔗 [WEBRTC-CALLBACK] ICE state change RECEIVED in CallManager!', name: 'CallManager');
log(' State: ${state.toString()}', name: 'CallManager');
log(' Current call status: ${_callStatus.name}', name: 'CallManager');
log(' CallManager hashCode: $hashCode', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) {
log('✅ [WEBRTC-CALLBACK] ICE connection established - updating status to CONNECTED', name: 'CallManager');
_updateCallStatus(CallStatus.connected);
_startCallDurationTimer();
log('✅ [WEBRTC-CALLBACK] Status updated and timer started', name: 'CallManager');
} else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) {
log('❌ [WEBRTC-CALLBACK] ICE connection failed', name: 'CallManager');
// CRITICAL FIX: Don't cleanup immediately - try to notify peer first
_handleIceConnectionFailure();
} else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
log('⚠️ [WEBRTC-CALLBACK] ICE connection disconnected', name: 'CallManager');
// CRITICAL FIX: Also handle disconnect gracefully
_handleIceDisconnection();
} else {
log(' [WEBRTC-CALLBACK] ICE state: ${state.toString()}', name: 'CallManager');
}
};
log('✅ [WEBRTC] Callbacks set up', name: 'CallManager');
log(' onIceCandidate set?: ${_webrtcService.onIceCandidate != null}', name: 'CallManager');
log(' onRemoteStream set?: ${_webrtcService.onRemoteStream != null}', name: 'CallManager');
log(' onIceConnectionStateChange set?: ${_webrtcService.onIceConnectionStateChange != null}', 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();
// Dispose WebRTC service (it's a singleton, just cleanup the peer connection)
_webrtcService.dispose();
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('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] ⚠️ OnIncomingCallAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Args: $args', name: 'CallManager');
log('═══════════════════════════════════════════', 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('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] ✅ OnCallAcceptedAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Args: $args', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
log(' Current call: ${_currentCall?.callId}', name: 'CallManager');
log(' Call direction: ${_currentCall?.direction.name}', name: 'CallManager');
log(' Peer: ${_currentCall?.peerId}', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
// CRITICAL: Only the CALLER should create an offer
// The RECEIVER should wait for OnOfferAsync
if (_currentCall?.direction != CallDirection.outgoing) {
log('⚠️ [EVENT] This is an INCOMING call - receiver should NOT create offer', name: 'CallManager');
log(' Current direction: ${_currentCall?.direction.name}', name: 'CallManager');
log(' ✅ Receiver will wait for OnOfferAsync from caller', name: 'CallManager');
return;
}
// FIXED: Accept event in both connecting and outgoingRinging states
// Backend might send OnCallAcceptedAsync very quickly before status changes to outgoingRinging
if (_callStatus != CallStatus.connecting && _callStatus != CallStatus.outgoingRinging) {
log('⚠️ [EVENT] Not in valid state for creating offer (current: ${_callStatus.name})', name: 'CallManager');
log(' Expected: connecting OR outgoingRinging', name: 'CallManager');
return;
}
// If we're still in connecting, update to outgoingRinging first
if (_callStatus == CallStatus.connecting) {
log('🔄 [EVENT] Status was connecting, updating to outgoingRinging first', name: 'CallManager');
_updateCallStatus(CallStatus.outgoingRinging);
}
_callTimeoutTimer?.cancel();
_updateCallStatus(CallStatus.connecting);
try {
// Ensure WebRTC is initialized before creating offer
// CALLER SIDE: Create and send offer
log('🔧 [EVENT] [CALLER] Creating SDP offer...', name: 'CallManager');
final offer = await _webrtcService.createOffer();
log('✅ [EVENT] [CALLER] SDP offer created (length: ${offer.sdp?.length ?? 0})', name: 'CallManager');
log(' Offer SDP type: ${offer.type}', name: 'CallManager');
log('📤 [EVENT] [CALLER] Sending offer via OfferAsync...', name: 'CallManager');
log(' To peer: ${_currentCall!.peerId}', name: 'CallManager');
log(' Call ID: ${_currentCall!.callId}', name: 'CallManager');
await _signalRService.invoke('OfferAsync', args: [
_currentCall!.peerId,
offer.sdp ?? '',
_currentCall!.callId,
]);
log('✅ [EVENT] [CALLER] Offer sent successfully via OfferAsync', 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('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] ❌ OnCallDeclinedAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Args: $args', name: 'CallManager');
log('═══════════════════════════════════════════', 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) async {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] 🔴 OnHangUpAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Args: $args', name: 'CallManager');
log(' Current call status: ${_callStatus.name}', name: 'CallManager');
log(' Current call ID: ${_currentCall?.callId}', name: 'CallManager');
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
// CRITICAL FIX: Don't manually navigate - call pages listen to status and auto-pop when idle
// Avoid double navigation which causes navigation errors
log('🧭 [EVENT-HANGUP] Skipping manual navigation - call page will auto-pop on status change', name: 'CallManager');
// End CallKit call first
if (_currentCall != null) {
log('📱 [EVENT-HANGUP] Ending CallKit call...', name: 'CallManager');
try {
await _callKitService.endCall(_currentCall!.callId);
log('✅ [EVENT-HANGUP] CallKit call ended', name: 'CallManager');
} catch (e) {
log('⚠️ [EVENT-HANGUP] Error ending CallKit: $e', name: 'CallManager');
}
}
// Cleanup call resources - this will set status to idle, triggering call page navigation
log('🧹 [EVENT-HANGUP] Starting cleanup...', name: 'CallManager');
_cleanup();
// CRITICAL FIX: Verify SignalR is still connected after cleanup
log('🔍 [EVENT-HANGUP] Post-cleanup SignalR verification...', name: 'CallManager');
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
log(' SignalR connected: ${_signalRService.isConnected}', name: 'CallManager');
log(' CallManager status: ${_callStatus.name}', name: 'CallManager');
// CRITICAL: Ensure SignalR is ready for next call
if (!_signalRService.isConnected) {
log('⚠️ [EVENT-HANGUP] SignalR disconnected after cleanup, attempting reconnect...', name: 'CallManager');
try {
final reconnected = await _signalRService.ensureConnected();
log(' Reconnect result: $reconnected', name: 'CallManager');
log(' SignalR state after reconnect: ${_signalRService.connectionState}', name: 'CallManager');
if (!reconnected) {
log('❌ [EVENT-HANGUP] Failed to reconnect SignalR!', name: 'CallManager');
log(' This will prevent next call from working', name: 'CallManager');
} else {
log('✅ [EVENT-HANGUP] SignalR reconnected successfully', name: 'CallManager');
}
} catch (e) {
log('❌ [EVENT-HANGUP] Error reconnecting SignalR: $e', name: 'CallManager');
}
} else {
log('✅ [EVENT-HANGUP] SignalR still connected after cleanup', name: 'CallManager');
}
log('✅ [EVENT-HANGUP] Hangup event processed', name: 'CallManager');
log(' Call page will auto-navigate when it detects idle status', name: 'CallManager');
log(' SignalR ready for next call: ${_signalRService.isConnected}', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
}
void _handleOffer(List<Object?>? args) async {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] 📥 OnOfferAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
log('═══════════════════════════════════════════', 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 || offerSdp.isEmpty) {
log('⚠️ [EVENT] Offer SDP is null or empty', name: 'CallManager');
return;
}
log(' Offer SDP length: ${offerSdp.length}', name: 'CallManager');
// CRITICAL FIX: Wait for WebRTC initialization to complete before processing offer
if (!_webrtcService.isFullyInitialized) {
log('⏳ [EVENT] Waiting for WebRTC Initialization', name: 'CallManager');
await _webrtcService.waitForInitialization();
log('✅ [EVENT] WebRTC Initialization Complete', name: 'CallManager');
}
// Verify peer connection is ready
if (!_webrtcService.isPeerConnectionInitialized) {
log('❌ [EVENT] Peer connection not initialized - cannot process offer', name: 'CallManager');
return;
}
log('🔧 [EVENT] Creating SDP answer...', name: 'CallManager');
final answer = await _webrtcService.createAnswer(offerSdp);
log('✅ [EVENT] SDP answer created (length: ${answer.sdp?.length ?? 0})', name: 'CallManager');
log(' Answer SDP type: ${answer.type}', name: 'CallManager');
log('📤 [EVENT] Sending answer via AnswerOfferAsync...', name: 'CallManager');
log(' To peer: ${_currentCall!.peerId}', name: 'CallManager');
log(' Call ID: ${_currentCall!.callId}', name: 'CallManager');
await _signalRService.invoke('AnswerOfferAsync', args: [
_currentCall!.peerId,
answer.sdp ?? '',
_currentCall!.callId,
]);
log('✅ [EVENT] Answer sent successfully via AnswerOfferAsync', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [EVENT] Error handling offer: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
}
}
void _handleAnswer(List<Object?>? args) async {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] 📥 OnAnswerOfferAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
log(' WebRTC initialized: ${_webrtcService != null}', name: 'CallManager');
log('═══════════════════════════════════════════', 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 || answerSdp.isEmpty) {
log('⚠️ [EVENT] Answer SDP is null or empty', name: 'CallManager');
return;
}
log(' Answer SDP length: ${answerSdp.length}', name: 'CallManager');
log('🔧 [EVENT] Setting remote answer...', name: 'CallManager');
await _webrtcService!.setRemoteAnswer(answerSdp);
log('✅ [EVENT] Remote answer set successfully', name: 'CallManager');
log(' Now waiting for ICE connection to establish...', 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;
}
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');
log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', 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');
}
}
// ==================== Additional SignalR Event Handlers ====================
void _handleUserOnline(List<Object?>? args) {
log('📞 [EVENT] 🟢 OnUserOnlineAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
if (args != null && args.isNotEmpty) {
log(' User online: ${args[0]}', name: 'CallManager');
}
log('═══════════════════════════════════════════', name: 'CallManager');
// Notify listeners in case UI needs to update user status
notifyListeners();
}
void _handleUserOffline(List<Object?>? args) {
log('📞 [EVENT] 🔴 OnUserOfflineAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
if (args != null && args.isNotEmpty) {
log(' User offline: ${args[0]}', name: 'CallManager');
}
log('═══════════════════════════════════════════', name: 'CallManager');
// Notify listeners in case UI needs to update user status
notifyListeners();
}
void _handleCallHistoryUpdated(List<Object?>? args) {
log('📞 [EVENT] 📋 OnCallHistoryUpdated received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
if (args != null && args.isNotEmpty) {
log(' Call history update: ${args[0]}', name: 'CallManager');
}
log('═══════════════════════════════════════════', name: 'CallManager');
// Forward to ChatProvider if needed (call history is typically managed there)
// This is just logging for now
}
void _handleError(List<Object?>? args) {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] ❌ OnError received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
if (args != null && args.isNotEmpty) {
final error = args[0]?.toString() ?? 'Unknown error';
log(' Error: $error', name: 'CallManager');
// Show error to user if we have context
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(context, 'Call error: $error');
}
}
log('═══════════════════════════════════════════', name: 'CallManager');
}
// ==================== ICE Connection Failure Handlers ====================
/// Handle ICE connection failure
Future<void> _handleIceConnectionFailure() async {
log('❌ [ICE] Connection failed - attempting graceful hangup...', name: 'CallManager');
try {
// Try to notify peer via HangUpAsync before cleanup
if (_currentCall != null && await _signalRService.ensureConnected()) {
log('📤 [ICE] Notifying peer about connection failure...', name: 'CallManager');
await _signalRService.invoke('HangUpAsync', args: [
_myEmployeeNumber ?? '',
_currentCall!.peerId,
_moduleId ?? '1',
]);
}
} catch (e) {
log('⚠️ [ICE] Failed to notify peer: $e', name: 'CallManager');
}
// Cleanup and show error to user
_cleanup();
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(
context,
'Connection failed. Please check your network and try again.',
);
}
}
/// Handle ICE connection disconnection
Future<void> _handleIceDisconnection() async {
log('⚠️ [ICE] Connection disconnected - waiting for reconnection...', name: 'CallManager');
// Wait a few seconds to see if connection recovers
await Future.delayed(const Duration(seconds: 5));
// Check if still disconnected
final state = _webrtcService.getIceConnectionState();
if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
log('❌ [ICE] Still disconnected after 5s - treating as failure', name: 'CallManager');
await _handleIceConnectionFailure();
} else {
log('✅ [ICE] Connection recovered: $state', name: 'CallManager');
}
}
}