diff --git a/lib/modules/cx_module/chat/chat_page.dart b/lib/modules/cx_module/chat/chat_page.dart index cf0eba68..d7693368 100644 --- a/lib/modules/cx_module/chat/chat_page.dart +++ b/lib/modules/cx_module/chat/chat_page.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; @@ -204,6 +205,24 @@ class _ChatPageState extends State { appBar: DefaultAppBar( title: widget.title, actions: [ + // NEW: Call readiness indicator - shows if device can receive calls + if (kDebugMode) + Selector( + selector: (_, provider) => provider.areCallHandlersRegistered, + builder: (context, handlersRegistered, _) => Padding( + padding: const EdgeInsets.only(right: 8), + child: Center( + child: Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: handlersRegistered ? Colors.green : Colors.red, + ), + ), + ), + ), + ), Selector( selector: (_, provider) => provider.recipient, builder: (context, recipient, _) => IconButton( diff --git a/lib/modules/cx_module/chat/chat_provider.dart b/lib/modules/cx_module/chat/chat_provider.dart index 5d15835a..9fa6741f 100644 --- a/lib/modules/cx_module/chat/chat_provider.dart +++ b/lib/modules/cx_module/chat/chat_provider.dart @@ -44,9 +44,11 @@ import 'package:signalr_netcore/signalr_client.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart'; import 'package:test_sa/controllers/api_routes/urls.dart'; import 'package:test_sa/extensions/string_extensions.dart'; +import 'package:test_sa/main.dart'; import 'package:test_sa/modules/cx_module/chat/model/chat_login_response_model.dart'; import 'package:uuid/uuid.dart'; import 'package:flutter/material.dart' as Material; +import 'package:flutter/material.dart'; // NEW - for WidgetsBinding and showDialog import 'api_client.dart'; import 'chat_api_client.dart'; @@ -56,7 +58,9 @@ import 'model/get_search_user_chat_model.dart'; import 'model/get_single_user_chat_list_model.dart'; import 'model/unread_message_model.dart'; import 'model/user_chat_history_model.dart'; -import 'model/call_session.dart'; // NEW - Import call session model +import 'model/call_session.dart'; +import 'call/call_debug_helper.dart'; +import 'call/incoming_call_dialog.dart'; // NEW - Import incoming call dialog //Need to refactor this remove unused code. @@ -102,6 +106,10 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { bool isPeerMuted = false; bool isPeerCameraOn = true; + // NEW: Call handlers registration status + bool _callHandlersRegistered = false; + bool get areCallHandlersRegistered => _callHandlersRegistered; + bool get isCallInProgress => callStatus != CallStatus.idle; /// OPTIMIZATION: Improved connection disposal to prevent memory leaks @@ -345,27 +353,30 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { chatHubConnection = await getHubConnection(); await chatHubConnection!.start(); + + // NEW: Log connection details for diagnostics + log('═══════════════════════════════════════════', name: 'ChatProvider'); + log('🔌 SignalR Hub Connection: Started', name: 'ChatProvider'); + log('🔑 Connection ID: ${chatHubConnection!.connectionId ?? "NULL"}', name: 'ChatProvider'); + log('👤 User ID: ${chatLoginResponse!.userId}', name: 'ChatProvider'); + log('💬 Conversation ID: $conversationID', name: 'ChatProvider'); + log('📞 My Employee Number: ${sender?.employeeNumber ?? "NOT SET"}', name: 'ChatProvider'); + log('═══════════════════════════════════════════', name: 'ChatProvider'); + if (kDebugMode) { print("🔌 SignalR Hub Connection: Started"); } await chatHubConnection!.invoke("JoinConversation", args: [conversationID]); + log('✅ Joined conversation: $conversationID', name: 'ChatProvider'); - // Register chat event handlers - chatHubConnection!.on("ReceiveMessage", onMsgReceived1); - chatHubConnection!.on("OnMessageReceivedAsync", onMsgReceived); - chatHubConnection!.on("OnSubmitChatAsync", onSubmitChatAsync); - chatHubConnection!.on("OnTypingAsync", OnTypingAsync); - chatHubConnection!.on("OnStopTypingAsync", OnStopTypingAsync); - chatHubConnection!.on("OnSeenChatUserAsync", onSeenUserChatAsync); - chatHubConnection!.on("OnAckSeenAsync", onAckSeenAsync); - - // Register call event handlers (PHASE 1) - _registerCallHandlers(); + // Register ALL event handlers (chat + call) + _registerAllEventHandlers(); //group On message // chatHubConnection.on("OnDeliveredGroupChatHistoryAsync", onGroupMsgReceived); } catch (e) { + log('❌ [CONNECTION] Error building SignalR connection: $e', name: 'ChatProvider'); if (kDebugMode) { print('⚠️ Error building SignalR connection: $e'); } @@ -375,18 +386,72 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { } } + /// Register all event handlers (chat + call) - called on initial connection and after reconnect + void _registerAllEventHandlers() { + if (chatHubConnection == null) { + log('⚠️ Cannot register event handlers - connection is null', name: 'ChatProvider'); + return; + } + + log('🔧 [EVENT HANDLERS] Registering all event handlers...', name: 'ChatProvider'); + + // Register chat event handlers + chatHubConnection!.on("ReceiveMessage", onMsgReceived1); + chatHubConnection!.on("OnMessageReceivedAsync", onMsgReceived); + chatHubConnection!.on("OnSubmitChatAsync", onSubmitChatAsync); + chatHubConnection!.on("OnTypingAsync", OnTypingAsync); + chatHubConnection!.on("OnStopTypingAsync", OnStopTypingAsync); + chatHubConnection!.on("OnSeenChatUserAsync", onSeenUserChatAsync); + chatHubConnection!.on("OnAckSeenAsync", onAckSeenAsync); + + // Register call event handlers + _registerCallHandlers(); + + log('✅ [EVENT HANDLERS] All event handlers registered successfully', name: 'ChatProvider'); + } + Future getHubConnection() async { if (kDebugMode) { print('🔧 Creating new SignalR hub connection...'); } HubConnection hub; - HttpConnectionOptions httpOp = HttpConnectionOptions(skipNegotiation: false, logMessageContent: true); + HttpConnectionOptions httpOp = HttpConnectionOptions( + skipNegotiation: false, + logMessageContent: true, + ); + hub = HubConnectionBuilder() .withUrl("${URLs.chatHubUrlChat}?UserId=${chatLoginResponse!.userId}&source=Desktop&access_token=${chatLoginResponse!.token}", options: httpOp) .withAutomaticReconnect(retryDelays: [2000, 5000, 10000, 20000]).build(); + // NEW: Add global event logging in debug mode AND re-register handlers on reconnect if (kDebugMode) { + // Log connection state changes + hub.onclose(({Exception? error}) { + log('🔴 [SignalR] Connection closed: $error', name: 'ChatProvider'); + }); + + hub.onreconnecting(({Exception? error}) { + log('🟡 [SignalR] Reconnecting: $error', name: 'ChatProvider'); + }); + + hub.onreconnected(({String? connectionId}) async { + log('🟢 [SignalR] Reconnected: $connectionId', name: 'ChatProvider'); + log('🔄 [SignalR] Re-registering all event handlers after reconnection...', name: 'ChatProvider'); + + // CRITICAL: Re-register all event handlers after reconnection + _registerAllEventHandlers(); + + log('✅ [SignalR] Event handlers re-registered after reconnection', name: 'ChatProvider'); + }); + + print('✅ SignalR hub connection created with debug logging and auto-reregister'); + } else { + // Production: still need to re-register handlers on reconnect + hub.onreconnected(({String? connectionId}) async { + _registerAllEventHandlers(); + }); print('✅ SignalR hub connection created'); } @@ -816,7 +881,12 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Start a new audio or video call Future startCall(Participants recipient, CallType callType) async { + log('🔵 [CALL] startCall() invoked', name: 'ChatProvider'); + log('🔵 [CALL] Parameters - recipient: ${recipient.employeeNumber}, callType: $callType', name: 'ChatProvider'); + log('🔵 [CALL] Current callStatus: $callStatus', name: 'ChatProvider'); + if (callStatus != CallStatus.idle) { + log('⚠️ [CALL] Cannot start call - already in a call (status: $callStatus)', name: 'ChatProvider'); if (kDebugMode) { print('⚠️ Cannot start call - already in a call'); } @@ -824,26 +894,35 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { } if (sender == null) { + log('❌ [CALL] Cannot start call - sender is null', name: 'ChatProvider'); if (kDebugMode) { print('⚠️ Cannot start call - sender is null'); } return; } + log('🔵 [CALL] Sender: ${sender!.employeeNumber}', name: 'ChatProvider'); callStatus = CallStatus.checkingPermissions; notifyListeners(); + log('🔵 [CALL] Status changed to: checkingPermissions', name: 'ChatProvider'); try { // Generate unique call ID final callId = const Uuid().v4(); final isVideo = callType == CallType.video; + log('🔵 [CALL] Generated callId: $callId', name: 'ChatProvider'); + log('🔵 [CALL] Call type: ${isVideo ? "VIDEO" : "AUDIO"}', name: 'ChatProvider'); // Request permissions + log('🔵 [CALL] Requesting microphone permission...', name: 'ChatProvider'); final micStatus = await Permission.microphone.request(); + log('🔵 [CALL] Microphone permission status: ${micStatus.name}', name: 'ChatProvider'); + if (!micStatus.isGranted) { callStatus = CallStatus.idle; currentCall = null; notifyListeners(); + log('❌ [CALL] Microphone permission denied - aborting call', name: 'ChatProvider'); if (kDebugMode) { print('⚠️ Microphone permission denied'); } @@ -851,11 +930,15 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { } if (isVideo) { + log('🔵 [CALL] Requesting camera permission...', name: 'ChatProvider'); final cameraStatus = await Permission.camera.request(); + log('🔵 [CALL] Camera permission status: ${cameraStatus.name}', name: 'ChatProvider'); + if (!cameraStatus.isGranted) { callStatus = CallStatus.idle; currentCall = null; notifyListeners(); + log('❌ [CALL] Camera permission denied - aborting call', name: 'ChatProvider'); if (kDebugMode) { print('⚠️ Camera permission denied'); } @@ -864,6 +947,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { } // Create call session + log('🔵 [CALL] Creating CallSession...', name: 'ChatProvider'); currentCall = CallSession( callId: callId, type: callType, @@ -873,8 +957,21 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { peerAvatar: recipient.image, startTime: DateTime.now(), ); + log('✅ [CALL] CallSession created - peerId: ${currentCall!.peerId}, peerName: ${currentCall!.peerName}', name: 'ChatProvider'); // Invoke SignalR CallUserAsync + log('🔵 [CALL] Checking SignalR connection status...', name: 'ChatProvider'); + if (chatHubConnection == null) { + log('❌ [CALL] SignalR connection is null!', name: 'ChatProvider'); + throw Exception('SignalR connection not established'); + } + + log('🔵 [CALL] SignalR connection state: ${chatHubConnection!.state}', name: 'ChatProvider'); + log('🔵 [CALL] Invoking CallUserAsync with args:', name: 'ChatProvider'); + log(' - source: ${sender!.employeeNumber}', name: 'ChatProvider'); + log(' - target: ${recipient.employeeNumber}', name: 'ChatProvider'); + log(' - isVideoCall: $isVideo', name: 'ChatProvider'); + await chatHubConnection?.invoke( 'CallUserAsync', args: [ @@ -883,22 +980,28 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { isVideo, ], ); + log('✅ [CALL] CallUserAsync invoked successfully', name: 'ChatProvider'); callStatus = CallStatus.outgoingRinging; notifyListeners(); + log('🔵 [CALL] Status changed to: outgoingRinging', name: 'ChatProvider'); // Start 60s timeout _callTimeoutTimer?.cancel(); + log('🔵 [CALL] Starting 60s timeout timer...', name: 'ChatProvider'); _callTimeoutTimer = Timer(const Duration(seconds: 60), () { + log('⏱️ [CALL] Timeout timer fired', name: 'ChatProvider'); if (callStatus == CallStatus.outgoingRinging) { _handleCallTimeout(); } }); + log('✅ [CALL] Call started successfully - callId: $callId', name: 'ChatProvider'); if (kDebugMode) { print('✅ Call started: $callId'); } - } catch (e) { + } catch (e, stackTrace) { + log('❌ [CALL] Error starting call: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); if (kDebugMode) { print('❌ Error starting call: $e'); } @@ -910,15 +1013,27 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Toggle microphone mute void toggleMute() { + log('🔵 [CALL] toggleMute() called - current state: $isMuted', name: 'ChatProvider'); isMuted = !isMuted; notifyListeners(); + log('🔵 [CALL] Microphone ${isMuted ? "muted" : "unmuted"}', name: 'ChatProvider'); // Notify peer via SignalR if (currentCall != null && sender != null) { + log('🔵 [CALL] Invoking AudioToggle to notify peer', name: 'ChatProvider'); + log(' - source: ${sender!.employeeNumber}', name: 'ChatProvider'); + log(' - target: ${currentCall!.peerId}', name: 'ChatProvider'); + chatHubConnection?.invoke( 'AudioToggle', args: [sender!.employeeNumber ?? '', currentCall!.peerId], - ); + ).then((_) { + log('✅ [CALL] AudioToggle invoked successfully', name: 'ChatProvider'); + }).catchError((e) { + log('❌ [CALL] Error invoking AudioToggle: $e', name: 'ChatProvider'); + }); + } else { + log('⚠️ [CALL] Cannot notify peer - currentCall or sender is null', name: 'ChatProvider'); } if (kDebugMode) { @@ -928,8 +1043,10 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Toggle speakerphone void toggleSpeaker() { + log('🔵 [CALL] toggleSpeaker() called - current state: $isSpeakerOn', name: 'ChatProvider'); isSpeakerOn = !isSpeakerOn; notifyListeners(); + log('🔵 [CALL] Speaker ${isSpeakerOn ? "on" : "off"}', name: 'ChatProvider'); if (kDebugMode) { print('🔊 Speaker ${isSpeakerOn ? "on" : "off"}'); @@ -938,15 +1055,27 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Toggle camera (video only) void toggleCamera() { + log('🔵 [CALL] toggleCamera() called - current state: $isCameraOn', name: 'ChatProvider'); isCameraOn = !isCameraOn; notifyListeners(); + log('🔵 [CALL] Camera ${isCameraOn ? "on" : "off"}', name: 'ChatProvider'); // Notify peer via SignalR if (currentCall != null && sender != null) { + log('🔵 [CALL] Invoking CameraToggle to notify peer', name: 'ChatProvider'); + log(' - source: ${sender!.employeeNumber}', name: 'ChatProvider'); + log(' - target: ${currentCall!.peerId}', name: 'ChatProvider'); + chatHubConnection?.invoke( 'CameraToggle', args: [sender!.employeeNumber ?? '', currentCall!.peerId], - ); + ).then((_) { + log('✅ [CALL] CameraToggle invoked successfully', name: 'ChatProvider'); + }).catchError((e) { + log('❌ [CALL] Error invoking CameraToggle: $e', name: 'ChatProvider'); + }); + } else { + log('⚠️ [CALL] Cannot notify peer - currentCall or sender is null', name: 'ChatProvider'); } if (kDebugMode) { @@ -956,6 +1085,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Switch between front and rear camera void switchCamera() { + log('🔵 [CALL] switchCamera() called', name: 'ChatProvider'); if (kDebugMode) { print('🔄 Switch camera requested'); } @@ -965,13 +1095,21 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Switch from audio call to video call Future switchToVideoCall() async { + log('🔵 [CALL] switchToVideoCall() called', name: 'ChatProvider'); + log('🔵 [CALL] Current call type: ${currentCall?.type}', name: 'ChatProvider'); + if (currentCall == null || currentCall!.type == CallType.video) { + log('⚠️ [CALL] Cannot switch - currentCall is null or already video', name: 'ChatProvider'); return; } // Request camera permission + log('🔵 [CALL] Requesting camera permission for upgrade...', name: 'ChatProvider'); final cameraStatus = await Permission.camera.request(); + log('🔵 [CALL] Camera permission status: ${cameraStatus.name}', name: 'ChatProvider'); + if (!cameraStatus.isGranted) { + log('❌ [CALL] Camera permission denied - cannot upgrade to video', name: 'ChatProvider'); if (kDebugMode) { print('⚠️ Camera permission denied'); } @@ -979,6 +1117,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { } // Update call type (create new session to maintain immutability) + log('🔵 [CALL] Upgrading call to video...', name: 'ChatProvider'); currentCall = CallSession( callId: currentCall!.callId, type: CallType.video, @@ -993,6 +1132,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { ); isCameraOn = true; notifyListeners(); + log('✅ [CALL] Successfully switched to video call', name: 'ChatProvider'); if (kDebugMode) { print('📹 Switched to video call'); @@ -1001,12 +1141,24 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// End the current call Future hangUp() async { + log('🔵 [CALL] hangUp() called', name: 'ChatProvider'); + log('🔵 [CALL] Current status: $callStatus', name: 'ChatProvider'); + log('🔵 [CALL] Current call: ${currentCall?.callId}', name: 'ChatProvider'); + if (currentCall == null || callStatus == CallStatus.idle) { + log('⚠️ [CALL] No active call to hang up', name: 'ChatProvider'); return; } try { // Invoke SignalR HangUpAsync + log('🔵 [CALL] Invoking HangUpAsync with args:', name: 'ChatProvider'); + log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider'); + log(' - target: ${currentCall!.peerId}', name: 'ChatProvider'); + log(' - moduleCode: $moduleID', name: 'ChatProvider'); + log(' - referenceId: ${referenceID?.toString() ?? "null"}', name: 'ChatProvider'); + log(' - conversationId: ${chatParticipantModel?.id?.toString() ?? "null"}', name: 'ChatProvider'); + await chatHubConnection?.invoke( 'HangUpAsync', args: [ @@ -1017,11 +1169,13 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { chatParticipantModel?.id?.toString() ?? '', ], ); + log('✅ [CALL] HangUpAsync invoked successfully', name: 'ChatProvider'); if (kDebugMode) { print('📞 Call ended'); } - } catch (e) { + } catch (e, stackTrace) { + log('❌ [CALL] Error invoking HangUpAsync: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); if (kDebugMode) { print('⚠️ Error ending call: $e'); } @@ -1032,11 +1186,21 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Handle call timeout (60s no answer) void _handleCallTimeout() { + log('⏱️ [CALL] _handleCallTimeout() - Call timeout after 60s', name: 'ChatProvider'); + log('🔵 [CALL] Current status: $callStatus', name: 'ChatProvider'); + if (kDebugMode) { print('⏱️ Call timeout - no answer'); } // Invoke CallMissedAsync + log('🔵 [CALL] Invoking CallMissedAsync with args:', name: 'ChatProvider'); + log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider'); + log(' - target: ${currentCall?.peerId}', name: 'ChatProvider'); + log(' - moduleCode: $moduleID', name: 'ChatProvider'); + log(' - referenceId: ${referenceID?.toString() ?? "null"}', name: 'ChatProvider'); + log(' - conversationId: ${chatParticipantModel?.id?.toString() ?? "null"}', name: 'ChatProvider'); + chatHubConnection?.invoke( 'CallMissedAsync', args: [ @@ -1046,15 +1210,26 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], - ); + ).then((_) { + log('✅ [CALL] CallMissedAsync invoked successfully', name: 'ChatProvider'); + }).catchError((e) { + log('❌ [CALL] Error invoking CallMissedAsync: $e', name: 'ChatProvider'); + }); _teardownCall(); } /// Clean up call resources void _teardownCall() { + log('🧹 [CALL] _teardownCall() - Cleaning up call resources', name: 'ChatProvider'); + log('🔵 [CALL] Previous status: $callStatus', name: 'ChatProvider'); + log('🔵 [CALL] Call duration: ${callDuration.inSeconds}s', name: 'ChatProvider'); + _callTimeoutTimer?.cancel(); + log('🔵 [CALL] Timeout timer cancelled', name: 'ChatProvider'); + _callDurationTimer?.cancel(); + log('🔵 [CALL] Duration timer cancelled', name: 'ChatProvider'); callStatus = CallStatus.idle; currentCall = null; @@ -1066,6 +1241,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { isPeerCameraOn = true; notifyListeners(); + log('✅ [CALL] Call resources cleaned up - status reset to idle', name: 'ChatProvider'); if (kDebugMode) { print('🧹 Call resources cleaned up'); @@ -1074,6 +1250,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Start call duration timer when connected void _startCallDurationTimer() { + log('⏱️ [CALL] _startCallDurationTimer() - Starting call duration counter', name: 'ChatProvider'); _callDurationTimer?.cancel(); callDuration = Duration.zero; @@ -1081,6 +1258,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { callDuration = Duration(seconds: callDuration.inSeconds + 1); notifyListeners(); }); + log('✅ [CALL] Duration timer started', name: 'ChatProvider'); if (kDebugMode) { print('⏱️ Call duration timer started'); @@ -1089,20 +1267,33 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Accept incoming call Future acceptCall() async { + log('🔵 [CALL] acceptCall() called', name: 'ChatProvider'); + log('🔵 [CALL] Current status: $callStatus', name: 'ChatProvider'); + log('🔵 [CALL] Current call: ${currentCall?.callId}', name: 'ChatProvider'); + if (currentCall == null || callStatus != CallStatus.incomingRinging) { + log('⚠️ [CALL] Cannot accept - invalid state (call: ${currentCall == null ? "null" : "exists"}, status: $callStatus)', name: 'ChatProvider'); return; } // Request permissions + log('🔵 [CALL] Requesting microphone permission...', name: 'ChatProvider'); final micStatus = await Permission.microphone.request(); + log('🔵 [CALL] Microphone permission: ${micStatus.name}', name: 'ChatProvider'); + if (!micStatus.isGranted) { + log('❌ [CALL] Microphone permission denied - declining call', name: 'ChatProvider'); await declineCall('permission_denied'); return; } if (currentCall!.type == CallType.video) { + log('🔵 [CALL] Video call - requesting camera permission...', name: 'ChatProvider'); final cameraStatus = await Permission.camera.request(); + log('🔵 [CALL] Camera permission: ${cameraStatus.name}', name: 'ChatProvider'); + if (!cameraStatus.isGranted) { + log('❌ [CALL] Camera permission denied - declining call', name: 'ChatProvider'); await declineCall('permission_denied'); return; } @@ -1110,6 +1301,13 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { try { // Invoke SignalR AnswerCallAsync + log('🔵 [CALL] Invoking AnswerCallAsync with args:', name: 'ChatProvider'); + log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider'); + log(' - target: ${currentCall!.peerId}', name: 'ChatProvider'); + log(' - moduleCode: $moduleID', name: 'ChatProvider'); + log(' - referenceId: ${referenceID?.toString() ?? "null"}', name: 'ChatProvider'); + log(' - conversationId: ${chatParticipantModel?.id?.toString() ?? "null"}', name: 'ChatProvider'); + await chatHubConnection?.invoke( 'AnswerCallAsync', args: [ @@ -1120,14 +1318,17 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { chatParticipantModel?.id?.toString() ?? '', ], ); + log('✅ [CALL] AnswerCallAsync invoked successfully', name: 'ChatProvider'); callStatus = CallStatus.connecting; notifyListeners(); + log('🔵 [CALL] Status changed to: connecting', name: 'ChatProvider'); if (kDebugMode) { print('✅ Call accepted'); } - } catch (e) { + } catch (e, stackTrace) { + log('❌ [CALL] Error accepting call: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); if (kDebugMode) { print('❌ Error accepting call: $e'); } @@ -1137,12 +1338,23 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Decline incoming call Future declineCall(String reason) async { + log('🔵 [CALL] declineCall() called with reason: $reason', name: 'ChatProvider'); + log('🔵 [CALL] Current call: ${currentCall?.callId}', name: 'ChatProvider'); + if (currentCall == null) { + log('⚠️ [CALL] No call to decline', name: 'ChatProvider'); return; } try { // Invoke SignalR CallDeclinedAsync + log('🔵 [CALL] Invoking CallDeclinedAsync with args:', name: 'ChatProvider'); + log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider'); + log(' - target: ${currentCall!.peerId}', name: 'ChatProvider'); + log(' - moduleCode: $moduleID', name: 'ChatProvider'); + log(' - referenceId: ${referenceID?.toString() ?? "null"}', name: 'ChatProvider'); + log(' - conversationId: ${chatParticipantModel?.id?.toString() ?? "null"}', name: 'ChatProvider'); + await chatHubConnection?.invoke( 'CallDeclinedAsync', args: [ @@ -1153,11 +1365,13 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { chatParticipantModel?.id?.toString() ?? '', ], ); + log('✅ [CALL] CallDeclinedAsync invoked successfully', name: 'ChatProvider'); if (kDebugMode) { print('📞 Call declined: $reason'); } - } catch (e) { + } catch (e, stackTrace) { + log('❌ [CALL] Error declining call: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); if (kDebugMode) { print('⚠️ Error declining call: $e'); } @@ -1168,44 +1382,128 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Register call-related SignalR event handlers void _registerCallHandlers() { - if (chatHubConnection == null) return; + log('═══════════════════════════════════════════', name: 'ChatProvider'); + log('📞 [CALL] Starting call handler registration...', name: 'ChatProvider'); + + if (chatHubConnection == null) { + log('❌ [CALL] Cannot register handlers - chatHubConnection is null', name: 'ChatProvider'); + return; + } + + // NEW: Add a GLOBAL catch-all handler to see ALL SignalR events in debug mode + if (kDebugMode) { + log('🔍 [DEBUG] Setting up GLOBAL event interceptor to log ALL incoming SignalR messages', name: 'ChatProvider'); + + // Store the original method handlers + final originalHandlers = {}; + + // Register a generic listener for common call event patterns + final allPossibleCallEvents = [ + 'OnIncomingCallAsync', + 'OnIncomingCall', + 'IncomingCall', + 'OnCallAcceptedAsync', + 'OnCallAccepted', + 'CallAccepted', + 'OnCallDeclinedAsync', + 'OnCallDeclined', + 'CallDeclined', + 'OnHangUpAsync', + 'OnHangUp', + 'HangUp', + 'OnAudioToggle', + 'AudioToggle', + 'OnCameraToggle', + 'CameraToggle', + 'CallUserAsync', // Echo back + 'OnCallStarted', // Alternative + 'OnCallRinging', // Alternative + ]; + + for (final eventName in allPossibleCallEvents) { + chatHubConnection!.on(eventName, (args) { + log('🔔🔔🔔 [GLOBAL DEBUG] Received SignalR event: "$eventName"', name: 'ChatProvider'); + log(' Args: $args', name: 'ChatProvider'); + log(' Args type: ${args?.runtimeType}', name: 'ChatProvider'); + if (args != null && args.isNotEmpty) { + log(' First arg: ${args.first}', name: 'ChatProvider'); + log(' First arg type: ${args.first?.runtimeType}', name: 'ChatProvider'); + } + }); + } + } // Incoming call + log('🔵 [CALL] Registering: OnIncomingCallAsync', name: 'ChatProvider'); chatHubConnection!.on("OnIncomingCallAsync", _onIncomingCall); // Call accepted + log('🔵 [CALL] Registering: OnCallAcceptedAsync', name: 'ChatProvider'); chatHubConnection!.on("OnCallAcceptedAsync", _onCallAccepted); // Call declined + log('🔵 [CALL] Registering: OnCallDeclinedAsync', name: 'ChatProvider'); chatHubConnection!.on("OnCallDeclinedAsync", _onCallDeclined); // Call ended + log('🔵 [CALL] Registering: OnHangUpAsync', name: 'ChatProvider'); chatHubConnection!.on("OnHangUpAsync", _onCallEnded); // Peer audio toggle + log('🔵 [CALL] Registering: OnAudioToggle', name: 'ChatProvider'); chatHubConnection!.on("OnAudioToggle", _onPeerAudioToggle); // Peer camera toggle + log('🔵 [CALL] Registering: OnCameraToggle', name: 'ChatProvider'); chatHubConnection!.on("OnCameraToggle", _onPeerCameraToggle); + log('✅ [CALL] All call handlers registered successfully', name: 'ChatProvider'); + log('📞 [CALL] Ready to receive: OnIncomingCallAsync, OnCallAcceptedAsync, OnCallDeclinedAsync, OnHangUpAsync', name: 'ChatProvider'); + log('📞 [CALL] My Employee Number: ${sender?.employeeNumber ?? "NOT SET YET"}', name: 'ChatProvider'); + log('📞 [CALL] My User ID: ${chatLoginResponse?.userId ?? "NOT SET"}', name: 'ChatProvider'); + log('📞 [CALL] SignalR Connection ID: ${chatHubConnection?.connectionId ?? "NULL"}', name: 'ChatProvider'); + log('═══════════════════════════════════════════', name: 'ChatProvider'); + if (kDebugMode) { - print('✅ Call handlers registered'); + print('✅ Call handlers registered - watching for ALL call events'); } + + _callHandlersRegistered = true; } /// Handle incoming call event void _onIncomingCall(List? args) { - if (args == null || args.isEmpty) return; + log('📞 [CALL EVENT] OnIncomingCallAsync received', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Current status: $callStatus', name: 'ChatProvider'); + + if (args == null || args.isEmpty) { + log('⚠️ [CALL EVENT] OnIncomingCallAsync - args is null or empty', name: 'ChatProvider'); + return; + } try { final data = args.first as Map; + log('🔵 [CALL EVENT] Parsed data: $data', name: 'ChatProvider'); + final callerId = data['sourceUserId'] as String?; final callerName = data['userName'] as String? ?? 'Unknown'; final isVideo = data['isVideoCall'] as bool? ?? false; + log('🔵 [CALL EVENT] Parsed values:', name: 'ChatProvider'); + log(' - callerId: $callerId', name: 'ChatProvider'); + log(' - callerName: $callerName', name: 'ChatProvider'); + log(' - isVideoCall: $isVideo', name: 'ChatProvider'); + // Check if already in a call if (callStatus != CallStatus.idle) { + log('⚠️ [CALL EVENT] Already in a call (status: $callStatus) - auto-rejecting with busy', name: 'ChatProvider'); + // Auto-reject with busy status + log('🔵 [CALL EVENT] Invoking CallDeclinedAsync (busy) with args:', name: 'ChatProvider'); + log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider'); + log(' - target: $callerId', name: 'ChatProvider'); + chatHubConnection?.invoke( 'CallDeclinedAsync', args: [ @@ -1215,11 +1513,16 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], - ); + ).then((_) { + log('✅ [CALL EVENT] Auto-rejection sent successfully', name: 'ChatProvider'); + }).catchError((e) { + log('❌ [CALL EVENT] Error sending auto-rejection: $e', name: 'ChatProvider'); + }); return; } // Create call session + log('🔵 [CALL EVENT] Creating incoming CallSession...', name: 'ChatProvider'); currentCall = CallSession( callId: const Uuid().v4(), type: isVideo ? CallType.video : CallType.audio, @@ -1229,14 +1532,37 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { peerAvatar: null, startTime: DateTime.now(), ); + log('✅ [CALL EVENT] CallSession created - callId: ${currentCall!.callId}', name: 'ChatProvider'); callStatus = CallStatus.incomingRinging; notifyListeners(); + log('🔵 [CALL EVENT] Status changed to: incomingRinging', name: 'ChatProvider'); + log('✅ [CALL EVENT] Incoming ${isVideo ? "video" : "audio"} call from $callerName processed', name: 'ChatProvider'); + + // NEW: Show incoming call dialog using global navigator + log('🔵 [CALL EVENT] Showing incoming call dialog...', name: 'ChatProvider'); + final context = navigatorKey.currentContext; + if (context != null && currentCall != null) { + // Use a post-frame callback to ensure we're not in the middle of a build + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => IncomingCallDialog(call: currentCall!), + ); + log('✅ [CALL EVENT] Incoming call dialog shown', name: 'ChatProvider'); + } + }); + } else { + log('⚠️ [CALL EVENT] Cannot show dialog - context is null or call is null', name: 'ChatProvider'); + } if (kDebugMode) { print('📞 Incoming ${isVideo ? "video" : "audio"} call from $callerName'); } - } catch (e) { + } catch (e, stackTrace) { + log('❌ [CALL EVENT] Error handling incoming call: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); if (kDebugMode) { print('❌ Error handling incoming call: $e'); } @@ -1245,30 +1571,50 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Handle call accepted event void _onCallAccepted(List? args) { + log('✅ [CALL EVENT] OnCallAcceptedAsync received', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Current status: $callStatus', name: 'ChatProvider'); + if (callStatus == CallStatus.outgoingRinging) { + log('🔵 [CALL EVENT] Call was accepted by peer - stopping timeout timer', name: 'ChatProvider'); _callTimeoutTimer?.cancel(); + callStatus = CallStatus.connecting; notifyListeners(); + log('🔵 [CALL EVENT] Status changed to: connecting', name: 'ChatProvider'); + log('✅ [CALL EVENT] Call accepted - proceeding with connection', name: 'ChatProvider'); if (kDebugMode) { print('✅ Call was accepted'); } + } else { + log('⚠️ [CALL EVENT] Received OnCallAcceptedAsync but status is not outgoingRinging: $callStatus', name: 'ChatProvider'); } } /// Handle call declined event void _onCallDeclined(List? args) { - if (args == null || args.isEmpty) return; + log('📞 [CALL EVENT] OnCallDeclinedAsync received', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Current status: $callStatus', name: 'ChatProvider'); + + if (args == null || args.isEmpty) { + log('⚠️ [CALL EVENT] OnCallDeclinedAsync - args is null or empty', name: 'ChatProvider'); + return; + } try { final reason = args.first as String? ?? 'declined'; + log('🔵 [CALL EVENT] Decline reason: $reason', name: 'ChatProvider'); + log('✅ [CALL EVENT] Call was declined by peer', name: 'ChatProvider'); if (kDebugMode) { print('📞 Call declined: $reason'); } _teardownCall(); - } catch (e) { + } catch (e, stackTrace) { + log('❌ [CALL EVENT] Error handling call declined: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); if (kDebugMode) { print('❌ Error handling call declined: $e'); } @@ -1277,6 +1623,11 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Handle call ended event void _onCallEnded(List? args) { + log('📞 [CALL EVENT] OnHangUpAsync received', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Current status: $callStatus', name: 'ChatProvider'); + log('✅ [CALL EVENT] Call ended by peer - cleaning up', name: 'ChatProvider'); + if (kDebugMode) { print('📞 Call ended by peer'); } @@ -1286,8 +1637,14 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Handle peer audio toggle event void _onPeerAudioToggle(List? args) { + log('🎤 [CALL EVENT] OnAudioToggle received', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Previous peer mute state: $isPeerMuted', name: 'ChatProvider'); + isPeerMuted = !isPeerMuted; notifyListeners(); + log('🔵 [CALL EVENT] New peer mute state: $isPeerMuted', name: 'ChatProvider'); + log('✅ [CALL EVENT] Peer ${isPeerMuted ? "muted" : "unmuted"} their microphone', name: 'ChatProvider'); if (kDebugMode) { print('🎤 Peer ${isPeerMuted ? "muted" : "unmuted"}'); @@ -1296,24 +1653,13 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Handle peer camera toggle event void _onPeerCameraToggle(List? args) { + log('📹 [CALL EVENT] OnCameraToggle received', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Raw args: $args', name: 'ChatProvider'); + log('🔵 [CALL EVENT] Previous peer camera state: $isPeerCameraOn', name: 'ChatProvider'); + isPeerCameraOn = !isPeerCameraOn; notifyListeners(); - - if (kDebugMode) { - print('📹 Peer camera ${isPeerCameraOn ? "on" : "off"}'); - } - } - - /// Mark call as connected and start duration timer - void markCallConnected() { - if (callStatus == CallStatus.connecting) { - callStatus = CallStatus.connected; - _startCallDurationTimer(); - notifyListeners(); - - if (kDebugMode) { - print('✅ Call connected'); - } - } + log('🔵 [CALL EVENT] New peer camera state: $isPeerCameraOn', name: 'ChatProvider'); + log('✅ [CALL EVENT] Peer turned camera ${isPeerCameraOn ? "on" : "off"}', name: 'ChatProvider'); } }