import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'package:flutter/material.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'; import 'package:test_sa/modules/cx_module/chat/services/pending_call_storage.dart'; import 'package:test_sa/modules/cx_module/chat/services/call_coordinator.dart'; class CallManager extends ChangeNotifier { static final CallManager _instance = CallManager._internal(); factory CallManager() => _instance; CallManager._internal(); // Services - Retrieved from DI SignalRService get _signalRService => getIt(); WebRTCService get _webrtcService => getIt(); 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 // This preserves the COMPLETE notification payload Map? _pendingCallData; // CRITICAL FIX: Navigation pending flag for background calls // When app is in background and call is accepted, we set this flag // The main UI will check this flag when it resumes and navigate bool _navigationPending = false; // CRITICAL FIX: WebRTC initialization synchronization Completer? _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; // CRITICAL: Public getter for navigation pending flag bool get isNavigationPending => _navigationPending; /// Initialize CallManager with user credentials Future initialize({ required String userId, required String authToken, String? conversationId, String? moduleId, String? referenceId, String? employeeNumber, }) async { try { // 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 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) { _conversationId = conversationId; _moduleId = moduleId; _referenceId = referenceId; if (conversationChanged && conversationId != null) { await _signalRService.initialize( userId: userId, authToken: authToken, conversationId: conversationId, ); } } if (_myEmployeeNumber != employeeNumber) { _myEmployeeNumber = employeeNumber; } return; } // 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(); } } catch (e, stackTrace) { log('❌ [CallManager] Initialization error: $e\nStack: $stackTrace', name: 'CallManager'); rethrow; } } /// Initialize CallKit service Future _initializeCallKit() async { if (_callKitInitialized) { return; } try { await _callKitService.initialize(); _callKitService.onCallAccepted = _handleCallKitAccepted; _callKitService.onCallDeclined = _handleCallKitDeclined; _callKitService.onCallEnded = _handleCallKitEnded; _callKitService.onCallTimeout = _handleCallKitTimeout; _callKitInitialized = true; } catch (e, stackTrace) { log('❌ [CallKit] Initialization error: $e\nStack: $stackTrace', name: 'CallManager'); } } /// 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; } /// Start outgoing call Future startCall({ required String peerId, required String peerName, required CallType callType, String? peerAvatar, }) async { try { if (_userId == null || _authToken == null) { final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showGenericError(context, 'Call system not ready. Please try again.'); } return; } if (!_handlersRegistered) { _registerCallHandlers(); } _updateCallStatus(CallStatus.connecting); if (!await _signalRService.ensureConnected()) { _updateCallStatus(CallStatus.idle); final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showSignalRNotConnected(context); } return; } final callId = const Uuid().v4(); _currentCall = CallSession( callId: callId, type: callType, direction: CallDirection.outgoing, peerId: peerId, peerName: peerName, peerAvatar: peerAvatar, startTime: DateTime.now(), ); await _signalRService.invoke( 'CallUserAsync', args: [ _myEmployeeNumber ?? '', peerId, callType == CallType.video, ], ); if (_currentCall?.callId != callId || _callStatus == CallStatus.idle) { return; } _updateCallStatus(CallStatus.outgoingRinging); await _initializeWebRTC(); if (_currentCall?.callId != callId || _callStatus == CallStatus.idle) { return; } _callTimeoutTimer = Timer(const Duration(seconds: 60), () { if (_callStatus == CallStatus.outgoingRinging) { _cleanup(); final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showGenericError(context, 'Call timeout - no answer'); } } }); _navigateToCallScreen(); } catch (e, stackTrace) { log('❌ [OUTGOING] Error starting call: $e', name: 'CallManager', error: e, stackTrace: stackTrace); await _cleanup(); final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showGenericError(context, 'Failed to start call: $e'); } } } /// Handle incoming call from notification Future handleIncomingCallNotification({ required String callId, required String callerId, required String callerName, required bool isVideoCall, Map? extraData, }) async { try { _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(), }; if (_userId == null || _authToken == null) { if (_cachedUserId != null && _cachedAuthToken != null && _cachedEmployeeNumber != null) { await initialize( userId: _cachedUserId!, authToken: _cachedAuthToken!, conversationId: extraData?['conversationId'] as String?, moduleId: extraData?['moduleId'] as String?, referenceId: extraData?['referenceId'] as String?, employeeNumber: _cachedEmployeeNumber, ); } else if (navigatorKey.currentContext != null) { try { final chatProvider = Provider.of(navigatorKey.currentContext!, listen: false); final chatLoginResponse = chatProvider.chatLoginResponse; final sender = chatProvider.sender; if (chatLoginResponse != null) { 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, ); } else { return; } } catch (e) { return; } } else { return; } } if (_callStatus != CallStatus.idle) { if (!await _signalRService.ensureConnected()) { return; } await _signalRService.invoke('CallDeclinedAsync', args: [ _myEmployeeNumber ?? '', callerId, 'CALLER_BUSY', ]); return; } if (!await _signalRService.ensureConnected()) { final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showSignalRNotConnected(context); } return; } _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); if (!_callKitInitialized) { await _initializeCallKit(); } await _callKitService.showIncomingCall( callId: callId, callerName: callerName, callerNumber: callerId, isVideo: isVideoCall, extra: extraData, ); _callTimeoutTimer = Timer(const Duration(seconds: 30), () async { if (_callStatus == CallStatus.incomingRinging) { await _handleCallTimeout(); } }); } catch (e, stackTrace) { log('❌ [INCOMING] Error handling incoming call: $e', name: 'CallManager', error: e, stackTrace: stackTrace); await _cleanup(); } } /// Handle call timeout (for incoming calls) Future _handleCallTimeout() async { try { if (_currentCall == null) return; if (await _signalRService.ensureConnected()) { await _signalRService.invoke('CallMissedAsync', args: [ _myEmployeeNumber ?? '', _currentCall!.peerId, _moduleId ?? '', ]); } await _cleanup(); } catch (e, stackTrace) { log('❌ [TIMEOUT] Error: $e', name: 'CallManager', error: e, stackTrace: stackTrace); await _cleanup(); } } /// Accept incoming call Future acceptCall() async { try { log('═══════════════════════════════════════════', name: 'CallManager'); log('✅ [ACCEPT] ▶️ ACCEPT CALL STARTED', name: 'CallManager'); log('✅ [ACCEPT] Timestamp: ${DateTime.now().toIso8601String()}', 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'); await _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'); // CRITICAL FIX: Check context availability final hasContext = navigatorKey.currentContext != null; log(' App lifecycle state: ${WidgetsBinding.instance.lifecycleState?.name ?? "NULL"}', name: 'CallManager'); if (hasContext) { log(' Context widget: ${navigatorKey.currentContext!.widget.runtimeType}', name: 'CallManager'); } else { log(' Context available: FALSE (background acceptance)', name: 'CallManager'); } if (hasContext) { log('✅ [ACCEPT] App in foreground with context', name: 'CallManager'); } else { log(' Lifecycle state: ${WidgetsBinding.instance.lifecycleState?.name ?? "null"}', name: 'CallManager'); } // CRITICAL: Ensure SignalR connected before accepting log('🔌 [ACCEPT] Ensuring SignalR connection...', name: 'CallManager'); if (!await _signalRService.ensureConnected()) { log('❌ [ACCEPT] SignalR connection failed', name: 'CallManager'); final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showSignalRNotConnected(context); } await _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'); // CRITICAL: Try parsing moduleId and referenceId as integers if they're numeric strings final moduleIdValue = _moduleId ?? '0'; final referenceIdValue = _referenceId ?? '0'; // Try to parse as int if possible (backend might expect int instead of string) final moduleIdArg = int.tryParse(moduleIdValue) ?? moduleIdValue; final referenceIdArg = int.tryParse(referenceIdValue) ?? referenceIdValue; log(' Module ID arg type: ${moduleIdArg.runtimeType} (value: $moduleIdArg)', name: 'CallManager'); log(' Reference ID arg type: ${referenceIdArg.runtimeType} (value: $referenceIdArg)', name: 'CallManager'); try { await _signalRService.invoke('AnswerCallAsync', args: [ _myEmployeeNumber ?? '', _currentCall!.peerId, moduleIdArg, // referenceIdArg, // _conversationId ?? '', ]); log('✅ [ACCEPT] AnswerCallAsync invoked successfully', name: 'CallManager'); } catch (answerError) { log('❌ [ACCEPT] AnswerCallAsync FAILED on server', name: 'CallManager'); // Cleanup and show user-friendly error await _cleanup(); final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showGenericError( context, 'Unable to accept call. The caller may have ended the call or there may be a connection issue.', ); } // Rethrow to be caught by outer catch block rethrow; } // 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'); log(' WebRTC negotiation can proceed in background', name: 'CallManager'); // CRITICAL FIX: Navigation logic with comprehensive logging log('═══════════════════════════════════════════', name: 'CallManager'); log('🧭 [ACCEPT] ▶️ NAVIGATION PHASE STARTED', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager'); if (!hasContext) { log('🔄 [NAV] App was in BACKGROUND - setting navigation pending flag', name: 'CallManager'); log(' Current time: ${DateTime.now().toIso8601String()}', name: 'CallManager'); log(' Lifecycle state: ${WidgetsBinding.instance.lifecycleState?.name ?? "NULL"}', name: 'CallManager'); // CRITICAL FIX: Set navigation pending flag instead of trying to navigate from background isolate _navigationPending = true; log('✅ [NAV] Navigation pending flag SET', name: 'CallManager'); log(' Main UI will check this flag when app resumes', name: 'CallManager'); log(' Call handlePendingNavigation() from main UI lifecycle observer', name: 'CallManager'); } else { // Foreground: Navigate immediately log('🎯 [NAV] App IS in FOREGROUND - navigating immediately', name: 'CallManager'); log(' Current time: ${DateTime.now().toIso8601String()}', name: 'CallManager'); log(' Context available: YES', name: 'CallManager'); log(' Context type: ${navigatorKey.currentContext.runtimeType}', name: 'CallManager'); log('🚀 [NAV] Calling _navigateToCallScreen() [IMMEDIATE]...', name: 'CallManager'); try { _navigateToCallScreen(); log('✅ [NAV] _navigateToCallScreen() [IMMEDIATE] returned successfully', name: 'CallManager'); } catch (e, stackTrace) { log('❌ [NAV] Navigation error (foreground): $e\nStack: $stackTrace', name: 'CallManager'); } } log('═══════════════════════════════════════════', name: 'CallManager'); } catch (e, stackTrace) { log('❌ [ACCEPT] ❌ ERROR in acceptCall(): $e\nStack: $stackTrace', name: 'CallManager'); await _cleanup(); final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showGenericError(context, 'Failed to accept call: $e'); } } } /// Decline incoming call Future declineCall(String reason) async { try { if (_currentCall == null) { return; } _callTimeoutTimer?.cancel(); if (await _signalRService.ensureConnected()) { await _signalRService.invoke('CallDeclinedAsync', args: [ _myEmployeeNumber ?? '', _currentCall!.peerId, _moduleId ?? '', // reason, ]); } await _cleanup(); } catch (e, stackTrace) { log('❌ [DECLINE] Error declining call: $e', name: 'CallManager', error: e, stackTrace: stackTrace); await _cleanup(); } } /// Hang up call Future hangUp() async { try { if (_currentCall == null) { return; } final peerId = _currentCall!.peerId; try { if (!_signalRService.isConnected) { final reconnected = await _signalRService.ensureConnected(); if (!reconnected) { // Connection failed, continue with cleanup anyway } } if (_signalRService.isConnected) { await _signalRService.invoke('HangUpAsync', args: [ _myEmployeeNumber ?? '', peerId, _moduleId ?? '', ]); } } catch (e) { // Continue with cleanup even if SignalR call fails } // try { // await _callKitService.endCall(callId); // } catch (e) { // // Ignore CallKit errors // } await _cleanup(); if (!_signalRService.isConnected) { await _signalRService.ensureConnected(); } } catch (e, stackTrace) { log('❌ [HANGUP] Error hanging up: $e', name: 'CallManager', error: e, stackTrace: stackTrace); await _cleanup(); try { await _signalRService.ensureConnected(); } catch (reconnectError) { // Ignore reconnect errors } } } /// Toggle mute Future toggleMute() async { _isMuted = !_isMuted; _webrtcService.setMicrophoneMuted(_isMuted); if (await _signalRService.ensureConnected()) { await _signalRService.invoke('AudioToggle', args: [ _myEmployeeNumber ?? '', _currentCall?.peerId ?? '', ]); } notifyListeners(); } /// Toggle speaker Future toggleSpeaker() async { _isSpeakerOn = !_isSpeakerOn; await _webrtcService.setSpeakerphoneEnabled(_isSpeakerOn); notifyListeners(); } /// Toggle camera Future toggleCamera() async { if (_currentCall?.type != CallType.video) return; _isCameraOn = !_isCameraOn; _webrtcService.setCameraEnabled(_isCameraOn); if (await _signalRService.ensureConnected()) { await _signalRService.invoke('CameraToggle', args: [ _myEmployeeNumber ?? '', _currentCall?.peerId ?? '', ]); } notifyListeners(); } /// Switch camera Future switchCamera() async { if (_currentCall?.type != CallType.video || !_isCameraOn) return; await _webrtcService.switchCamera(); } // ==================== Private Methods ==================== Future _initializeWebRTC() async { // Reset completer for new initialization _webrtcInitCompleter = Completer(); try { _setupWebRTCCallbacks(); // Initialize with timeout protection final initFuture = _currentCall!.type == CallType.audio ? _webrtcService.initializeForAudioCall() : _webrtcService.initializeForVideoCall(); // Add 10 second timeout for initialization await initFuture.timeout( const Duration(seconds: 10), onTimeout: () { throw TimeoutException('WebRTC initialization timed out'); }, ); if (_currentCall!.type == CallType.audio) { _isSpeakerOn = false; notifyListeners(); } // Complete the completer if (!_webrtcInitCompleter!.isCompleted) { _webrtcInitCompleter!.complete(); } // Process pending offer if any if (_pendingOfferSdp != null) { final offerSdp = _pendingOfferSdp!; _pendingOfferSdp = null; log('📥 [WEBRTC] Processing pending offer after initialization', name: 'CallManager'); try { final answer = await _webrtcService.createAnswer(offerSdp); await _signalRService.invoke('AnswerOfferAsync', args: [ _currentCall!.peerId, answer.sdp ?? '', _currentCall!.callId, ]); log('✅ [WEBRTC] Pending offer processed successfully', name: 'CallManager'); } catch (e, stackTrace) { log('❌ [WEBRTC] Error processing pending offer: $e\nStack: $stackTrace', name: 'CallManager'); } } } catch (e, stackTrace) { log('❌ [WEBRTC] Initialization error: $e\nStack: $stackTrace', name: 'CallManager'); if (!_webrtcInitCompleter!.isCompleted) { _webrtcInitCompleter!.completeError(e, stackTrace); } rethrow; } } void _setupWebRTCCallbacks() { _webrtcService.onIceCandidate = (RTCIceCandidate candidate) { 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) { notifyListeners(); }; _webrtcService.onIceConnectionStateChange = (RTCIceConnectionState state) { if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) { _updateCallStatus(CallStatus.connected); _startCallDurationTimer(); } else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) { _handleIceConnectionFailure(); } else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) { _handleIceDisconnection(); } }; } void _navigateToCallScreen() { final context = navigatorKey.currentContext; if (context == null || _currentCall == null) { log('⚠️ [NAV] Cannot navigate - context or call is null', name: 'CallManager'); return; } try { // Check if we're already on a call screen to prevent duplicate navigation final currentRoute = ModalRoute.of(context); if (currentRoute != null && (currentRoute.settings.name == '/audio_call' || currentRoute.settings.name == '/video_call')) { log('⚠️ [NAV] Already on call screen, skipping navigation', name: 'CallManager'); return; } final route = MaterialPageRoute( settings: RouteSettings(name: _currentCall?.type == CallType.audio ? '/audio_call' : '/video_call'), builder: (context) => _currentCall?.type == CallType.audio ? const AudioCallPage() : const VideoCallPage(), ); Navigator.of(context).push(route).then((result) { // Navigation completed successfully log('✅ [NAV] Navigation completed', name: 'CallManager'); }).catchError((error, stackTrace) { log('❌ [NAV] Navigation future error: $error\nStack: $stackTrace', name: 'CallManager'); }); } catch (e, stackTrace) { log('❌ [NAV] ❌ NAVIGATION EXCEPTION: $e\nStack: $stackTrace', name: 'CallManager'); } } void _updateCallStatus(CallStatus newStatus) { if (_callStatus == newStatus) return; _callStatus = newStatus; notifyListeners(); } void _startCallDurationTimer() { _callDurationTimer?.cancel(); _callDuration = Duration.zero; _callDurationTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _callDuration = Duration(seconds: _callDuration.inSeconds + 1); notifyListeners(); }); } Future _cleanup() async { _callTimeoutTimer?.cancel(); _callDurationTimer?.cancel(); // Clear pending state _navigationPending = false; _pendingCallData = null; _pendingOfferSdp = null; _webrtcInitCompleter = null; try { await _webrtcService.dispose(); } catch (e) { log('⚠️ [CLEANUP] WebRTC dispose error: $e', name: 'CallManager'); } if (_currentCall != null) { try { await _callKitService.endCall(_currentCall!.callId); } catch (e) { log('⚠️ [CLEANUP] CallKit end error: $e', name: 'CallManager'); } } _currentCall = null; _updateCallStatus(CallStatus.idle); _callDuration = Duration.zero; _isMuted = false; _isSpeakerOn = false; _isCameraOn = true; _isPeerMuted = false; _isPeerCameraOn = true; } /// Reset service (for logout or testing) Future reset() async { await _cleanup(); _callKitInitialized = false; _handlersRegistered = false; _moduleId = null; _referenceId = null; _conversationId = null; _myEmployeeNumber = null; } // ==================== SignalR Event Handlers ==================== void _handleIncomingCall(List? args) { if (args == null || args.isEmpty) { return; } try { final callData = args[0] as Map; final callId = callData['callId']?.toString() ?? ''; final callerEmployeeNumber = callData['sourceUserId']?.toString() ?? callData['employeeNumber']?.toString() ?? ''; final callerName = callData['userName']?.toString() ?? callData['id']?.toString() ?? 'Unknown Caller'; final isVideoCall = callData['isVideoCall'] as bool? ?? false; if (_currentCall?.callId == callId) { return; } _currentCall = CallSession( callId: callId, peerId: callerEmployeeNumber, peerName: callerName, type: isVideoCall ? CallType.video : CallType.audio, direction: CallDirection.incoming, startTime: DateTime.now(), ); _updateCallStatus(CallStatus.incomingRinging); } catch (e, stackTrace) { log('❌ [EVENT] Failed to set up incoming call: $e\nStack: $stackTrace', name: 'CallManager'); } } void _handleCallAccepted(List? args) async { if (_currentCall?.direction != CallDirection.outgoing) { return; } if (_callStatus != CallStatus.connecting && _callStatus != CallStatus.outgoingRinging) { return; } if (_callStatus == CallStatus.connecting) { _updateCallStatus(CallStatus.outgoingRinging); } _callTimeoutTimer?.cancel(); _updateCallStatus(CallStatus.connecting); try { final offer = await _webrtcService.createOffer(); await _signalRService.invoke('OfferAsync', args: [ _currentCall!.peerId, offer.sdp ?? '', _currentCall!.callId, ]); } catch (e, stackTrace) { log('❌ [EVENT] Error handling call accepted: $e', name: 'CallManager', error: e, stackTrace: stackTrace); await _cleanup(); } } void _handleCallDeclined(List? args) async { String? reason; if (args != null && args.isNotEmpty) { reason = args[0]?.toString(); } 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'); } } await _cleanup(); } void _handleHangUp(List? args) async { // if (_currentCall != null) { // try { // await _callKitService.endCall(_currentCall!.callId); // } catch (e) { // // Ignore errors // } // } await _cleanup(); if (!_signalRService.isConnected) { try { await _signalRService.ensureConnected(); } catch (e) { // Ignore errors } } } void _handleOffer(List? args) async { if (args == null || args.isEmpty) { log('⚠️ [EVENT] Offer received but args empty', 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('📥 [EVENT] Offer received', name: 'CallManager'); // If WebRTC is still initializing, queue the offer if (_webrtcInitCompleter != null && !_webrtcInitCompleter!.isCompleted) { log('⏳ [EVENT] WebRTC still initializing, queuing offer', name: 'CallManager'); _pendingOfferSdp = offerSdp; return; } // Wait for initialization to complete with timeout if (_webrtcInitCompleter != null) { try { await _webrtcInitCompleter!.future.timeout( const Duration(seconds: 5), onTimeout: () { throw TimeoutException('Waiting for WebRTC initialization timed out'); }, ); } catch (e) { log('❌ [EVENT] Error waiting for WebRTC init: $e', name: 'CallManager'); return; } } // Verify peer connection is ready if (!_webrtcService.isPeerConnectionInitialized) { log('⚠️ [EVENT] PeerConnection not initialized, cannot process offer', name: 'CallManager'); return; } log('🔄 [EVENT] Creating answer for offer', name: 'CallManager'); final answer = await _webrtcService.createAnswer(offerSdp); log('📤 [EVENT] Sending answer to peer', 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? args) async { if (args == null || args.isEmpty) { log('⚠️ [EVENT] Answer received but args empty', 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('📥 [EVENT] Answer received, 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? args) async { if (args == null || args.isEmpty) { return; } try { final candidateJson = args[0] as String?; if (candidateJson == null) { return; } final candidateData = jsonDecode(candidateJson) as Map; final candidate = RTCIceCandidate( candidateData['candidate'] as String?, candidateData['sdpMid'] as String?, candidateData['sdpMLineIndex'] as int?, ); await _webrtcService.addIceCandidate(candidate); } catch (e, stackTrace) { log('❌ [EVENT] Error handling ICE candidate: $e', name: 'CallManager', error: e, stackTrace: stackTrace); } } void _handleAudioToggle(List? args) { _isPeerMuted = !_isPeerMuted; notifyListeners(); } void _handleCameraToggle(List? args) { _isPeerCameraOn = !_isPeerCameraOn; notifyListeners(); } // ==================== CallKit Event Handlers ==================== void _handleCallKitAccepted(String callId) async { if (_currentCall?.callId == callId) { await acceptCall(); return; } if (_currentCall == null) { final pendingData = await PendingCallStorage.getPendingCall(); if (pendingData != null && pendingData['callId'] == callId) { _pendingCallData = pendingData; await acceptCall(); return; } } } void _handleCallKitDeclined(String callId) async { if (_currentCall?.callId == callId) { await declineCall('user_declined'); return; } if (_currentCall == null) { final pendingData = await PendingCallStorage.getPendingCall(); if (pendingData != null && pendingData['callId'] == callId) { await PendingCallStorage.clearPendingCall(); await PendingCallStorage.clearAcceptedCallMarker(); } } } void _handleCallKitEnded(String callId) { if (_currentCall?.callId == callId) { hangUp(); } } void _handleCallKitTimeout(String callId) { if (_currentCall?.callId == callId) { _handleCallTimeout(); } } // ==================== Additional SignalR Event Handlers ==================== void _handleUserOnline(List? args) { notifyListeners(); } void _handleUserOffline(List? args) { notifyListeners(); } void _handleCallHistoryUpdated(List? args) { // Forward to ChatProvider if needed } void _handleError(List? args) { if (args != null && args.isNotEmpty) { final error = args[0]?.toString() ?? 'Unknown error'; final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showGenericError(context, 'Call error: $error'); } } } // ==================== ICE Connection Failure Handlers ==================== /// Handle ICE connection failure Future _handleIceConnectionFailure() async { try { if (_currentCall != null && await _signalRService.ensureConnected()) { await _signalRService.invoke('HangUpAsync', args: [ _myEmployeeNumber ?? '', _currentCall!.peerId, _moduleId ?? '1', ]); } } catch (e) { // Ignore errors } await _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 _handleIceDisconnection() async { await Future.delayed(const Duration(seconds: 5)); final state = _webrtcService.getIceConnectionState(); if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) { await _handleIceConnectionFailure(); } } // ==================== PENDING CALL RESTORATION (Background/Terminated App) ==================== /// Check for pending calls on app resume Future checkForPendingCalls() async { try { final pendingCallData = await PendingCallStorage.getPendingCall(); if (pendingCallData == null) { return; } final acceptedCallId = await PendingCallStorage.getAcceptedCallId(); if (acceptedCallId == pendingCallData['callId']) { _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(), ); _pendingCallData = pendingCallData; final coordinator = CallCoordinator(); final initialized = await coordinator.initializeForIncomingCall( callerEmployeeNumber: pendingCallData['callerEmployeeNumber'] as String? ?? pendingCallData['callerId'] as String, conversationId: pendingCallData['conversationId'] as String?, moduleId: pendingCallData['moduleId'] as String?, referenceId: pendingCallData['referenceId'] as String?, ); if (initialized) { _navigationPending = true; } else { await PendingCallStorage.clearPendingCall(); await PendingCallStorage.clearAcceptedCallMarker(); } } } catch (e, stackTrace) { log('❌ [MAIN ISOLATE] Error checking pending calls: $e\nStack: $stackTrace', name: 'CallManager'); } } /// Handle pending navigation when app resumes Future handlePendingNavigation() async { try { if (!_navigationPending) { return; } if (_currentCall == null) { _navigationPending = false; return; } final hasContext = navigatorKey.currentContext != null; final hasState = navigatorKey.currentState != null; if (!hasContext || !hasState) { return; } await WidgetsBinding.instance.endOfFrame; _navigationPending = false; _navigateToCallScreen(); await PendingCallStorage.clearPendingCall(); await PendingCallStorage.clearAcceptedCallMarker(); } catch (e, stackTrace) { log('❌ [NAVIGATION] Error handling pending navigation: $e', name: 'CallManager', error: e, stackTrace: stackTrace); _navigationPending = false; } } }