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'; /// CallManager - Singleton service that manages all call responsibilities /// Works independently of ChatProvider /// Uses SignalRService for SignalR communication class CallManager extends ChangeNotifier { static final CallManager _instance = CallManager._internal(); factory CallManager() => _instance; CallManager._internal(); // Services final SignalRService _signalRService = SignalRService(); final CallKitService _callKitService = CallKitService(); WebRTCService? _webrtcService; // Call state CallSession? _currentCall; CallStatus _callStatus = CallStatus.idle; Duration _callDuration = Duration.zero; Timer? _callDurationTimer; Timer? _callTimeoutTimer; // Call controls bool _isMuted = false; bool _isSpeakerOn = false; bool _isCameraOn = true; bool _isPeerMuted = false; bool _isPeerCameraOn = true; // Initialization state bool _callKitInitialized = false; bool _handlersRegistered = false; // Module context (for SignalR invocations) String? _userId; String? _authToken; String? _moduleId; String? _referenceId; String? _conversationId; String? _myEmployeeNumber; // Getters CallSession? get currentCall => _currentCall; CallStatus get callStatus => _callStatus; Duration get callDuration => _callDuration; bool get isMuted => _isMuted; bool get isSpeakerOn => _isSpeakerOn; bool get isCameraOn => _isCameraOn; bool get isPeerMuted => _isPeerMuted; bool get isPeerCameraOn => _isPeerCameraOn; WebRTCService? get webrtcService => _webrtcService; bool get isCallInProgress => _callStatus != CallStatus.idle; /// Initialize CallManager with user credentials Future initialize({ required String userId, required String authToken, String? conversationId, String? moduleId, String? referenceId, String? employeeNumber, }) async { try { log('═══════════════════════════════════════════', name: 'CallManager'); log('🔧 [INIT] Initializing CallManager...', name: 'CallManager'); log(' User ID: $userId', name: 'CallManager'); log(' Conversation ID: ${conversationId ?? "none"}', name: 'CallManager'); // Store module context _userId = userId; _authToken = authToken; _moduleId = moduleId; _referenceId = referenceId; _conversationId = conversationId; _myEmployeeNumber = employeeNumber; // Initialize SignalR connection log('🔌 [INIT] Initializing SignalR connection...', name: 'CallManager'); final connected = await _signalRService.initialize( userId: userId, authToken: authToken, conversationId: conversationId, ); if (!connected) { throw Exception('Failed to initialize SignalR connection'); } log('✅ [INIT] SignalR connected', name: 'CallManager'); // Initialize CallKit if (!_callKitInitialized) { await _initializeCallKit(); } // Register call event handlers if (!_handlersRegistered) { _registerCallHandlers(); } log('✅ [INIT] CallManager initialized successfully', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager'); } catch (e, stackTrace) { log('❌ [INIT] Error initializing CallManager: $e', name: 'CallManager', error: e, stackTrace: stackTrace); rethrow; } } /// Initialize CallKit service Future _initializeCallKit() async { try { log('📞 [CALLKIT] Initializing CallKit...', name: 'CallManager'); await _callKitService.initialize(); // Setup CallKit callbacks _callKitService.onCallAccepted = _handleCallKitAccepted; _callKitService.onCallDeclined = _handleCallKitDeclined; _callKitService.onCallEnded = _handleCallKitEnded; _callKitService.onCallTimeout = _handleCallKitTimeout; _callKitInitialized = true; log('✅ [CALLKIT] CallKit initialized', name: 'CallManager'); } catch (e, stackTrace) { log('❌ [CALLKIT] Error initializing: $e', name: 'CallManager', error: e, stackTrace: stackTrace); } } /// Register SignalR call event handlers void _registerCallHandlers() { log('🔧 [HANDLERS] Registering call event handlers...', name: 'CallManager'); _signalRService.on('OnIncomingCallAsync', _handleIncomingCall); _signalRService.on('OnCallAcceptedAsync', _handleCallAccepted); _signalRService.on('OnCallDeclinedAsync', _handleCallDeclined); _signalRService.on('OnHangUpAsync', _handleHangUp); _signalRService.on('OnOfferAsync', _handleOffer); _signalRService.on('OnAnswerOfferAsync', _handleAnswer); _signalRService.on('OnIceCandidateAsync', _handleIceCandidate); _signalRService.on('OnAudioToggle', _handleAudioToggle); _signalRService.on('OnCameraToggle', _handleCameraToggle); _handlersRegistered = true; log('✅ [HANDLERS] All call handlers registered', name: 'CallManager'); log(' - OnIncomingCallAsync', name: 'CallManager'); log(' - OnCallAcceptedAsync ⭐', name: 'CallManager'); log(' - OnCallDeclinedAsync', name: 'CallManager'); log(' - OnHangUpAsync ⭐', name: 'CallManager'); log(' - OnOfferAsync ⭐', name: 'CallManager'); log(' - OnAnswerOfferAsync ⭐', name: 'CallManager'); log(' - OnIceCandidateAsync ⭐', name: 'CallManager'); log(' - OnAudioToggle', name: 'CallManager'); log(' - OnCameraToggle', name: 'CallManager'); } /// Start outgoing call Future 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'); _updateCallStatus(CallStatus.outgoingRinging); // Initialize WebRTC log('🔧 [OUTGOING] Initializing WebRTC...', name: 'CallManager'); await _initializeWebRTC(); log('✅ [OUTGOING] WebRTC initialized', name: 'CallManager'); // Start timeout timer (60 seconds for outgoing calls) _callTimeoutTimer = Timer(const Duration(seconds: 60), () { if (_callStatus == CallStatus.outgoingRinging) { log('⏱️ [OUTGOING] Call timeout - no answer after 60s', name: 'CallManager'); _cleanup(); final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showGenericError(context, 'Call timeout - no answer'); } } }); log('⏱️ [OUTGOING] Timeout timer started (60s)', name: 'CallManager'); // Navigate to call screen _navigateToCallScreen(); log('✅ [OUTGOING] Navigated to call screen', name: 'CallManager'); log('✅ [OUTGOING] Call started successfully', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager'); } catch (e, stackTrace) { log('❌ [OUTGOING] Error starting call: $e', name: 'CallManager', error: e, stackTrace: stackTrace); _cleanup(); final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showGenericError(context, 'Failed to start call: $e'); } } } /// Handle incoming call from notification Future handleIncomingCallNotification({ required String callId, required String callerId, required String callerName, required bool isVideoCall, Map? 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 FIX: Auto-initialize CallManager if not already initialized if (_userId == null || _authToken == null) { log('⚠️ [INCOMING] CallManager not initialized - attempting auto-initialization...', name: 'CallManager'); // Try to get credentials from ChatProvider try { final chatProvider = Provider.of(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', name: 'CallManager'); } else { log('❌ [INCOMING] No credentials available in ChatProvider', name: 'CallManager'); // Show error to user final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showGenericError(context, 'Unable to receive call. Please login to chat first.'); } return; } } catch (e) { log('❌ [INCOMING] Auto-initialization failed: $e', name: 'CallManager'); // Show error to user final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showGenericError(context, 'Unable to receive call. Please open chat first.'); } 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 _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 acceptCall() async { try { log('═══════════════════════════════════════════', name: 'CallManager'); log('✅ [ACCEPT] Accepting call...', name: 'CallManager'); log('🔍 [ACCEPT] CODE VERSION: 2026-07-19-v3-DEBUG', name: 'CallManager'); 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'); // Ensure SignalR connected 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'); _cleanup(); return; } log('✅ [ACCEPT] SignalR connected', name: 'CallManager'); log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager'); _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'); log(' Waiting for backend to process...', name: 'CallManager'); // 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 service: ${_webrtcService != null ? "Created" : "NULL"}', 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('ℹ️ [ACCEPT] Expected sequence:', name: 'CallManager'); log(' 1. Caller receives OnCallAcceptedAsync', name: 'CallManager'); log(' 2. Caller creates offer and sends OfferAsync', name: 'CallManager'); log(' 3. We receive OnOfferAsync', name: 'CallManager'); log(' 4. We create answer and send AnswerOfferAsync', name: 'CallManager'); log(' 5. Caller receives OnAnswerOfferAsync', name: 'CallManager'); log(' 6. ICE candidates exchange', name: 'CallManager'); log(' 7. Connection established', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager'); } catch (e, stackTrace) { log('❌ [ACCEPT] Error accepting call: $e', name: 'CallManager', error: e, stackTrace: stackTrace); _cleanup(); } } /// Decline incoming call Future 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 hangUp() async { try { log('═══════════════════════════════════════════', name: 'CallManager'); log('📞 [HANGUP] Hanging up...', name: 'CallManager'); if (_currentCall == null) { log('⚠️ [HANGUP] No call to hang up', name: 'CallManager'); return; } // Ensure SignalR connected if (await _signalRService.ensureConnected()) { log('📤 [HANGUP] Invoking HangUpAsync...', name: 'CallManager'); await _signalRService.invoke('HangUpAsync', args: [ _myEmployeeNumber ?? '', _currentCall!.peerId, _moduleId ?? '0', _referenceId ?? '0', _conversationId ?? '', ]); log('✅ [HANGUP] HangUpAsync invoked', name: 'CallManager'); } else { log('⚠️ [HANGUP] SignalR not connected, skipping backend call', name: 'CallManager'); } _cleanup(); log('✅ [HANGUP] Call ended successfully', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager'); } catch (e, stackTrace) { log('❌ [HANGUP] Error hanging up: $e', name: 'CallManager', error: e, stackTrace: stackTrace); _cleanup(); } } /// Toggle mute Future 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 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 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 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 _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 _initializeWebRTC() async { log('🔧 [WEBRTC] Initializing WebRTC...', name: 'CallManager'); _webrtcService = WebRTCService(); _setupWebRTCCallbacks(); if (_currentCall!.type == CallType.audio) { log('🎵 [WEBRTC] Initializing for audio call', name: 'CallManager'); await _webrtcService!.initializeForAudioCall(); } else { log('📹 [WEBRTC] Initializing for video call', name: 'CallManager'); await _webrtcService!.initializeForVideoCall(); } log('✅ [WEBRTC] WebRTC initialized', name: 'CallManager'); } void _setupWebRTCCallbacks() { log('🔧 [WEBRTC] Setting up callbacks...', name: 'CallManager'); _webrtcService!.onIceCandidate = (RTCIceCandidate candidate) { log('🧊 [WEBRTC] ICE candidate generated', name: 'CallManager'); log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', name: 'CallManager'); final candidateJson = jsonEncode({ 'candidate': candidate.candidate, 'sdpMid': candidate.sdpMid, 'sdpMLineIndex': candidate.sdpMLineIndex, }); _signalRService.invoke('IceCandidateAsync', args: [ _currentCall!.peerId, candidateJson, _currentCall!.callId, ]); }; _webrtcService!.onRemoteStream = (MediaStream stream) { log('📡 [WEBRTC] Remote stream received', name: 'CallManager'); log(' Audio tracks: ${stream.getAudioTracks().length}', name: 'CallManager'); log(' Video tracks: ${stream.getVideoTracks().length}', name: 'CallManager'); notifyListeners(); }; _webrtcService!.onIceConnectionStateChange = (RTCIceConnectionState state) { log('🔗 [WEBRTC] ICE state: ${state.toString()}', name: 'CallManager'); if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) { log('✅ [WEBRTC] ICE connection established', name: 'CallManager'); _updateCallStatus(CallStatus.connected); _startCallDurationTimer(); } else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) { log('❌ [WEBRTC] ICE connection failed', name: 'CallManager'); _cleanup(); } else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) { log('⚠️ [WEBRTC] ICE connection disconnected', name: 'CallManager'); } }; log('✅ [WEBRTC] Callbacks set up', name: 'CallManager'); } void _navigateToCallScreen() { final context = navigatorKey.currentContext; if (context == null || _currentCall == null) { log('⚠️ [NAV] Cannot navigate - context or call is null', name: 'CallManager'); return; } log('🧭 [NAV] Navigating to ${_currentCall!.type.name} call screen', name: 'CallManager'); Navigator.of(context).push( MaterialPageRoute( builder: (context) => _currentCall!.type == CallType.audio ? const AudioCallPage() : const VideoCallPage(), ), ); } void _updateCallStatus(CallStatus newStatus) { if (_callStatus == newStatus) return; log('🔄 [STATUS] ${_callStatus.name} -> ${newStatus.name}', name: 'CallManager'); _callStatus = newStatus; notifyListeners(); } void _startCallDurationTimer() { _callDurationTimer?.cancel(); _callDuration = Duration.zero; log('⏱️ [TIMER] Starting call duration timer', name: 'CallManager'); _callDurationTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _callDuration = Duration(seconds: _callDuration.inSeconds + 1); notifyListeners(); }); } void _cleanup() { log('🧹 [CLEANUP] Starting cleanup...', name: 'CallManager'); _callTimeoutTimer?.cancel(); _callDurationTimer?.cancel(); _webrtcService?.dispose(); _webrtcService = null; if (_currentCall != null) { _callKitService.endCall(_currentCall!.callId); } _currentCall = null; _updateCallStatus(CallStatus.idle); _callDuration = Duration.zero; _isMuted = false; _isSpeakerOn = false; _isCameraOn = true; _isPeerMuted = false; _isPeerCameraOn = true; log('✅ [CLEANUP] Cleanup complete', name: 'CallManager'); } /// Reset service (for logout or testing) Future 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? 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? 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 if (_webrtcService == null) { log('⚠️ [EVENT] WebRTC not initialized yet, waiting...', name: 'CallManager'); // Wait a bit for WebRTC to initialize (it should be initializing in parallel) await Future.delayed(const Duration(milliseconds: 500)); if (_webrtcService == null) { log('❌ [EVENT] WebRTC still not initialized after wait', name: 'CallManager'); return; } } // 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? 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? args) { 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('═══════════════════════════════════════════', name: 'CallManager'); _cleanup(); } void _handleOffer(List? 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(' WebRTC initialized: ${_webrtcService != null}', 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'); if (_webrtcService == null) { log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager'); return; } log('🔧 [EVENT] Creating SDP answer...', name: 'CallManager'); final answer = await _webrtcService!.createAnswer(offerSdp); log('✅ [EVENT] SDP answer created (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? 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'); if (_webrtcService == null) { log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager'); return; } log('🔧 [EVENT] Setting remote answer...', name: 'CallManager'); await _webrtcService!.setRemoteAnswer(answerSdp); log('✅ [EVENT] Remote answer set successfully', name: 'CallManager'); 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? args) async { log('📞 [EVENT] 🧊 OnIceCandidateAsync received', name: 'CallManager'); if (args == null || args.isEmpty) { log('⚠️ [EVENT] No ICE candidate data received', name: 'CallManager'); return; } try { final candidateJson = args[0] as String?; if (candidateJson == null) { log('⚠️ [EVENT] Candidate JSON is null', name: 'CallManager'); return; } if (_webrtcService == null) { log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager'); return; } final candidateData = jsonDecode(candidateJson) as Map; 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? args) { log('📞 [EVENT] 🎤 OnAudioToggle received', name: 'CallManager'); log(' Peer muted: $_isPeerMuted -> ${!_isPeerMuted}', name: 'CallManager'); _isPeerMuted = !_isPeerMuted; notifyListeners(); } void _handleCameraToggle(List? 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'); } } }