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

1194 lines
47 KiB
Dart

This file contains invisible Unicode characters!

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

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

import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/main.dart';
import 'package:test_sa/modules/cx_module/chat/call/audio_call_page.dart';
import 'package:test_sa/modules/cx_module/chat/call/video_call_page.dart';
import 'package:test_sa/modules/cx_module/chat/call/services/webrtc_service.dart';
import 'package:test_sa/modules/cx_module/chat/call/services/callkit_service.dart';
import 'package:test_sa/modules/cx_module/chat/call/call_error_handler.dart';
import 'package:test_sa/modules/cx_module/chat/model/call_session.dart';
import 'package:test_sa/modules/cx_module/chat/services/signalr_service.dart';
import 'package:test_sa/modules/cx_module/chat/chat_provider.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:uuid/uuid.dart';
import 'package:test_sa/core/di/service_locator.dart';
/// CallManager - Singleton service that manages all call responsibilities
/// Works independently of ChatProvider
/// Uses SignalRService via Dependency Injection
class CallManager extends ChangeNotifier {
static final CallManager _instance = CallManager._internal();
factory CallManager() => _instance;
CallManager._internal() {
log('🏗️ [CallManager] Singleton instance created (hashCode: $hashCode)', name: 'CallManager');
}
// Services - Retrieved from DI
SignalRService get _signalRService => getIt<SignalRService>();
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<void> initialize({
required String userId,
required String authToken,
String? conversationId,
String? moduleId,
String? referenceId,
String? employeeNumber,
}) async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('🔧 [INIT] Initializing CallManager...', name: 'CallManager');
log(' User ID: $userId', name: 'CallManager');
log(' Conversation ID: ${conversationId ?? "none"}', name: 'CallManager');
// Store module context
_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<void> _initializeCallKit() async {
try {
log('📞 [CALLKIT] Initializing CallKit...', name: 'CallManager');
await _callKitService.initialize();
// Setup CallKit callbacks
_callKitService.onCallAccepted = _handleCallKitAccepted;
_callKitService.onCallDeclined = _handleCallKitDeclined;
_callKitService.onCallEnded = _handleCallKitEnded;
_callKitService.onCallTimeout = _handleCallKitTimeout;
_callKitInitialized = true;
log('✅ [CALLKIT] CallKit initialized', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [CALLKIT] Error initializing: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
}
}
/// Register SignalR call event handlers
void _registerCallHandlers() {
log('🔧 [HANDLERS] Registering call event handlers...', name: 'CallManager');
_signalRService.on('OnIncomingCallAsync', _handleIncomingCall);
_signalRService.on('OnCallAcceptedAsync', _handleCallAccepted);
_signalRService.on('OnCallDeclinedAsync', _handleCallDeclined);
_signalRService.on('OnHangUpAsync', _handleHangUp);
_signalRService.on('OnOfferAsync', _handleOffer);
_signalRService.on('OnAnswerOfferAsync', _handleAnswer);
_signalRService.on('OnIceCandidateAsync', _handleIceCandidate);
_signalRService.on('OnAudioToggle', _handleAudioToggle);
_signalRService.on('OnCameraToggle', _handleCameraToggle);
_handlersRegistered = true;
log('✅ [HANDLERS] All call handlers registered', name: 'CallManager');
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<void> startCall({
required String peerId,
required String peerName,
required CallType callType,
String? peerAvatar,
}) async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [OUTGOING] Starting ${callType.name} call to $peerName ($peerId)', name: 'CallManager');
// CRITICAL: Verify CallManager is initialized
if (_userId == null || _authToken == null) {
log('❌ [OUTGOING] CallManager not initialized!', name: 'CallManager');
log(' _userId: ${_userId ?? "NULL"}', name: 'CallManager');
log(' _authToken: ${_authToken != null ? "SET" : "NULL"}', name: 'CallManager');
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(context, 'Call system not ready. Please try again.');
}
return;
}
// CRITICAL: Verify handlers are registered
if (!_handlersRegistered) {
log('❌ [OUTGOING] Call handlers not registered!', name: 'CallManager');
log(' Attempting to register handlers now...', name: 'CallManager');
_registerCallHandlers();
}
log('✅ [OUTGOING] CallManager initialized', name: 'CallManager');
log(' User ID: $_userId', name: 'CallManager');
log(' Employee Number: $_myEmployeeNumber', name: 'CallManager');
log(' Handlers registered: $_handlersRegistered', name: 'CallManager');
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
// Update status
_updateCallStatus(CallStatus.checkingPermissions);
log('🔍 [OUTGOING] Checking permissions...', name: 'CallManager');
// Check permissions
if (!await _checkPermissions(callType)) {
log('❌ [OUTGOING] Permissions denied', name: 'CallManager');
_updateCallStatus(CallStatus.idle);
return;
}
log('✅ [OUTGOING] Permissions granted', name: 'CallManager');
// CRITICAL: Ensure SignalR is connected before every invocation
_updateCallStatus(CallStatus.connecting);
log('🔌 [OUTGOING] Ensuring SignalR connection...', name: 'CallManager');
if (!await _signalRService.ensureConnected()) {
log('❌ [OUTGOING] SignalR connection failed', name: 'CallManager');
_updateCallStatus(CallStatus.idle);
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showSignalRNotConnected(context);
}
return;
}
log('✅ [OUTGOING] SignalR connected - state: ${_signalRService.connectionState}', name: 'CallManager');
// Generate call ID
final callId = const Uuid().v4();
log('🆔 [OUTGOING] Generated call ID: $callId', name: 'CallManager');
// Create call session
_currentCall = CallSession(
callId: callId,
type: callType,
direction: CallDirection.outgoing,
peerId: peerId,
peerName: peerName,
peerAvatar: peerAvatar,
startTime: DateTime.now(),
);
// Invoke CallUserAsync
log('📤 [OUTGOING] Invoking CallUserAsync...', name: 'CallManager');
log(' From: ${_myEmployeeNumber ?? ""}', name: 'CallManager');
log(' To: $peerId', name: 'CallManager');
log(' Video: ${callType == CallType.video}', name: 'CallManager');
await _signalRService.invoke(
'CallUserAsync',
args: [
_myEmployeeNumber ?? '',
peerId,
callType == CallType.video,
],
);
log('✅ [OUTGOING] CallUserAsync invoked successfully', name: 'CallManager');
_updateCallStatus(CallStatus.outgoingRinging);
// Initialize WebRTC
log('🔧 [OUTGOING] Initializing WebRTC...', name: 'CallManager');
await _initializeWebRTC();
log('✅ [OUTGOING] WebRTC initialized', name: 'CallManager');
// Start timeout timer (60 seconds for outgoing calls)
_callTimeoutTimer = Timer(const Duration(seconds: 60), () {
if (_callStatus == CallStatus.outgoingRinging) {
log('⏱️ [OUTGOING] Call timeout - no answer after 60s', name: 'CallManager');
_cleanup();
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(context, 'Call timeout - no answer');
}
}
});
log('⏱️ [OUTGOING] Timeout timer started (60s)', name: 'CallManager');
// Navigate to call screen
_navigateToCallScreen();
log('✅ [OUTGOING] Navigated to call screen', name: 'CallManager');
log('✅ [OUTGOING] Call started successfully', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [OUTGOING] Error starting call: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(context, 'Failed to start call: $e');
}
}
}
/// Handle incoming call from notification
Future<void> handleIncomingCallNotification({
required String callId,
required String callerId,
required String callerName,
required bool isVideoCall,
Map<String, dynamic>? extraData,
}) async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [INCOMING] Handling incoming call notification', name: 'CallManager');
log(' Call ID: $callId', name: 'CallManager');
log(' Caller: $callerName ($callerId)', name: 'CallManager');
log(' Video: $isVideoCall', name: 'CallManager');
// 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<ChatProvider>(navigatorKey.currentContext!, listen: false);
final chatLoginResponse = chatProvider.chatLoginResponse;
final sender = chatProvider.sender; // Get sender for employee number
if (chatLoginResponse != null) {
log('🔧 [INCOMING] Found credentials in ChatProvider', name: 'CallManager');
log(' User ID: ${chatLoginResponse.userId}', name: 'CallManager');
log(' Employee Number: ${sender?.employeeNumber ?? "NOT AVAILABLE"}', name: 'CallManager');
await initialize(
userId: chatLoginResponse.userId.toString(),
authToken: chatLoginResponse.token ?? '',
conversationId: extraData?['conversationId'] as String?,
moduleId: extraData?['moduleId'] as String?,
referenceId: extraData?['referenceId'] as String?,
employeeNumber: sender?.employeeNumber, // Get from sender participant
);
log('✅ [INCOMING] Auto-initialization successful', 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<void> _handleCallTimeout() async {
try {
log('⏱️ [TIMEOUT] Handling call timeout...', name: 'CallManager');
if (_currentCall == null) return;
// Invoke CallMissedAsync
if (await _signalRService.ensureConnected()) {
await _signalRService.invoke('CallMissedAsync', args: [
_myEmployeeNumber ?? '',
_currentCall!.peerId,
]);
log('✅ [TIMEOUT] CallMissedAsync invoked', name: 'CallManager');
}
_cleanup();
} catch (e, stackTrace) {
log('❌ [TIMEOUT] Error: $e', name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
}
}
/// Accept incoming call
Future<void> acceptCall() async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('✅ [ACCEPT] Accepting call...', name: 'CallManager');
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<void> declineCall(String reason) async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('❌ [DECLINE] Declining call - reason: $reason', name: 'CallManager');
if (_currentCall == null) {
log('⚠️ [DECLINE] No call to decline', name: 'CallManager');
return;
}
// Cancel timeout timer
_callTimeoutTimer?.cancel();
// Ensure SignalR connected
if (await _signalRService.ensureConnected()) {
log('📤 [DECLINE] Invoking CallDeclinedAsync...', name: 'CallManager');
await _signalRService.invoke('CallDeclinedAsync', args: [
_myEmployeeNumber ?? '',
_currentCall!.peerId,
reason,
]);
log('✅ [DECLINE] CallDeclinedAsync invoked', name: 'CallManager');
} else {
log('⚠️ [DECLINE] SignalR not connected, skipping backend call', name: 'CallManager');
}
_cleanup();
log('✅ [DECLINE] Call declined successfully', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [DECLINE] Error declining call: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
}
}
/// Hang up call
Future<void> hangUp() async {
try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [HANGUP] Hanging up...', name: 'CallManager');
if (_currentCall == null) {
log('⚠️ [HANGUP] No call to hang up', name: 'CallManager');
return;
}
// Ensure SignalR connected
if (await _signalRService.ensureConnected()) {
log('📤 [HANGUP] Invoking HangUpAsync...', name: 'CallManager');
await _signalRService.invoke('HangUpAsync', args: [
_myEmployeeNumber ?? '',
_currentCall!.peerId,
_moduleId ?? '0',
_referenceId ?? '0',
_conversationId ?? '',
]);
log('✅ [HANGUP] HangUpAsync invoked', name: 'CallManager');
} else {
log('⚠️ [HANGUP] SignalR not connected, skipping backend call', name: 'CallManager');
}
_cleanup();
log('✅ [HANGUP] Call ended successfully', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [HANGUP] Error hanging up: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
_cleanup();
}
}
/// Toggle mute
Future<void> toggleMute() async {
log('🎤 [CONTROL] Toggling mute: $_isMuted -> ${!_isMuted}', name: 'CallManager');
_isMuted = !_isMuted;
_webrtcService?.setMicrophoneMuted(_isMuted);
if (await _signalRService.ensureConnected()) {
await _signalRService.invoke('AudioToggle', args: [
_myEmployeeNumber ?? '',
_currentCall?.peerId ?? '',
]);
}
notifyListeners();
log('✅ [CONTROL] Mute toggled: $_isMuted', name: 'CallManager');
}
/// Toggle speaker
Future<void> toggleSpeaker() async {
log('🔊 [CONTROL] Toggling speaker: $_isSpeakerOn -> ${!_isSpeakerOn}', name: 'CallManager');
_isSpeakerOn = !_isSpeakerOn;
await _webrtcService?.setSpeakerphoneEnabled(_isSpeakerOn);
notifyListeners();
log('✅ [CONTROL] Speaker toggled: $_isSpeakerOn', name: 'CallManager');
}
/// Toggle camera
Future<void> toggleCamera() async {
if (_currentCall?.type != CallType.video) return;
log('📹 [CONTROL] Toggling camera: $_isCameraOn -> ${!_isCameraOn}', name: 'CallManager');
_isCameraOn = !_isCameraOn;
_webrtcService?.setCameraEnabled(_isCameraOn);
if (await _signalRService.ensureConnected()) {
await _signalRService.invoke('CameraToggle', args: [
_myEmployeeNumber ?? '',
_currentCall?.peerId ?? '',
]);
}
notifyListeners();
log('✅ [CONTROL] Camera toggled: $_isCameraOn', name: 'CallManager');
}
/// Switch camera
Future<void> switchCamera() async {
if (_currentCall?.type != CallType.video || !_isCameraOn) return;
log('🔄 [CONTROL] Switching camera...', name: 'CallManager');
await _webrtcService?.switchCamera();
log('✅ [CONTROL] Camera switched', name: 'CallManager');
}
// ==================== Private Methods ====================
Future<bool> _checkPermissions(CallType callType) async {
log('🔍 [PERMISSIONS] Checking permissions for ${callType.name} call...', name: 'CallManager');
final micStatus = await Permission.microphone.request();
if (!micStatus.isGranted) {
log('❌ [PERMISSIONS] Microphone permission denied', name: 'CallManager');
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showMicrophonePermissionDenied(context);
}
return false;
}
log('✅ [PERMISSIONS] Microphone permission granted', name: 'CallManager');
if (callType == CallType.video) {
final cameraStatus = await Permission.camera.request();
if (!cameraStatus.isGranted) {
log('❌ [PERMISSIONS] Camera permission denied', name: 'CallManager');
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showCameraPermissionDenied(context);
}
return false;
}
log('✅ [PERMISSIONS] Camera permission granted', name: 'CallManager');
}
return true;
}
Future<void> _initializeWebRTC() async {
log('🔧 [WEBRTC] Initializing WebRTC...', name: 'CallManager');
_webrtcService = WebRTCService();
_setupWebRTCCallbacks();
if (_currentCall!.type == CallType.audio) {
log('🎵 [WEBRTC] Initializing for audio call', name: 'CallManager');
await _webrtcService!.initializeForAudioCall();
} else {
log('📹 [WEBRTC] Initializing for video call', name: 'CallManager');
await _webrtcService!.initializeForVideoCall();
}
log('✅ [WEBRTC] WebRTC initialized', name: 'CallManager');
}
void _setupWebRTCCallbacks() {
log('🔧 [WEBRTC] Setting up callbacks...', name: 'CallManager');
_webrtcService!.onIceCandidate = (RTCIceCandidate candidate) {
log('🧊 [WEBRTC] ICE candidate generated', name: 'CallManager');
log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', name: 'CallManager');
final candidateJson = jsonEncode({
'candidate': candidate.candidate,
'sdpMid': candidate.sdpMid,
'sdpMLineIndex': candidate.sdpMLineIndex,
});
_signalRService.invoke('IceCandidateAsync', args: [
_currentCall!.peerId,
candidateJson,
_currentCall!.callId,
]);
};
_webrtcService!.onRemoteStream = (MediaStream stream) {
log('📡 [WEBRTC] Remote stream received', name: 'CallManager');
log(' Audio tracks: ${stream.getAudioTracks().length}', name: 'CallManager');
log(' Video tracks: ${stream.getVideoTracks().length}', name: 'CallManager');
notifyListeners();
};
_webrtcService!.onIceConnectionStateChange = (RTCIceConnectionState state) {
log('🔗 [WEBRTC] ICE state: ${state.toString()}', name: 'CallManager');
if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) {
log('✅ [WEBRTC] ICE connection established', name: 'CallManager');
_updateCallStatus(CallStatus.connected);
_startCallDurationTimer();
} else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) {
log('❌ [WEBRTC] ICE connection failed', name: 'CallManager');
_cleanup();
} else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
log('⚠️ [WEBRTC] ICE connection disconnected', name: 'CallManager');
}
};
log('✅ [WEBRTC] Callbacks set up', name: 'CallManager');
}
void _navigateToCallScreen() {
final context = navigatorKey.currentContext;
if (context == null || _currentCall == null) {
log('⚠️ [NAV] Cannot navigate - context or call is null', name: 'CallManager');
return;
}
log('🧭 [NAV] Navigating to ${_currentCall!.type.name} call screen', name: 'CallManager');
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _currentCall!.type == CallType.audio
? const AudioCallPage()
: const VideoCallPage(),
),
);
}
void _updateCallStatus(CallStatus newStatus) {
if (_callStatus == newStatus) return;
log('🔄 [STATUS] ${_callStatus.name} -> ${newStatus.name}', name: 'CallManager');
_callStatus = newStatus;
notifyListeners();
}
void _startCallDurationTimer() {
_callDurationTimer?.cancel();
_callDuration = Duration.zero;
log('⏱️ [TIMER] Starting call duration timer', name: 'CallManager');
_callDurationTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_callDuration = Duration(seconds: _callDuration.inSeconds + 1);
notifyListeners();
});
}
void _cleanup() {
log('🧹 [CLEANUP] Starting cleanup...', name: 'CallManager');
_callTimeoutTimer?.cancel();
_callDurationTimer?.cancel();
_webrtcService?.dispose();
_webrtcService = null;
if (_currentCall != null) {
_callKitService.endCall(_currentCall!.callId);
}
_currentCall = null;
_updateCallStatus(CallStatus.idle);
_callDuration = Duration.zero;
_isMuted = false;
_isSpeakerOn = false;
_isCameraOn = true;
_isPeerMuted = false;
_isPeerCameraOn = true;
log('✅ [CLEANUP] Cleanup complete', name: 'CallManager');
}
/// Reset service (for logout or testing)
Future<void> reset() async {
log('🔄 [RESET] Resetting CallManager...', name: 'CallManager');
_cleanup();
_callKitInitialized = false;
_handlersRegistered = false;
_moduleId = null;
_referenceId = null;
_conversationId = null;
_myEmployeeNumber = null;
log('✅ [RESET] CallManager reset complete', name: 'CallManager');
}
// ==================== SignalR Event Handlers ====================
void _handleIncomingCall(List<Object?>? args) {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] ⚠️ OnIncomingCallAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Args: $args', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
// Note: Incoming calls are now handled by CallNotificationService
// This event is kept for chat screen integration
log(' [EVENT] Incoming call handled by notification service', name: 'CallManager');
}
void _handleCallAccepted(List<Object?>? args) async {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] ✅ OnCallAcceptedAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Args: $args', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
log(' Current call: ${_currentCall?.callId}', name: 'CallManager');
log(' Call direction: ${_currentCall?.direction.name}', name: 'CallManager');
log(' Peer: ${_currentCall?.peerId}', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
// CRITICAL: Only the CALLER should create an offer
// The RECEIVER should wait for OnOfferAsync
if (_currentCall?.direction != CallDirection.outgoing) {
log('⚠️ [EVENT] This is an INCOMING call - receiver should NOT create offer', name: 'CallManager');
log(' Current direction: ${_currentCall?.direction.name}', name: 'CallManager');
log(' ✅ Receiver will wait for OnOfferAsync from caller', name: 'CallManager');
return;
}
// FIXED: Accept event in both connecting and outgoingRinging states
// Backend might send OnCallAcceptedAsync very quickly before status changes to outgoingRinging
if (_callStatus != CallStatus.connecting && _callStatus != CallStatus.outgoingRinging) {
log('⚠️ [EVENT] Not in valid state for creating offer (current: ${_callStatus.name})', name: 'CallManager');
log(' Expected: connecting OR outgoingRinging', name: 'CallManager');
return;
}
// If we're still in connecting, update to outgoingRinging first
if (_callStatus == CallStatus.connecting) {
log('🔄 [EVENT] Status was connecting, updating to outgoingRinging first', name: 'CallManager');
_updateCallStatus(CallStatus.outgoingRinging);
}
_callTimeoutTimer?.cancel();
_updateCallStatus(CallStatus.connecting);
try {
// Ensure WebRTC is initialized before creating offer
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<Object?>? args) {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] ❌ OnCallDeclinedAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Args: $args', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
// Extract decline reason if available
String? reason;
if (args != null && args.isNotEmpty) {
reason = args[0]?.toString();
log(' Decline reason: $reason', name: 'CallManager');
}
// Show appropriate message
final context = navigatorKey.currentContext;
if (context != null && reason != null) {
if (reason == 'USER_BUSY') {
CallErrorHandler.showGenericError(context, 'The user is currently busy on another call');
} else if (reason == 'USER_OFFLINE') {
CallErrorHandler.showGenericError(context, 'The user is currently offline');
}
}
_cleanup();
}
void _handleHangUp(List<Object?>? args) {
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');
// CRITICAL: Navigate back to close call screen on both devices
final context = navigatorKey.currentContext;
if (context != null && Navigator.of(context).canPop()) {
log('🧭 [HANGUP] Navigating back to close call screen', name: 'CallManager');
Navigator.of(context).pop();
}
_cleanup();
log('✅ [HANGUP] Call ended and screen closed', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
}
void _handleOffer(List<Object?>? args) async {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] 📥 OnOfferAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
log(' 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<Object?>? args) async {
log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] 📥 OnAnswerOfferAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
log(' WebRTC initialized: ${_webrtcService != null}', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
if (args == null || args.isEmpty) {
log('⚠️ [EVENT] No answer data received', name: 'CallManager');
return;
}
try {
final answerSdp = args[0] as String?;
if (answerSdp == null || answerSdp.isEmpty) {
log('⚠️ [EVENT] Answer SDP is null or empty', name: 'CallManager');
return;
}
log(' Answer SDP length: ${answerSdp.length}', name: 'CallManager');
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<Object?>? args) async {
log('📞 [EVENT] 🧊 OnIceCandidateAsync received', name: 'CallManager');
if (args == null || args.isEmpty) {
log('⚠️ [EVENT] No ICE candidate data received', name: 'CallManager');
return;
}
try {
final candidateJson = args[0] as String?;
if (candidateJson == null) {
log('⚠️ [EVENT] Candidate JSON is null', name: 'CallManager');
return;
}
if (_webrtcService == null) {
log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager');
return;
}
final candidateData = jsonDecode(candidateJson) as Map<String, dynamic>;
final candidate = RTCIceCandidate(
candidateData['candidate'] as String?,
candidateData['sdpMid'] as String?,
candidateData['sdpMLineIndex'] as int?,
);
log('🧊 [EVENT] Adding remote ICE candidate...', name: 'CallManager');
log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', name: 'CallManager');
await _webrtcService!.addIceCandidate(candidate);
log('✅ [EVENT] Remote ICE candidate added', name: 'CallManager');
} catch (e, stackTrace) {
log('❌ [EVENT] Error handling ICE candidate: $e',
name: 'CallManager', error: e, stackTrace: stackTrace);
}
}
void _handleAudioToggle(List<Object?>? args) {
log('📞 [EVENT] 🎤 OnAudioToggle received', name: 'CallManager');
log(' Peer muted: $_isPeerMuted -> ${!_isPeerMuted}', name: 'CallManager');
_isPeerMuted = !_isPeerMuted;
notifyListeners();
}
void _handleCameraToggle(List<Object?>? args) {
log('📞 [EVENT] 📹 OnCameraToggle received', name: 'CallManager');
log(' Peer camera: $_isPeerCameraOn -> ${!_isPeerCameraOn}', name: 'CallManager');
_isPeerCameraOn = !_isPeerCameraOn;
notifyListeners();
}
// ==================== CallKit Event Handlers ====================
void _handleCallKitAccepted(String callId) {
log('✅ [CALLKIT] Call accepted: $callId', name: 'CallManager');
if (_currentCall?.callId == callId) {
acceptCall();
} else {
log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager');
}
}
void _handleCallKitDeclined(String callId) {
log('❌ [CALLKIT] Call declined: $callId', name: 'CallManager');
if (_currentCall?.callId == callId) {
declineCall('user_declined');
} else {
log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager');
}
}
void _handleCallKitEnded(String callId) {
log('🔴 [CALLKIT] Call ended: $callId', name: 'CallManager');
if (_currentCall?.callId == callId) {
hangUp();
} else {
log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager');
}
}
void _handleCallKitTimeout(String callId) {
log('⏱️ [CALLKIT] Call timeout: $callId', name: 'CallManager');
if (_currentCall?.callId == callId) {
_handleCallTimeout();
} else {
log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager');
}
}
}