diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4be07bc5..f791d99c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ - + @@ -17,9 +18,10 @@ - - + + + @@ -141,5 +143,15 @@ + + + diff --git a/ios/Runner.plist/Info.plist b/ios/Runner.plist/Info.plist index 55a3a3d4..846b6656 100644 --- a/ios/Runner.plist/Info.plist +++ b/ios/Runner.plist/Info.plist @@ -46,6 +46,11 @@ LSApplicationQueriesSchemes tel + + UIBackgroundModes + + audio + voip UIViewControllerBasedStatusBarAppearance @@ -53,6 +58,9 @@ Recognize Speech NSMicrophoneUsageDescription To Record Audio + NSCameraUsageDescription + Camera access is required for video calls. + NSCalendarsUsageDescription This app requires access to your calendar to help you manage events and schedule appointments. NSContactsUsageDescription diff --git a/lib/controllers/notification/firebase_notification_manger.dart b/lib/controllers/notification/firebase_notification_manger.dart index b909df67..5a647ac9 100644 --- a/lib/controllers/notification/firebase_notification_manger.dart +++ b/lib/controllers/notification/firebase_notification_manger.dart @@ -25,26 +25,15 @@ import 'package:flutter_callkit_incoming/entities/entities.dart'; @pragma('vm:entry-point') Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { try { - log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger'); - log('🚀 [BACKGROUND ISOLATE] Handler started', name: 'FirebaseNotificationManger'); - log('📱 [BACKGROUND] Push notification received', name: 'FirebaseNotificationManger'); - log('📱 [BACKGROUND] Message ID: ${message.messageId}', name: 'FirebaseNotificationManger'); log('📱 [BACKGROUND] Data payload: ${message.data}', name: 'FirebaseNotificationManger'); - log('📱 [BACKGROUND] Notification title: ${message.notification?.title}', name: 'FirebaseNotificationManger'); - log('📱 [BACKGROUND] Notification body: ${message.notification?.body}', name: 'FirebaseNotificationManger'); - final notificationType = message.data['notificationType'] as String? ?? message.data['type'] as String?; final transactionType = message.data['transactionType'] as String?; log('📱 [BACKGROUND] Notification Type: $notificationType', name: 'FirebaseNotificationManger'); - log('📱 [BACKGROUND] Transaction Type: $transactionType', name: 'FirebaseNotificationManger'); // Handle incoming call notifications if (notificationType == 'incoming_call' || transactionType == 'call') { - log('📞 [BACKGROUND] Incoming call notification detected', name: 'FirebaseNotificationManger'); - log('📞 [BACKGROUND] Call data:', name: 'FirebaseNotificationManger'); - // Extract call data final callId = message.data['callId'] as String? ?? message.data['sessionId'] as String? ?? @@ -75,19 +64,9 @@ Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { final referenceId = message.data['referenceId'] as String?; final conversationId = message.data['conversationId'] as String?; - log(' - callId: $callId', name: 'FirebaseNotificationManger'); - log(' - callerId: $callerId', name: 'FirebaseNotificationManger'); - log(' - callerName: $callerName', name: 'FirebaseNotificationManger'); - log(' - callerEmployeeNumber: $callerEmployeeNumber', name: 'FirebaseNotificationManger'); - log(' - isVideoCall: $isVideoCall', name: 'FirebaseNotificationManger'); - log(' - moduleId: $moduleId', name: 'FirebaseNotificationManger'); - log(' - referenceId: $referenceId', name: 'FirebaseNotificationManger'); - log(' - conversationId: $conversationId', name: 'FirebaseNotificationManger'); - // CRITICAL FIX: DO NOT initialize any communication services in background isolate // Only persist call data and show CallKit - - log('💾 [BACKGROUND] Saving call data to SharedPreferences...', name: 'FirebaseNotificationManger'); + ///TODO need to find other solution to this after testing await PendingCallStorage.savePendingCall( callId: callId, callerId: callerId, @@ -98,8 +77,6 @@ Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { referenceId: referenceId, conversationId: conversationId, ); - log('✅ [BACKGROUND] Call data saved successfully', name: 'FirebaseNotificationManger'); - // Show CallKit incoming call UI log('📱 [BACKGROUND] Displaying CallKit UI...', name: 'FirebaseNotificationManger'); final callKitParams = CallKitParams( @@ -158,7 +135,6 @@ Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { await FlutterCallkitIncoming.showCallkitIncoming(callKitParams); log('✅ [BACKGROUND] CallKit UI displayed', name: 'FirebaseNotificationManger'); - log('ℹ️ [BACKGROUND] Main isolate will initialize communication services when app resumes', name: 'FirebaseNotificationManger'); } else { log('📱 [BACKGROUND] Non-call notification type', name: 'FirebaseNotificationManger'); } @@ -213,10 +189,6 @@ class FirebaseNotificationManger { } static void _onMessageReceived(h_push.RemoteMessage remoteMessage) { - log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger'); - log('📱 [HUAWEI] Message received', name: 'FirebaseNotificationManger'); - log('📱 [HUAWEI] Full message data: ${remoteMessage.toMap()}', name: 'FirebaseNotificationManger'); - log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger'); } static void _onMessageReceiveError(Object error) { @@ -247,26 +219,9 @@ class FirebaseNotificationManger { final transactionType = messageData['transactionType'] as String?; log('📱 [HANDLE_MESSAGE] Notification Type: $notificationType', name: 'FirebaseNotificationManger'); - log('📱 [HANDLE_MESSAGE] Transaction Type: $transactionType', name: 'FirebaseNotificationManger'); if (notificationType == 'incoming_call' || transactionType == 'call') { - log('📞 [FOREGROUND] Incoming call notification detected', name: 'FirebaseNotificationManger'); - log('📞 [FOREGROUND] Call data:', name: 'FirebaseNotificationManger'); - log(' - callId: ${messageData['callId']}', name: 'FirebaseNotificationManger'); - log(' - callerId: ${messageData['callerId']}', name: 'FirebaseNotificationManger'); - log(' - calleeEmployeeNumber: ${messageData['calleeEmployeeNumber']}', name: 'FirebaseNotificationManger'); - log(' - callerName: ${messageData['callerName']}', name: 'FirebaseNotificationManger'); - log(' - callerUserName: ${messageData['callerUserName']}', name: 'FirebaseNotificationManger'); - log(' - callerEmployeeNumber: ${messageData['callerEmployeeNumber']}', name: 'FirebaseNotificationManger'); - log(' - isVideoCall: ${messageData['isVideoCall']}', name: 'FirebaseNotificationManger'); - log(' - callType: ${messageData['callType']}', name: 'FirebaseNotificationManger'); - log(' - moduleId: ${messageData['moduleId']}', name: 'FirebaseNotificationManger'); - log(' - referenceId: ${messageData['referenceId']}', name: 'FirebaseNotificationManger'); - log(' - conversationId: ${messageData['conversationId']}', name: 'FirebaseNotificationManger'); - log(' - applicationId: ${messageData['applicationId']}', name: 'FirebaseNotificationManger'); - // CRITICAL FIX: Show CallKit UI in foreground just like background - // Extract call data (same as background handler) final callId = messageData['callId'] as String? ?? messageData['sessionId'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString(); @@ -356,7 +311,6 @@ class FirebaseNotificationManger { await FlutterCallkitIncoming.showCallkitIncoming(callKitParams); log('✅ [FOREGROUND] CallKit UI displayed successfully', name: 'FirebaseNotificationManger'); - log('ℹ️ [FOREGROUND] SignalR OnIncomingCallAsync will handle call state in CallManager', name: 'FirebaseNotificationManger'); } catch (e, stackTrace) { log('❌ [FOREGROUND] Failed to show CallKit UI: $e\nStack: $stackTrace', name: 'FirebaseNotificationManger'); } @@ -364,11 +318,7 @@ class FirebaseNotificationManger { log('���══════════════════════════════════════════', name: 'FirebaseNotificationManger'); return; } - - log('📱 [HANDLE_MESSAGE] Non-call notification - processing normally', name: 'FirebaseNotificationManger'); log('📱 [HANDLE_MESSAGE] Request Type: ${messageData["requestType"]}', name: 'FirebaseNotificationManger'); - log('📱 [HANDLE_MESSAGE] Request Number: ${messageData["requestNumber"]}', name: 'FirebaseNotificationManger'); - log('📱 [HANDLE_MESSAGE] Module ID: ${messageData["moduleId"]}', name: 'FirebaseNotificationManger'); if (messageData["requestType"] != null && messageData["requestNumber"] != null) { Widget? serviceClass; @@ -482,25 +432,18 @@ class FirebaseNotificationManger { } static initialized(BuildContext context) async { - log('🔧 [INIT] Initializing FirebaseNotificationManger', name: 'FirebaseNotificationManger'); //TOD0 add platform check here also if (!(await isGoogleServicesAvailable()) && Platform.isAndroid) { log('📱 [INIT] Using Huawei Push Services', name: 'FirebaseNotificationManger'); - // NEW: Handle Huawei initial notification var initialNotification = await h_push.Push.getInitialNotification(); if (initialNotification != null) { - log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger'); - log('📱 [HUAWEI_INITIAL] Initial notification found', name: 'FirebaseNotificationManger'); - log('📱 [HUAWEI_INITIAL] Full data: $initialNotification', name: 'FirebaseNotificationManger'); - Map remoteData = Map.from(initialNotification["extras"] as Map); log('📱 [HUAWEI_INITIAL] Extras data: $remoteData', name: 'FirebaseNotificationManger'); // Check if it's a call notification final notificationType = remoteData['notificationType'] as String?; - log('📱 [HUAWEI_INITIAL] Notification Type: $notificationType', name: 'FirebaseNotificationManger'); if (notificationType == 'incoming_call') { log('📞 [HUAWEI_INITIAL] Call notification - will be handled by SignalR', name: 'FirebaseNotificationManger'); @@ -516,11 +459,6 @@ class FirebaseNotificationManger { h_push.Push.onNotificationOpenedApp.listen((message) { try { - log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger'); - log('📱 [HUAWEI_TAP] Notification tapped', name: 'FirebaseNotificationManger'); - log('📱 [HUAWEI_TAP] Message type: ${message.runtimeType}', name: 'FirebaseNotificationManger'); - log('📱 [HUAWEI_TAP] Full message: $message', name: 'FirebaseNotificationManger'); - if (message is Map) { Map remoteData = message; remoteData = remoteData["extras"]; @@ -576,13 +514,6 @@ class FirebaseNotificationManger { FirebaseMessaging.instance.getInitialMessage().then((initialMessage) { if (initialMessage != null) { - log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger'); - log('📱 [FCM_INITIAL] Initial message found', name: 'FirebaseNotificationManger'); - log('📱 [FCM_INITIAL] Message ID: ${initialMessage.messageId}', name: 'FirebaseNotificationManger'); - log('📱 [FCM_INITIAL] Data: ${initialMessage.data}', name: 'FirebaseNotificationManger'); - log('📱 [FCM_INITIAL] Title: ${initialMessage.notification?.title}', name: 'FirebaseNotificationManger'); - log('📱 [FCM_INITIAL] Body: ${initialMessage.notification?.body}', name: 'FirebaseNotificationManger'); - // Check if it's a call notification final notificationType = initialMessage.data['notificationType'] as String? ?? initialMessage.data['type'] as String?; diff --git a/lib/core/di/service_locator.dart b/lib/core/di/service_locator.dart index 743ae269..3e1464dd 100644 --- a/lib/core/di/service_locator.dart +++ b/lib/core/di/service_locator.dart @@ -12,12 +12,7 @@ final getIt = GetIt.instance; /// This is called ONCE during app startup in the MAIN ISOLATE ONLY /// NEVER call this from background isolate Future setupServiceLocator() async { - log('🔧 [DI] Setting up service locator (main isolate)...', name: 'ServiceLocator'); - - // Register all services with duplicate checks await _registerServices(); - - log('✅ [DI] Service locator initialized', name: 'ServiceLocator'); } /// Internal helper to register services with duplicate protection @@ -25,7 +20,6 @@ Future _registerServices() async { // 1. SignalR Service if (!getIt.isRegistered()) { getIt.registerLazySingleton(() => SignalRService()); - log(' ✅ SignalRService registered as singleton', name: 'ServiceLocator'); } else { log(' ⚠️ SignalRService already registered, skipping', name: 'ServiceLocator'); } @@ -33,7 +27,6 @@ Future _registerServices() async { // 2. WebRTC Service if (!getIt.isRegistered()) { getIt.registerLazySingleton(() => WebRTCService()); - log(' ✅ WebRTCService registered as singleton', name: 'ServiceLocator'); } else { log(' ⚠️ WebRTCService already registered, skipping', name: 'ServiceLocator'); } @@ -41,7 +34,6 @@ Future _registerServices() async { // 3. CallManager (depends on SignalRService, WebRTCService) if (!getIt.isRegistered()) { getIt.registerLazySingleton(() => CallManager()); - log(' ✅ CallManager registered as singleton', name: 'ServiceLocator'); } else { log(' ⚠️ CallManager already registered, skipping', name: 'ServiceLocator'); } @@ -49,7 +41,6 @@ Future _registerServices() async { // 4. CallNotificationService (depends on CallManager) if (!getIt.isRegistered()) { getIt.registerLazySingleton(() => CallNotificationService()); - log(' ✅ CallNotificationService registered as singleton', name: 'ServiceLocator'); } else { log(' ⚠️ CallNotificationService already registered, skipping', name: 'ServiceLocator'); } @@ -67,7 +58,6 @@ void _verifyDependencies() { CallNotificationService, ]; - log('🔍 [DI] Verifying dependencies...', name: 'ServiceLocator'); bool allRegistered = true; for (final type in dependencies) { @@ -80,7 +70,6 @@ void _verifyDependencies() { } if (allRegistered) { - log('✅ [DI] All dependencies verified successfully', name: 'ServiceLocator'); } else { log('❌ [DI] Some dependencies are missing!', name: 'ServiceLocator'); throw StateError('Required dependencies are not registered in GetIt'); @@ -97,7 +86,6 @@ Future resetServices() async { if (getIt.isRegistered()) { await getIt().reset(); } - log('✅ [DI] All services reset', name: 'ServiceLocator'); } catch (e) { log('❌ [DI] Error resetting services: $e', name: 'ServiceLocator'); } diff --git a/lib/main.dart b/lib/main.dart index e0b81ce6..458bc81e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:localization/localization.dart'; import 'package:flutter_timezone/flutter_timezone.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/providers/api/all_requests_provider.dart'; import 'package:test_sa/controllers/providers/api/comments_provider.dart'; @@ -188,7 +189,8 @@ void main() async { /// only portrait mode SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); - + ///todo remove this after testing call functionality + await requestCallPermissions(); runApp(ChangeNotifierProvider(create: (_) => SettingProvider(), child: const MyApp())); } @@ -205,6 +207,13 @@ Future _configureLocalTimeZone() async { return; } } +///Temporary function to request camera and microphone permissions for call functionality testing +Future requestCallPermissions() async { + await [ + Permission.camera, + Permission.microphone, + ].request(); +} final navigatorKey = GlobalKey(); diff --git a/lib/modules/cm_module/cm_detail_page.dart b/lib/modules/cm_module/cm_detail_page.dart index e9de2371..cba5508a 100644 --- a/lib/modules/cm_module/cm_detail_page.dart +++ b/lib/modules/cm_module/cm_detail_page.dart @@ -49,23 +49,6 @@ class _CMDetailPageState extends State { super.initState(); _requestProvider = Provider.of(context, listen: false); WidgetsBinding.instance.addPostFrameCallback((_) { - // CRITICAL FIX: NEVER reset ChatProvider/SignalR connection - // CallManager already initialized it from LandPage and is using it for calls - // Resetting will disconnect SignalR and break incoming calls - - final chatProvider = Provider.of(context, listen: false); - final signalRService = getIt(); - - log('📋 [CMDetailPage] Checking chat state...', name: 'CMDetailPage'); - log(' SignalR connected: ${signalRService.isConnected}', name: 'CMDetailPage'); - log(' Chat logged in: ${chatProvider.chatLoginResponse != null}', name: 'CMDetailPage'); - log(' Current reference: ${chatProvider.referenceID}', name: 'CMDetailPage'); - log(' New reference: ${widget.requestId}', name: 'CMDetailPage'); - - // DON'T RESET - just let ChatWidget handle its own initialization - // ChatWidget will check credentials and update conversation if needed - log('✅ [CMDetailPage] Skipping reset - ChatWidget will handle initialization', name: 'CMDetailPage'); - getInitialData(); }); } diff --git a/lib/modules/cx_module/chat/call/audio_call_page.dart b/lib/modules/cx_module/chat/call/audio_call_page.dart index 30bf50eb..98702636 100644 --- a/lib/modules/cx_module/chat/call/audio_call_page.dart +++ b/lib/modules/cx_module/chat/call/audio_call_page.dart @@ -1,16 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart'; import 'package:test_sa/modules/cx_module/chat/model/call_session.dart'; -import 'package:test_sa/modules/cx_module/chat/call/video_call_page.dart'; +import 'package:test_sa/modules/cx_module/chat/call/utils/permission_helper.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; -import 'package:test_sa/new_views/app_style/app_themes.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; class AudioCallPage extends StatefulWidget { @@ -29,6 +25,30 @@ class _AudioCallPageState extends State { _callManager = CallManager(); // Listen for call end and navigate back _callManager.addListener(_checkCallStatus); + + // CRITICAL: Check permissions after first frame + // Permissions must be requested in UI layer, not service layer + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkPermissions(); + }); + } + + /// Request audio call permissions using production-ready helper + Future _checkPermissions() async { + if (!mounted) return; + + // Request all audio call permissions (microphone) + final granted = await CallPermissionHelper.requestAudioCallPermissions(context); + + if (!granted && mounted) { + // Permission denied - end the call gracefully + await CallPermissionHelper.handlePermissionDeniedDuringCall( + context, + 'Microphone', + onEndCall: () => _callManager.declineCall('permission_denied'), + delaySeconds: 2, + ); + } } void _checkCallStatus() { diff --git a/lib/modules/cx_module/chat/call/services/callkit_service.dart b/lib/modules/cx_module/chat/call/services/callkit_service.dart index 60c3b6e1..7534861c 100644 --- a/lib/modules/cx_module/chat/call/services/callkit_service.dart +++ b/lib/modules/cx_module/chat/call/services/callkit_service.dart @@ -1,9 +1,7 @@ import 'dart:async'; import 'dart:developer'; -import 'package:flutter/foundation.dart'; import 'package:flutter_callkit_incoming/entities/entities.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; -import 'package:uuid/uuid.dart'; import 'package:test_sa/modules/cx_module/chat/services/pending_call_storage.dart'; /// Service to handle CallKit (iOS) and ConnectionService (Android) integration @@ -30,165 +28,51 @@ class CallKitService { /// Initialize CallKit service and listen for events Future initialize() async { - // CRITICAL: Prevent duplicate initialization if (_isInitialized) { - if (kDebugMode) { - log('⚠️ [CallKit] Already initialized, skipping duplicate initialization', name: 'CallKitService'); - } return; } - if (kDebugMode) { - log('📞 [CallKit] Initializing CallKit service', name: 'CallKitService'); - } - - // Listen for CallKit events - ONLY ONCE _eventSubscription = FlutterCallkitIncoming.onEvent.listen(_handleCallKitEvent); _isInitialized = true; - - if (kDebugMode) { - log('✅ [CallKit] Service initialized (singleton event listener registered)', name: 'CallKitService'); - } } /// Handle CallKit events from native side void _handleCallKitEvent(CallEvent? event) { if (event == null) return; - if (kDebugMode) { - log('📞 [CallKit] Event received: ${event.toString()}', name: 'CallKitService'); - log('📞 [CallKit] Event type: ${event.eventName}', name: 'CallKitService'); - } - - // Handle different event types using pattern matching switch (event) { case CallEventActionCallAccept(:final callKitParams): - // User accepted the call via CallKit UI final callId = callKitParams.id; - if (kDebugMode) { - log('✅ [CallKit] Call ACCEPTED via native UI - calling callback', name: 'CallKitService'); - log(' Call ID: $callId', name: 'CallKitService'); - } - - // CRITICAL: Mark call as accepted for background/terminated app restoration PendingCallStorage.markCallAcceptedFromBackground(callId); - if (kDebugMode) { - log('✅ [CallKit] Marked call as accepted in storage for main isolate', name: 'CallKitService'); - } - onCallAccepted?.call(callId); break; case CallEventActionCallDecline(:final callKitParams): - // User declined the call via CallKit UI final callId = callKitParams.id; - if (kDebugMode) { - log('❌ [CallKit] Call DECLINED via native UI - calling callback', name: 'CallKitService'); - log(' Call ID: $callId', name: 'CallKitService'); - } onCallDeclined?.call(callId); break; case CallEventActionCallEnded(:final callKitParams): - // User ended the call via CallKit UI final callId = callKitParams.id; - if (kDebugMode) { - log('🔴 [CallKit] Call ENDED via native UI - calling callback', name: 'CallKitService'); - log(' Call ID: $callId', name: 'CallKitService'); - } onCallEnded?.call(callId); break; case CallEventActionCallTimeout(:final id): - // Call timed out (no answer) - if (kDebugMode) { - log('⏱️ [CallKit] Call TIMEOUT - calling callback', name: 'CallKitService'); - log(' Call ID: $id', name: 'CallKitService'); - } onCallTimeout?.call(id); break; - case CallEventActionCallToggleMute(:final id, :final isMuted): - // User toggled mute via CallKit UI - if (kDebugMode) { - log('🔇 [CallKit] Mute toggled: $isMuted for call $id', name: 'CallKitService'); - } - break; - - case CallEventActionCallToggleHold(:final id, :final isOnHold): - // User toggled hold via CallKit UI - if (kDebugMode) { - log('⏸️ [CallKit] Hold toggled: $isOnHold for call $id', name: 'CallKitService'); - } - break; - - case CallEventActionCallIncoming(:final callKitParams): - // Incoming call notification shown - if (kDebugMode) { - log('📲 [CallKit] Incoming call notification for: ${callKitParams.id}', name: 'CallKitService'); - } - break; - - case CallEventActionCallStart(:final callKitParams): - // Call started - if (kDebugMode) { - log('📞 [CallKit] Call started: ${callKitParams.id}', name: 'CallKitService'); - } - break; - - case CallEventActionCallCallback(:final id): - // Callback event - if (kDebugMode) { - log('📞 [CallKit] Callback event for call: $id', name: 'CallKitService'); - } - break; - - case CallEventActionCallConnected(:final id): - // Call connected - if (kDebugMode) { - log('✅ [CallKit] Call connected: $id', name: 'CallKitService'); - } - break; - + case CallEventActionCallToggleMute(): + case CallEventActionCallToggleHold(): + case CallEventActionCallIncoming(): + case CallEventActionCallStart(): + case CallEventActionCallCallback(): + case CallEventActionCallConnected(): case CallEventActionDidUpdateDevicePushTokenVoip(): - // Push token updated - if (kDebugMode) { - log('🔔 [CallKit] Device push token updated', name: 'CallKitService'); - } - break; - - case CallEventActionCallToggleDmtf(:final id, :final digits, :final type): - // DTMF toggled - if (kDebugMode) { - log('🔢 [CallKit] DTMF toggled: $digits for call $id', name: 'CallKitService'); - } - break; - - case CallEventActionCallToggleGroup(:final id, :final callUUIDToGroupWith): - // Call group toggled - if (kDebugMode) { - log('👥 [CallKit] Group toggled for call $id with $callUUIDToGroupWith', name: 'CallKitService'); - } - break; - - case CallEventActionCallToggleAudioSession(:final isActive): - // Audio session toggled - if (kDebugMode) { - log('🔊 [CallKit] Audio session toggled: $isActive', name: 'CallKitService'); - } - break; - - case CallEventActionCallCustom(:final body): - // Custom event - if (kDebugMode) { - log('🔧 [CallKit] Custom event: $body', name: 'CallKitService'); - } + case CallEventActionCallToggleDmtf(): + case CallEventActionCallToggleGroup(): + case CallEventActionCallToggleAudioSession(): + case CallEventActionCallCustom(): break; - - default: - if (kDebugMode) { - log('ℹ️ [CallKit] Unhandled event type: ${event.eventName}', name: 'CallKitService'); - } } } @@ -202,24 +86,16 @@ class CallKitService { Map? extra, }) async { try { - if (kDebugMode) { - log('📞 [CallKit] Showing incoming call UI', name: 'CallKitService'); - log(' Caller: $callerName ($callerNumber)', name: 'CallKitService'); - log(' Video: $isVideo', name: 'CallKitService'); - log(' Call ID: $callId', name: 'CallKitService'); - } - _currentCallId = callId; - // Configure call parameters final params = CallKitParams( id: callId, nameCaller: callerName, appName: 'Atoms SA', avatar: callerAvatar, handle: callerNumber, - type: isVideo ? 1 : 0, // 0 = audio, 1 = video - duration: 30000, // 30 seconds timeout + type: isVideo ? 1 : 0, + duration: 30000, extra: extra ?? {}, headers: {'platform': 'flutter'}, android: AndroidParams( @@ -251,12 +127,7 @@ class CallKitService { ), ); - // Show the incoming call UI await FlutterCallkitIncoming.showCallkitIncoming(params); - - if (kDebugMode) { - log('✅ [CallKit] Incoming call UI displayed', name: 'CallKitService'); - } } catch (e, stackTrace) { log('❌ [CallKit] Error showing incoming call: $e', name: 'CallKitService', error: e, stackTrace: stackTrace); @@ -273,10 +144,6 @@ class CallKitService { required bool isVideo, }) async { try { - if (kDebugMode) { - log('📞 [CallKit] Starting outgoing call', name: 'CallKitService'); - } - _currentCallId = callId; final params = CallKitParams( @@ -296,10 +163,6 @@ class CallKitService { ); await FlutterCallkitIncoming.startCall(params); - - if (kDebugMode) { - log('✅ [CallKit] Outgoing call started', name: 'CallKitService'); - } } catch (e) { log('❌ [CallKit] Error starting outgoing call: $e', name: 'CallKitService'); } @@ -308,16 +171,7 @@ class CallKitService { /// Mark call as connected (when peer accepts) Future setCallConnected(String callId) async { try { - if (kDebugMode) { - log('✅ [CallKit] Marking call as connected: $callId', name: 'CallKitService'); - } - - // Update call to connected state - this will update the native UI await FlutterCallkitIncoming.setCallConnected(callId); - - if (kDebugMode) { - log('✅ [CallKit] Call marked as connected', name: 'CallKitService'); - } } catch (e) { log('❌ [CallKit] Error setting call connected: $e', name: 'CallKitService'); } @@ -326,19 +180,11 @@ class CallKitService { /// End the current call Future endCall(String callId) async { try { - if (kDebugMode) { - log('🔴 [CallKit] Ending call: $callId', name: 'CallKitService'); - } - await FlutterCallkitIncoming.endCall(callId); if (_currentCallId == callId) { _currentCallId = null; } - - if (kDebugMode) { - log('✅ [CallKit] Call ended', name: 'CallKitService'); - } } catch (e) { log('❌ [CallKit] Error ending call: $e', name: 'CallKitService'); } @@ -347,16 +193,8 @@ class CallKitService { /// End all active calls Future endAllCalls() async { try { - if (kDebugMode) { - log('🔴 [CallKit] Ending all calls', name: 'CallKitService'); - } - await FlutterCallkitIncoming.endAllCalls(); _currentCallId = null; - - if (kDebugMode) { - log('✅ [CallKit] All calls ended', name: 'CallKitService'); - } } catch (e) { log('❌ [CallKit] Error ending all calls: $e', name: 'CallKitService'); } @@ -366,9 +204,6 @@ class CallKitService { Future> getActiveCalls() async { try { final calls = await FlutterCallkitIncoming.activeCalls(); - if (kDebugMode) { - log('📋 [CallKit] Active calls: ${calls.length}', name: 'CallKitService'); - } return calls; } catch (e) { log('❌ [CallKit] Error getting active calls: $e', name: 'CallKitService'); @@ -379,26 +214,15 @@ class CallKitService { /// Dispose and cleanup Future dispose() async { try { - if (kDebugMode) { - log('🧹 [CallKit] Disposing service', name: 'CallKitService'); - } - - // Cancel event subscription await _eventSubscription?.cancel(); _eventSubscription = null; - // End all active calls await endAllCalls(); - // Clear callbacks onCallAccepted = null; onCallDeclined = null; onCallEnded = null; onCallTimeout = null; - - if (kDebugMode) { - log('✅ [CallKit] Service disposed', name: 'CallKitService'); - } } catch (e) { log('❌ [CallKit] Error disposing service: $e', name: 'CallKitService'); } diff --git a/lib/modules/cx_module/chat/call/services/webrtc_service.dart b/lib/modules/cx_module/chat/call/services/webrtc_service.dart index 45317fd8..725b2672 100644 --- a/lib/modules/cx_module/chat/call/services/webrtc_service.dart +++ b/lib/modules/cx_module/chat/call/services/webrtc_service.dart @@ -44,36 +44,24 @@ class WebRTCService { bool get isPeerConnectionInitialized => _peerConnection != null && _isFullyInitialized; /// Wait for WebRTC initialization to complete - /// Returns immediately if already initialized Future waitForInitialization() async { if (_isFullyInitialized) { - log('✅ [WebRTC] Already initialized', name: 'WebRTCService'); return; } if (_initializationCompleter != null) { - log('⏳ [WebRTC] Waiting for initialization to complete...', name: 'WebRTCService'); await _initializationCompleter!.future; - log('✅ [WebRTC] Initialization wait completed', name: 'WebRTCService'); - } else { - log('⚠️ [WebRTC] No initialization in progress', name: 'WebRTCService'); } } /// Enable test mode (use only Google STUN servers for debugging) static void enableTestMode() { _useTestMode = true; - if (kDebugMode) { - log('⚙️ [WebRTC] Test mode enabled', name: 'WebRTCService'); - } } /// Disable test mode (use backend TURN servers) static void disableTestMode() { _useTestMode = false; - if (kDebugMode) { - log('⚙️ [WebRTC] Test mode disabled', name: 'WebRTCService'); - } } // Configuration @@ -150,33 +138,17 @@ class WebRTCService { /// Initialize WebRTC for audio call Future initializeForAudioCall() async { - // CRITICAL FIX: Create completer and mark as not initialized _initializationCompleter = Completer(); _isFullyInitialized = false; try { - log('🔧 [WebRTC] Initialization Started', name: 'WebRTCService'); - _isVideoCall = false; - - // Get local audio stream try { - log('🎤 [WebRTC] Requesting microphone access...', name: 'WebRTCService'); _localStream = await navigator.mediaDevices.getUserMedia(_mediaConstraints); - - // CRITICAL: Verify audio tracks were captured final audioTracks = _localStream!.getAudioTracks(); - log('✅ [WebRTC] Local audio stream captured', name: 'WebRTCService'); - log(' Audio tracks count: ${audioTracks.length}', name: 'WebRTCService'); - for (var i = 0; i < audioTracks.length; i++) { final track = audioTracks[i]; - log(' Track $i: ${track.kind} - ID: ${track.id}', name: 'WebRTCService'); - log(' Track $i: enabled=${track.enabled}, muted=${track.muted}', name: 'WebRTCService'); - - // CRITICAL FIX: Ensure track is enabled if (!track.enabled) { - log('⚠️ [WebRTC] Track $i was disabled, enabling it', name: 'WebRTCService'); track.enabled = true; } } @@ -186,211 +158,167 @@ class WebRTCService { } } catch (e) { - log('❌ [WebRTC] Failed to get audio stream: $e', name: 'WebRTCService'); throw Exception('Microphone access denied or unavailable'); } - // Create peer connection - log('🔧 [WebRTC] Creating peer connection...', name: 'WebRTCService'); _peerConnection = await createPeerConnection(_configuration); if (_peerConnection == null) { throw Exception('Failed to create peer connection'); } - log('✅ [WebRTC] Peer connection created', name: 'WebRTCService'); - // Add local stream tracks to peer connection - log('📤 [WebRTC] Adding local tracks to peer connection...', name: 'WebRTCService'); final tracks = _localStream!.getTracks(); for (var i = 0; i < tracks.length; i++) { final track = tracks[i]; - log(' Adding track $i: ${track.kind} (ID: ${track.id})', name: 'WebRTCService'); - await _peerConnection!.addTrack(track, _localStream!); - - log(' ✅ Track $i added successfully', name: 'WebRTCService'); } - log('✅ [WebRTC] All local tracks added to peer connection', name: 'WebRTCService'); - // Setup peer connection event handlers _setupPeerConnectionListeners(); - // CRITICAL FIX: Set speakerphone OFF by default (use earpiece) - log('🔇 [WebRTC] Setting speakerphone OFF by default (earpiece mode)...', name: 'WebRTCService'); try { await Helper.setSpeakerphoneOn(false); - log('✅ [WebRTC] Speakerphone disabled (earpiece mode)', name: 'WebRTCService'); } catch (e) { - log('⚠️ [WebRTC] Failed to disable speakerphone: $e', name: 'WebRTCService'); + // Ignore speakerphone errors } - // CRITICAL FIX: Mark as fully initialized AFTER everything is ready _isFullyInitialized = true; if (!_initializationCompleter!.isCompleted) { _initializationCompleter!.complete(); } - log('✅ [WebRTC] Initialization Completed - PeerConnection Ready', name: 'WebRTCService'); } catch (e, stackTrace) { log('❌ [WebRTC] Initialization error: $e\nStack: $stackTrace', name: 'WebRTCService'); - // Complete with error if (_initializationCompleter != null && !_initializationCompleter!.isCompleted) { _initializationCompleter!.completeError(e, stackTrace); } _isFullyInitialized = false; - // Clean up any partial initialization await dispose(); rethrow; } } + /// Initialize WebRTC for video call /// Initialize WebRTC for video call Future initializeForVideoCall() async { - try { - if (kDebugMode) { - log('🔧 [WebRTC] Initializing video call', name: 'WebRTCService'); - } + _initializationCompleter = Completer(); + _isFullyInitialized = false; + try { _isVideoCall = true; // Initialize video renderers localRenderer = RTCVideoRenderer(); remoteRenderer = RTCVideoRenderer(); + await localRenderer!.initialize(); + await remoteRenderer!.initialize(); + + // Get camera + microphone stream first try { - await localRenderer!.initialize(); - await remoteRenderer!.initialize(); + _localStream = await navigator.mediaDevices.getUserMedia(_videoConstraints); } catch (e) { - log('❌ [WebRTC] Failed to initialize renderers: $e', name: 'WebRTCService'); - throw Exception('Failed to initialize video renderers'); + log( + '❌ [WebRTC] Failed to get video stream: $e', + name: 'WebRTCService', + ); + throw Exception('Camera or microphone access denied or unavailable'); } - // Create peer connection FIRST (before getting media) + if (_localStream == null) { + throw Exception('Local video stream is null'); + } + + // Attach local stream to renderer + localRenderer!.srcObject = _localStream; + + // Create peer connection final config = _useTestMode ? _testConfiguration : _configuration; + _peerConnection = await createPeerConnection(config); if (_peerConnection == null) { throw Exception('Failed to create peer connection'); } - // Setup peer connection listeners immediately + // Setup listeners _setupPeerConnectionListeners(); - // Get local audio + video stream - try { - _localStream = await navigator.mediaDevices.getUserMedia(_videoConstraints); - } catch (e) { - log('❌ [WebRTC] Failed to get video stream: $e', name: 'WebRTCService'); - throw Exception('Camera or microphone access denied or unavailable'); + // Add local tracks + final tracks = _localStream!.getTracks(); + + if (tracks.isEmpty) { + throw Exception('No media tracks found'); } - // Set local stream to renderer - if (localRenderer != null) { - localRenderer!.srcObject = _localStream; + for (final track in tracks) { + await _peerConnection!.addTrack( + track, + _localStream!, + ); } - // Add local stream tracks to peer connection - _localStream!.getTracks().forEach((track) { - _peerConnection!.addTrack(track, _localStream!); - }); + _isFullyInitialized = true; - if (kDebugMode) { - log('✅ [WebRTC] Video call initialized', name: 'WebRTCService'); + if (!_initializationCompleter!.isCompleted) { + _initializationCompleter!.complete(); } + + log( + '✅ [WebRTC] Video initialization completed', + name: 'WebRTCService', + ); + } catch (e, stackTrace) { - log('❌ [WebRTC] Video initialization error: $e\nStack: $stackTrace', name: 'WebRTCService'); - // Clean up any partial initialization + log( + '❌ [WebRTC] Video initialization error: $e\nStack: $stackTrace', + name: 'WebRTCService', + error: e, + stackTrace: stackTrace, + ); + + if (_initializationCompleter != null && + !_initializationCompleter!.isCompleted) { + _initializationCompleter!.completeError( + e, + stackTrace, + ); + } + + _isFullyInitialized = false; + await dispose(); + rethrow; } } /// Setup peer connection event listeners void _setupPeerConnectionListeners() { - log('🔧 [WebRTC] Setting up peer connection listeners...', name: 'WebRTCService'); - - // Handle ICE candidates _peerConnection!.onIceCandidate = (RTCIceCandidate candidate) { - log('🧊 [WebRTC] onIceCandidate triggered', name: 'WebRTCService'); - if (onIceCandidate != null) { - onIceCandidate!(candidate); - } else { - log('⚠️ [WebRTC] onIceCandidate callback is NULL!', name: 'WebRTCService'); - } + onIceCandidate?.call(candidate); }; - // Handle ICE gathering state changes _peerConnection!.onIceGatheringState = (RTCIceGatheringState state) { - log('📊 [WebRTC] ICE gathering state: ${state.toString()}', name: 'WebRTCService'); - if (state == RTCIceGatheringState.RTCIceGatheringStateComplete) { - log('✅ [WebRTC] ICE gathering completed', name: 'WebRTCService'); - } + // Ice gathering state changed }; - // Handle ICE connection state changes _peerConnection!.onIceConnectionState = (RTCIceConnectionState state) { - log('═══════════════════════════════════════════', name: 'WebRTCService'); - log('🔗 [WebRTC] onIceConnectionState TRIGGERED', name: 'WebRTCService'); - log(' State: ${state.toString()}', name: 'WebRTCService'); - log(' Callback is null?: ${onIceConnectionStateChange == null}', name: 'WebRTCService'); - log('═══════════════════════════════════════════', name: 'WebRTCService'); - - // Log critical failures - if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) { - log('❌ [WebRTC] ICE connection failed', name: 'WebRTCService'); - } - - if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) { - log('✅ [WebRTC] ICE connection CONNECTED!', name: 'WebRTCService'); - } - - if (onIceConnectionStateChange != null) { - log('📤 [WebRTC] Forwarding ICE state to callback...', name: 'WebRTCService'); - onIceConnectionStateChange!(state); - log('✅ [WebRTC] ICE state forwarded to callback', name: 'WebRTCService'); - } else { - log('❌ [WebRTC] CRITICAL: onIceConnectionStateChange callback is NULL!', name: 'WebRTCService'); - log(' This means CallManager did not set up callbacks properly!', name: 'WebRTCService'); - } + onIceConnectionStateChange?.call(state); }; - // Handle remote stream _peerConnection!.onTrack = (RTCTrackEvent event) { - log('═══════════════════════════════════════════', name: 'WebRTCService'); - log('📡 [WebRTC] onTrack event received!', name: 'WebRTCService'); - log(' Track kind: ${event.track.kind}', name: 'WebRTCService'); - log(' Track ID: ${event.track.id}', name: 'WebRTCService'); - log(' Track enabled: ${event.track.enabled}', name: 'WebRTCService'); - log(' Track muted: ${event.track.muted}', name: 'WebRTCService'); - // log(' Track readyState: ${event.track.readyState}', name: 'WebRTCService'); - log(' Streams count: ${event.streams.length}', name: 'WebRTCService'); - if (event.streams.isNotEmpty) { final stream = event.streams[0]; - log(' Stream ID: ${stream.id}', name: 'WebRTCService'); - log(' Audio tracks: ${stream.getAudioTracks().length}', name: 'WebRTCService'); - log(' Video tracks: ${stream.getVideoTracks().length}', name: 'WebRTCService'); - - // Log all tracks in the stream - final allTracks = stream.getTracks(); - for (var i = 0; i < allTracks.length; i++) { - final track = allTracks[i]; - log(' Track $i: ${track.kind} - enabled=${track.enabled}, muted=${track.muted}', name: 'WebRTCService'); - } - // If this is the first remote stream or a different stream, update it if (_remoteStream == null || _remoteStream!.id != stream.id) { - log('✅ [WebRTC] Setting new remote stream', name: 'WebRTCService'); _remoteStream = stream; - // For video calls, assign stream to renderer if (remoteRenderer != null && _isVideoCall) { try { remoteRenderer!.srcObject = _remoteStream; - // Retry assignment after delays to ensure it sticks Future.delayed(const Duration(milliseconds: 100), () { if (remoteRenderer != null && remoteRenderer!.srcObject == null && _remoteStream != null) { remoteRenderer!.srcObject = _remoteStream; @@ -403,40 +331,27 @@ class WebRTCService { } }); } catch (e) { - log('⚠️ [WebRTC] Failed to assign remote stream: $e', name: 'WebRTCService'); + // Ignore errors } } - // Notify callback - if (onRemoteStream != null) { - onRemoteStream!(_remoteStream!); - } - - log('✅ [WebRTC] Remote stream set and callback notified', name: 'WebRTCService'); + onRemoteStream?.call(_remoteStream!); } else { - log('ℹ️ [WebRTC] Additional track on existing stream', name: 'WebRTCService'); - // Additional track on existing stream if (remoteRenderer != null && _isVideoCall && remoteRenderer!.srcObject == null && _remoteStream != null) { try { remoteRenderer!.srcObject = _remoteStream; } catch (e) { - log('⚠️ [WebRTC] Failed to assign stream (additional track): $e', name: 'WebRTCService'); + // Ignore errors } } - if (onRemoteStream != null) { - onRemoteStream!(_remoteStream!); - } + onRemoteStream?.call(_remoteStream!); } - } else { - log('⚠️ [WebRTC] No streams in onTrack event!', name: 'WebRTCService'); } - log('═══════════════════════════════════════════', name: 'WebRTCService'); }; - // Handle connection state changes _peerConnection!.onConnectionState = (RTCPeerConnectionState state) { - if (kDebugMode || state == RTCPeerConnectionState.RTCPeerConnectionStateFailed) { + if (state == RTCPeerConnectionState.RTCPeerConnectionStateFailed) { log('🔌 [WebRTC] Connection state: ${state.toString()}', name: 'WebRTCService'); } }; @@ -449,22 +364,6 @@ class WebRTCService { throw Exception('Peer connection not initialized'); } - log('🔧 [WebRTC] Creating SDP offer...', name: 'WebRTCService'); - - // CRITICAL: Verify local tracks before creating offer - final senders = await _peerConnection!.getSenders(); - log('📊 [WebRTC] Peer connection has ${senders.length} sender(s)', name: 'WebRTCService'); - for (var i = 0; i < senders.length; i++) { - final sender = senders[i]; - final track = sender.track; - if (track != null) { - log(' Sender $i: ${track.kind} track (ID: ${track.id})', name: 'WebRTCService'); - log(' Sender $i: enabled=${track.enabled}, muted=${track.muted}', name: 'WebRTCService'); - } else { - log(' Sender $i: NO TRACK!', name: 'WebRTCService'); - } - } - final offer = await _peerConnection!.createOffer({ 'offerToReceiveAudio': true, 'offerToReceiveVideo': _isVideoCall, @@ -473,32 +372,6 @@ class WebRTCService { await _peerConnection!.setLocalDescription(offer); - if (kDebugMode) { - log('✅ [WebRTC] SDP offer created', name: 'WebRTCService'); - log(' Offer type: ${offer.type}', name: 'WebRTCService'); - log(' Offer SDP length: ${offer.sdp?.length ?? 0}', name: 'WebRTCService'); - - // CRITICAL: Log SDP to verify audio track is included - if (offer.sdp != null) { - final sdpLines = offer.sdp!.split('\n'); - final audioLines = sdpLines.where((line) => - line.contains('m=audio') || - line.contains('a=sendrecv') || - line.contains('a=sendonly') || - line.contains('a=recvonly') - ).toList(); - - log('📋 [WebRTC] SDP Audio Configuration:', name: 'WebRTCService'); - for (final line in audioLines) { - log(' $line', name: 'WebRTCService'); - } - - if (!offer.sdp!.contains('m=audio')) { - log('❌ [WebRTC] WARNING: No audio media line in SDP offer!', name: 'WebRTCService'); - } - } - } - return offer; } catch (e, stackTrace) { log('❌ [WebRTC] Error creating offer: $e\nStack: $stackTrace', name: 'WebRTCService'); @@ -513,55 +386,12 @@ class WebRTCService { throw Exception('Peer connection not initialized'); } - log('🔧 [WebRTC] Creating SDP answer...', name: 'WebRTCService'); - log(' Received offer SDP length: ${offerSdp.length}', name: 'WebRTCService'); - - // CRITICAL: Log received offer to verify it has audio - if (kDebugMode) { - final offerLines = offerSdp.split('\n'); - final audioLines = offerLines.where((line) => - line.contains('m=audio') || - line.contains('a=sendrecv') || - line.contains('a=sendonly') || - line.contains('a=recvonly') - ).toList(); - - log('📋 [WebRTC] Received Offer Audio Configuration:', name: 'WebRTCService'); - for (final line in audioLines) { - log(' $line', name: 'WebRTCService'); - } - - if (!offerSdp.contains('m=audio')) { - log('❌ [WebRTC] WARNING: No audio media line in received offer!', name: 'WebRTCService'); - } - } - - // CRITICAL: Verify local tracks before creating answer - final senders = await _peerConnection!.getSenders(); - log('📊 [WebRTC] Peer connection has ${senders.length} sender(s)', name: 'WebRTCService'); - for (var i = 0; i < senders.length; i++) { - final sender = senders[i]; - final track = sender.track; - if (track != null) { - log(' Sender $i: ${track.kind} track (ID: ${track.id})', name: 'WebRTCService'); - log(' Sender $i: enabled=${track.enabled}, muted=${track.muted}', name: 'WebRTCService'); - } else { - log(' Sender $i: NO TRACK!', name: 'WebRTCService'); - } - } - - // Set remote description (offer from caller) final offer = RTCSessionDescription(offerSdp, 'offer'); - log('🔧 [WebRTC] Setting remote description (offer)...', name: 'WebRTCService'); await _peerConnection!.setRemoteDescription(offer); _remoteDescriptionSet = true; - log('✅ [WebRTC] Remote description set', name: 'WebRTCService'); - // Flush queued ICE candidates await _flushIceCandidateQueue(); - // Create answer - log('🔧 [WebRTC] Creating answer...', name: 'WebRTCService'); final answer = await _peerConnection!.createAnswer({ 'offerToReceiveAudio': true, 'offerToReceiveVideo': _isVideoCall, @@ -569,32 +399,6 @@ class WebRTCService { await _peerConnection!.setLocalDescription(answer); - if (kDebugMode) { - log('✅ [WebRTC] SDP answer created', name: 'WebRTCService'); - log(' Answer type: ${answer.type}', name: 'WebRTCService'); - log(' Answer SDP length: ${answer.sdp?.length ?? 0}', name: 'WebRTCService'); - - // CRITICAL: Log SDP to verify audio track is included - if (answer.sdp != null) { - final sdpLines = answer.sdp!.split('\n'); - final audioLines = sdpLines.where((line) => - line.contains('m=audio') || - line.contains('a=sendrecv') || - line.contains('a=sendonly') || - line.contains('a=recvonly') - ).toList(); - - log('📋 [WebRTC] SDP Answer Audio Configuration:', name: 'WebRTCService'); - for (final line in audioLines) { - log(' $line', name: 'WebRTCService'); - } - - if (!answer.sdp!.contains('m=audio')) { - log('❌ [WebRTC] WARNING: No audio media line in SDP answer!', name: 'WebRTCService'); - } - } - } - return answer; } catch (e, stackTrace) { log('❌ [WebRTC] Error creating answer: $e\nStack: $stackTrace', name: 'WebRTCService'); @@ -613,12 +417,8 @@ class WebRTCService { await _peerConnection!.setRemoteDescription(answer); _remoteDescriptionSet = true; - // Flush queued ICE candidates await _flushIceCandidateQueue(); - if (kDebugMode) { - log('✅ [WebRTC] Remote answer set', name: 'WebRTCService'); - } } catch (e, stackTrace) { log('❌ [WebRTC] Error setting remote answer: $e\nStack: $stackTrace', name: 'WebRTCService'); rethrow; @@ -661,10 +461,6 @@ class WebRTCService { return; } - if (kDebugMode) { - log('🔄 [WebRTC] Flushing ${_iceCandidateQueue.length} ICE candidates', name: 'WebRTCService'); - } - for (final candidate in _iceCandidateQueue) { try { await _peerConnection!.addCandidate(candidate); @@ -703,9 +499,6 @@ class WebRTCService { if (videoTracks.isNotEmpty) { try { await Helper.switchCamera(videoTracks.first); - if (kDebugMode) { - log('✅ [WebRTC] Camera switched', name: 'WebRTCService'); - } } catch (e) { log('❌ [WebRTC] Error switching camera: $e', name: 'WebRTCService'); } @@ -739,9 +532,6 @@ class WebRTCService { if (_remoteStream != null && remoteRenderer != null && _isVideoCall) { if (remoteRenderer!.srcObject == null) { remoteRenderer!.srcObject = _remoteStream; - if (kDebugMode) { - log('✅ [WebRTC] Remote renderer refreshed', name: 'WebRTCService'); - } } } } @@ -761,10 +551,6 @@ class WebRTCService { await _peerConnection!.setLocalDescription(offer); - if (kDebugMode) { - log('✅ [WebRTC] ICE restart initiated', name: 'WebRTCService'); - } - return offer; } catch (e) { log('❌ [WebRTC] ICE restart failed: $e', name: 'WebRTCService'); @@ -785,7 +571,6 @@ class WebRTCService { /// Dispose and cleanup Future dispose() async { try { - // Stop local stream tracks if (_localStream != null) { _localStream!.getTracks().forEach((track) { track.stop(); @@ -794,7 +579,6 @@ class WebRTCService { _localStream = null; } - // Stop remote stream tracks if (_remoteStream != null) { _remoteStream!.getTracks().forEach((track) { track.stop(); @@ -803,7 +587,6 @@ class WebRTCService { _remoteStream = null; } - // Dispose renderers if (localRenderer != null) { await localRenderer!.dispose(); localRenderer = null; @@ -814,20 +597,15 @@ class WebRTCService { remoteRenderer = null; } - // Close peer connection if (_peerConnection != null) { await _peerConnection!.close(); await _peerConnection!.dispose(); _peerConnection = null; } - // Clear ICE candidate queue _iceCandidateQueue.clear(); _remoteDescriptionSet = false; - if (kDebugMode) { - log('✅ [WebRTC] Service disposed', name: 'WebRTCService'); - } } catch (e) { log('⚠️ [WebRTC] Error during dispose: $e', name: 'WebRTCService'); } diff --git a/lib/modules/cx_module/chat/call/utils/permission_helper.dart b/lib/modules/cx_module/chat/call/utils/permission_helper.dart new file mode 100644 index 00000000..00ad669c --- /dev/null +++ b/lib/modules/cx_module/chat/call/utils/permission_helper.dart @@ -0,0 +1,264 @@ +import 'dart:async'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:test_sa/modules/cx_module/chat/call/call_error_handler.dart'; + +/// Production-ready permission helper for audio/video calls +/// Handles all permission requests with proper error handling and user feedback +class CallPermissionHelper { + CallPermissionHelper._(); + + /// Check and request microphone permission for audio calls + /// Returns true if permission is granted, false otherwise + static Future requestMicrophonePermission(BuildContext context) async { + try { + log('🎤 [PERMISSION] Checking microphone permission...', name: 'CallPermissionHelper'); + + // Check current status + final status = await Permission.microphone.status; + log(' Current status: ${status.name}', name: 'CallPermissionHelper'); + + if (status.isGranted) { + log('✅ [PERMISSION] Microphone already granted', name: 'CallPermissionHelper'); + return true; + } + + // If denied permanently, show settings dialog + if (status.isPermanentlyDenied) { + log('⚠️ [PERMISSION] Microphone permanently denied - showing settings dialog', name: 'CallPermissionHelper'); + if (context.mounted) { + await _showPermissionDeniedDialog( + context, + 'Microphone Permission Required', + 'This app needs microphone access to make calls. Please enable it in settings.', + Permission.microphone, + ); + } + return false; + } + + // Request permission + log('📤 [PERMISSION] Requesting microphone permission...', name: 'CallPermissionHelper'); + final result = await Permission.microphone.request(); + log(' Result: ${result.name}', name: 'CallPermissionHelper'); + + if (result.isGranted) { + log('✅ [PERMISSION] Microphone permission granted', name: 'CallPermissionHelper'); + return true; + } + + if (result.isPermanentlyDenied) { + log('❌ [PERMISSION] Microphone permanently denied', name: 'CallPermissionHelper'); + if (context.mounted) { + await _showPermissionDeniedDialog( + context, + 'Microphone Permission Required', + 'This app needs microphone access to make calls. Please enable it in settings.', + Permission.microphone, + ); + } + return false; + } + + // User denied + log('❌ [PERMISSION] Microphone permission denied by user', name: 'CallPermissionHelper'); + if (context.mounted) { + CallErrorHandler.showMicrophonePermissionDenied(context); + } + return false; + } catch (e, stackTrace) { + log('❌ [PERMISSION] Error requesting microphone permission: $e\nStack: $stackTrace', + name: 'CallPermissionHelper'); + if (context.mounted) { + CallErrorHandler.showGenericError(context, 'Failed to request microphone permission'); + } + return false; + } + } + + /// Check and request camera permission for video calls + /// Returns true if permission is granted, false otherwise + static Future requestCameraPermission(BuildContext context) async { + try { + log('📹 [PERMISSION] Checking camera permission...', name: 'CallPermissionHelper'); + + // Check current status + final status = await Permission.camera.status; + log(' Current status: ${status.name}', name: 'CallPermissionHelper'); + + if (status.isGranted) { + log('✅ [PERMISSION] Camera already granted', name: 'CallPermissionHelper'); + return true; + } + + // If denied permanently, show settings dialog + if (status.isPermanentlyDenied) { + log('⚠️ [PERMISSION] Camera permanently denied - showing settings dialog', name: 'CallPermissionHelper'); + if (context.mounted) { + await _showPermissionDeniedDialog( + context, + 'Camera Permission Required', + 'This app needs camera access for video calls. Please enable it in settings.', + Permission.camera, + ); + } + return false; + } + + // Request permission + log('📤 [PERMISSION] Requesting camera permission...', name: 'CallPermissionHelper'); + final result = await Permission.camera.request(); + log(' Result: ${result.name}', name: 'CallPermissionHelper'); + + if (result.isGranted) { + log('✅ [PERMISSION] Camera permission granted', name: 'CallPermissionHelper'); + return true; + } + + if (result.isPermanentlyDenied) { + log('❌ [PERMISSION] Camera permanently denied', name: 'CallPermissionHelper'); + if (context.mounted) { + await _showPermissionDeniedDialog( + context, + 'Camera Permission Required', + 'This app needs camera access for video calls. Please enable it in settings.', + Permission.camera, + ); + } + return false; + } + + // User denied + log('❌ [PERMISSION] Camera permission denied by user', name: 'CallPermissionHelper'); + if (context.mounted) { + CallErrorHandler.showCameraPermissionDenied(context); + } + return false; + } catch (e, stackTrace) { + log('❌ [PERMISSION] Error requesting camera permission: $e\nStack: $stackTrace', + name: 'CallPermissionHelper'); + if (context.mounted) { + CallErrorHandler.showGenericError(context, 'Failed to request camera permission'); + } + return false; + } + } + + /// Request all permissions required for audio calls (microphone only) + /// Returns true if all permissions are granted, false otherwise + static Future requestAudioCallPermissions(BuildContext context) async { + log('═══════════════════════════════════════════', name: 'CallPermissionHelper'); + log('🎵 [PERMISSION] Requesting audio call permissions...', name: 'CallPermissionHelper'); + + final micGranted = await requestMicrophonePermission(context); + + if (!micGranted) { + log('❌ [PERMISSION] Audio call permissions denied', name: 'CallPermissionHelper'); + log('═══════════════════════════════════════════', name: 'CallPermissionHelper'); + return false; + } + + log('✅ [PERMISSION] All audio call permissions granted', name: 'CallPermissionHelper'); + log('═══════════════════════════════════════════', name: 'CallPermissionHelper'); + return true; + } + + /// Request all permissions required for video calls (microphone + camera) + /// Returns true if all permissions are granted, false otherwise + static Future requestVideoCallPermissions(BuildContext context) async { + log('═══════════════════════════════════════════', name: 'CallPermissionHelper'); + log('📹 [PERMISSION] Requesting video call permissions...', name: 'CallPermissionHelper'); + + // Request both permissions + final micGranted = await requestMicrophonePermission(context); + if (!micGranted) { + log('❌ [PERMISSION] Video call permissions denied (microphone)', name: 'CallPermissionHelper'); + log('═══════════════════════════════════════════', name: 'CallPermissionHelper'); + return false; + } + + final cameraGranted = await requestCameraPermission(context); + if (!cameraGranted) { + log('❌ [PERMISSION] Video call permissions denied (camera)', name: 'CallPermissionHelper'); + log('═══════════════════════════════════════════', name: 'CallPermissionHelper'); + return false; + } + + log('✅ [PERMISSION] All video call permissions granted', name: 'CallPermissionHelper'); + log('═══════════════════════════════════════════', name: 'CallPermissionHelper'); + return true; + } + + /// Check if microphone permission is granted (without requesting) + static Future isMicrophoneGranted() async { + final status = await Permission.microphone.status; + return status.isGranted; + } + + /// Check if camera permission is granted (without requesting) + static Future isCameraGranted() async { + final status = await Permission.camera.status; + return status.isGranted; + } + + /// Show dialog when permission is permanently denied, with option to open settings + static Future _showPermissionDeniedDialog( + BuildContext context, + String title, + String message, + Permission permission, + ) async { + if (!context.mounted) return; + + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(); + await openAppSettings(); + }, + child: const Text('Open Settings'), + ), + ], + ); + }, + ); + } + + /// Handle permission denial during an active call + /// Shows error and optionally ends the call + static Future handlePermissionDeniedDuringCall( + BuildContext context, + String permissionName, { + required Function() onEndCall, + int delaySeconds = 2, + }) async { + if (!context.mounted) return; + + log('❌ [PERMISSION] $permissionName denied during active call', name: 'CallPermissionHelper'); + log(' Showing error and ending call in ${delaySeconds}s...', name: 'CallPermissionHelper'); + + CallErrorHandler.showGenericError( + context, + '$permissionName permission is required for this call.', + ); + + // Give user time to see the error + await Future.delayed(Duration(seconds: delaySeconds)); + + if (context.mounted) { + onEndCall(); + } + } +} diff --git a/lib/modules/cx_module/chat/call/video_call_page.dart b/lib/modules/cx_module/chat/call/video_call_page.dart index 823e4bc4..bbc9c309 100644 --- a/lib/modules/cx_module/chat/call/video_call_page.dart +++ b/lib/modules/cx_module/chat/call/video_call_page.dart @@ -3,13 +3,14 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:test_sa/core/di/service_locator.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart'; import 'package:test_sa/modules/cx_module/chat/model/call_session.dart'; +import 'package:test_sa/modules/cx_module/chat/call/utils/permission_helper.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; -import 'package:test_sa/new_views/app_style/app_themes.dart'; class VideoCallPage extends StatefulWidget { const VideoCallPage({Key? key}) : super(key: key); @@ -26,13 +27,38 @@ class _VideoCallPageState extends State { @override void initState() { super.initState(); - _callManager = CallManager(); + _callManager = getIt(); + // _callManager = CallManager(); // Listen for call end _callManager.addListener(_checkCallStatus); // Start periodic check to ensure remote stream gets assigned _startStreamCheckTimer(); + + // CRITICAL: Check permissions after first frame + // Permissions must be requested in UI layer, not service layer + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkPermissions(); + }); + } + + /// Request video call permissions using production-ready helper + Future _checkPermissions() async { + if (!mounted) return; + + // Request all video call permissions (microphone + camera) + final granted = await CallPermissionHelper.requestVideoCallPermissions(context); + + if (!granted && mounted) { + // Permission denied - end the call gracefully + await CallPermissionHelper.handlePermissionDeniedDuringCall( + context, + 'Camera or Microphone', + onEndCall: () => _callManager.declineCall('permission_denied'), + delaySeconds: 2, + ); + } } void _startStreamCheckTimer() { diff --git a/lib/modules/cx_module/chat/chat_page.dart b/lib/modules/cx_module/chat/chat_page.dart index fa19ee03..047b8ff1 100644 --- a/lib/modules/cx_module/chat/chat_page.dart +++ b/lib/modules/cx_module/chat/chat_page.dart @@ -190,8 +190,8 @@ class _ChatPageState extends State { @override void dispose() { - final chatProvider = Provider.of(context, listen: false); - chatProvider.chatHubConnection?.stop(); + // final chatProvider = Provider.of(context, listen: false); + // chatProvider.chatHubConnection?.stop(); playerController.dispose(); recorderController.dispose(); super.dispose(); diff --git a/lib/modules/cx_module/chat/chat_provider.dart b/lib/modules/cx_module/chat/chat_provider.dart index 02da8ec9..f72aaa9f 100644 --- a/lib/modules/cx_module/chat/chat_provider.dart +++ b/lib/modules/cx_module/chat/chat_provider.dart @@ -142,8 +142,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { // Just clear ChatProvider's local state // The SignalRService singleton will manage the connection lifecycle - log('🔄 [ChatProvider] Resetting chat state (preserving SignalR connection)', name: 'ChatProvider'); - + _chatHubConnection = null; chatLoginTokenLoading = false; @@ -199,25 +198,14 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { notifyListeners(); } try { - log('i am called ...'); chatLoginResponse = await ChatApiClient().getChatLoginToken(moduleId, requestId, title, myId, assigneeEmployeeNumber); - log('✅ Got chatLoginResponse'); chatParticipantModel = await ChatApiClient().loadParticipants(moduleId, requestId, assigneeEmployeeNumber); log('✅ Got chatParticipantModel: ${chatParticipantModel?.toJson()}'); - - // Log available participants for debugging - log('🔍 Looking for myId: $myId'); - log('🔍 Looking for assigneeEmployeeNumber: $assigneeEmployeeNumber'); - - // CRITICAL FIX: Match participants by userId, not employeeNumber - // The employeeNumber field contains userName, not actual employee number - // Use case-insensitive matching and handle not found gracefully try { sender = chatParticipantModel?.participants?.firstWhere( (participant) => participant.userId?.toLowerCase() == myId.toLowerCase() ); - log('✅ sender found: userId=${sender?.userId}, userName=${sender?.userName}, employeeNumber=${sender?.employeeNumber}'); } catch (e) { log('⚠️ Sender NOT FOUND for myId: $myId. Error: $e'); sender = null; @@ -233,16 +221,14 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { recipient = null; } - // CRITICAL: Cache credentials in CallCoordinator for background calls + /// CRITICAL:need to remove this after testing Cache credentials in CallCoordinator for background calls if (chatLoginResponse != null && chatParticipantModel != null && sender != null) { - log('💾 [ChatProvider] Caching credentials for future incoming calls...', name: 'ChatProvider'); final coordinator = CallCoordinator(); coordinator.cacheCredentials( loginResponse: chatLoginResponse!, participants: chatParticipantModel!, myEmployeeNumber: myId, ); - log('✅ [ChatProvider] Credentials cached successfully', name: 'ChatProvider'); } } catch (ex) { @@ -374,23 +360,9 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Prevents connection leaks if initialization fails Future buildHubConnection(String conversationID) async { try { - log('═══════════════════════════════════════════', name: 'ChatProvider'); - log('🔌 [ChatProvider] buildHubConnection called', name: 'ChatProvider'); log('💬 Conversation ID: $conversationID', name: 'ChatProvider'); - - // CRITICAL FIX: Use the SINGLETON SignalR service from DI - // DO NOT create a new connection - reuse the existing one final signalRService = _signalRService; - - log('🔍 [ChatProvider] SignalR service instance: ${signalRService.hashCode}', name: 'ChatProvider'); - log(' Is connected: ${signalRService.isConnected}', name: 'ChatProvider'); - log(' Connection state: ${signalRService.connectionState}', name: 'ChatProvider'); - - // CRITICAL FIX: Only initialize if not connected at all - // If already connected (from CallManager), just join the new conversation if (!signalRService.isConnected) { - log('🔌 [ChatProvider] SignalR not connected, initializing...', name: 'ChatProvider'); - final connected = await signalRService.initialize( userId: chatLoginResponse!.userId.toString(), authToken: chatLoginResponse!.token ?? '', @@ -400,23 +372,11 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { if (!connected) { throw Exception('Failed to initialize SignalR connection'); } - - log('✅ [ChatProvider] SignalR initialized', name: 'ChatProvider'); - log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider'); } else { - log('✅ [ChatProvider] SignalR already connected - reusing existing connection', name: 'ChatProvider'); - log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider'); - - // CRITICAL FIX: Just update the conversation context - // Do NOT call initialize() again - it will dispose the connection! - // The SignalRService.initialize() method already handles conversation switching if (conversationID.isNotEmpty) { try { await signalRService.invoke("JoinConversation", args: [conversationID]); - log('✅ [ChatProvider] Joined conversation: $conversationID', name: 'ChatProvider'); } catch (e) { - log('⚠️ [ChatProvider] Error joining conversation (will retry): $e', name: 'ChatProvider'); - // If join fails, try to reconnect with the conversation await signalRService.initialize( userId: chatLoginResponse!.userId.toString(), authToken: chatLoginResponse!.token ?? '', @@ -428,23 +388,12 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { // CRITICAL FIX: Reference the singleton connection, don't create a new one chatHubConnection = signalRService.hubConnection; - - // Log user details - log('👤 User ID: ${chatLoginResponse!.userId}', name: 'ChatProvider'); - log('📞 My Employee Number: ${sender?.employeeNumber ?? "NOT SET"}', name: 'ChatProvider'); - log('═══════════════════════════════════════════', name: 'ChatProvider'); - - // Register chat event handlers _registerChatEventHandlers(); } catch (e, stackTrace) { - log('❌ [ChatProvider] Error building SignalR connection: $e', - name: 'ChatProvider', error: e, stackTrace: stackTrace); if (kDebugMode) { print('⚠️ Error building SignalR connection: $e'); } - // CRITICAL FIX: Don't dispose on error - just log and continue - // The connection might still be usable for calls rethrow; } } @@ -455,10 +404,6 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { log('⚠️ Cannot register event handlers - connection is null', name: 'ChatProvider'); return; } - - log('🔧 [EVENT HANDLERS] Registering chat event handlers...', name: 'ChatProvider'); - - // Register chat event handlers via SignalRService _signalRService.on("ReceiveMessage", onMsgReceived1); _signalRService.on("OnMessageReceivedAsync", onMsgReceived); _signalRService.on("OnSubmitChatAsync", onSubmitChatAsync); @@ -471,9 +416,6 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { log('✅ [EVENT HANDLERS] Chat event handlers registered successfully', name: 'ChatProvider'); } - // ==================== CALL INFRASTRUCTURE ==================== - // All call operations now delegate to CallManager - /// Start a new audio or video call - delegates to CallManager Future startCall(Participants recipient, CallType callType) async { log('📞 [ChatProvider] startCall() - delegating to CallManager', name: 'ChatProvider'); @@ -487,12 +429,6 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { return; } - // CRITICAL: Ensure CallManager is properly initialized before starting call - log('🔧 [ChatProvider] Initializing CallManager before starting call...', name: 'ChatProvider'); - log(' User ID: ${chatLoginResponse?.userId}', name: 'ChatProvider'); - log(' Employee Number: ${sender?.employeeNumber}', name: 'ChatProvider'); - log(' Conversation ID: ${chatParticipantModel?.id}', name: 'ChatProvider'); - try { await CallManager().initialize( userId: chatLoginResponse!.userId.toString(), @@ -520,16 +456,9 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { // Delegate to CallManager log('📤 [ChatProvider] Delegating to CallManager.startCall()', name: 'ChatProvider'); - log(' Recipient data:', name: 'ChatProvider'); - log(' - userId: ${recipient.userId}', name: 'ChatProvider'); - log(' - userName: ${recipient.userName}', name: 'ChatProvider'); - log(' - employeeNumber: ${recipient.employeeNumber}', name: 'ChatProvider'); - log(' Type: ${callType.name}', name: 'ChatProvider'); // Use employeeNumber from ChatParticipantModel for the call final targetEmployeeNumber = recipient.employeeNumber ?? ''; - - log(' ✅ Calling with employeeNumber: $targetEmployeeNumber', name: 'ChatProvider'); await CallManager().startCall( peerId: targetEmployeeNumber, @@ -582,17 +511,9 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { notifyListeners(); } - /// Register call event handlers - kept for backwards compatibility - void _registerCallHandlers() { - // Call handlers are now managed by CallManager - // This method is kept for backwards compatibility but does nothing - log('ℹ️ [ChatProvider] Call handlers are managed by CallManager', name: 'ChatProvider'); - } /// Handle call history update void _onCallHistoryUpdated(List? args) async { - log('📞 [CALL HISTORY] OnCallHistoryUpdated received', name: 'ChatProvider'); - try { if (sender != null && recipient != null) { userChatHistory = await ChatApiClient().loadChatHistory( diff --git a/lib/modules/cx_module/chat/services/call_coordinator.dart b/lib/modules/cx_module/chat/services/call_coordinator.dart index 15f77fca..3c54037d 100644 --- a/lib/modules/cx_module/chat/services/call_coordinator.dart +++ b/lib/modules/cx_module/chat/services/call_coordinator.dart @@ -29,39 +29,18 @@ class CallCoordinator { required String moduleId, required String referenceId, }) async { - log('═══════════════════════════════════════════', name: 'CallCoordinator'); - log('📞 [OUTGOING] Initializing communication for outgoing call', name: 'CallCoordinator'); - try { - // Step 1: Ensure chat login credentials exist - log('🔐 [OUTGOING] Step 1: Checking chat login credentials...', name: 'CallCoordinator'); if (chatProvider.chatLoginResponse == null) { - log('❌ [OUTGOING] Chat login credentials not found', name: 'CallCoordinator'); - log(' User must open chat screen first to initialize credentials', name: 'CallCoordinator'); return false; } - log('✅ [OUTGOING] Chat login credentials available', name: 'CallCoordinator'); - // Step 2: Ensure participants loaded - log('👥 [OUTGOING] Step 2: Checking participants...', name: 'CallCoordinator'); if (chatProvider.sender == null || chatProvider.recipient == null) { - log('❌ [OUTGOING] Participants not loaded (sender: ${chatProvider.sender != null}, recipient: ${chatProvider.recipient != null})', name: 'CallCoordinator'); return false; } - log('✅ [OUTGOING] Participants loaded', name: 'CallCoordinator'); - // Step 3: Initialize CallManager with credentials - log('🔧 [OUTGOING] Step 3: Initializing CallManager...', name: 'CallCoordinator'); final userId = chatProvider.chatLoginResponse!.userId.toString(); final authToken = chatProvider.chatLoginResponse!.token ?? ''; - - // Use employeeNumber from sender participant in ChatParticipantModel final employeeNumber = chatProvider.sender?.employeeNumber; - - log(' Sender userId: ${chatProvider.sender?.userId}', name: 'CallCoordinator'); - log(' Sender userName: ${chatProvider.sender?.userName}', name: 'CallCoordinator'); - log(' Sender employeeNumber: ${chatProvider.sender?.employeeNumber}', name: 'CallCoordinator'); - log(' Using employeeNumber for CallUserAsync: $employeeNumber', name: 'CallCoordinator'); await _callManager.initialize( userId: userId, @@ -72,31 +51,22 @@ class CallCoordinator { employeeNumber: employeeNumber, ); - log('✅ [OUTGOING] Communication initialized successfully', name: 'CallCoordinator'); - log('═══════════════════════════════════════════', name: 'CallCoordinator'); return true; } catch (e, stackTrace) { - log('❌ [OUTGOING] Initialization failed: $e', - name: 'CallCoordinator', error: e, stackTrace: stackTrace); - log('═══════════════════════════════════════════', name: 'CallCoordinator'); + log('[CallCoordinator] ❌ initializeForOutgoingCall failed: $e', + error: e, stackTrace: stackTrace); return false; } } + Future initializeForIncomingCall({ required String callerEmployeeNumber, String? conversationId, String? moduleId, String? referenceId, }) async { - log('═══════════════════════════════════════════', name: 'CallCoordinator'); - log('📞 [INCOMING] Initializing communication for incoming call', name: 'CallCoordinator'); - log(' Caller: $callerEmployeeNumber', name: 'CallCoordinator'); - log(' Module ID: $moduleId', name: 'CallCoordinator'); - - // Prevent concurrent initialization if (_isInitializing) { - log('⏳ [INCOMING] Already initializing, waiting...', name: 'CallCoordinator'); await Future.delayed(const Duration(milliseconds: 500)); return _signalRService.isConnected; } @@ -108,44 +78,21 @@ class CallCoordinator { String? authToken; String? myEmployeeNumber; - // STEP 1: Try to read credentials from SharedPreferences (CRITICAL for background/terminated) - log('🔐 [INCOMING] Step 1: Reading credentials from SharedPreferences...', name: 'CallCoordinator'); final storedCredentials = await AuthStorage.getCredentials(); if (storedCredentials != null) { - // SUCCESS: Found stored credentials userId = storedCredentials.userId; authToken = storedCredentials.accessToken; myEmployeeNumber = storedCredentials.employeeNumber; - - log('✅ [INCOMING] Using stored credentials from SharedPreferences', name: 'CallCoordinator'); - log(' User ID: $userId', name: 'CallCoordinator'); - log(' Employee Number: ${myEmployeeNumber ?? "none"}', name: 'CallCoordinator'); - log(' Token length: ${authToken.length} chars', name: 'CallCoordinator'); + } else if (_cachedLoginResponse != null && _cachedMyEmployeeNumber != null) { + userId = _cachedLoginResponse!.userId.toString(); + authToken = _cachedLoginResponse!.token ?? ''; + myEmployeeNumber = _cachedMyEmployeeNumber; } else { - // Fall back to memory cache (for foreground calls) - log('⚠️ [INCOMING] No stored credentials in SharedPreferences', name: 'CallCoordinator'); - log(' Checking memory cache...', name: 'CallCoordinator'); - - if (_cachedLoginResponse != null && _cachedMyEmployeeNumber != null) { - userId = _cachedLoginResponse!.userId.toString(); - authToken = _cachedLoginResponse!.token ?? ''; - myEmployeeNumber = _cachedMyEmployeeNumber; - log('✅ [INCOMING] Using cached credentials from memory', name: 'CallCoordinator'); - log(' User ID: $userId', name: 'CallCoordinator'); - log(' Employee Number: $myEmployeeNumber', name: 'CallCoordinator'); - } else { - log('❌ [INCOMING] No credentials available anywhere', name: 'CallCoordinator'); - log(' Cannot initialize SignalR without credentials', name: 'CallCoordinator'); - log(' User must be logged in first', name: 'CallCoordinator'); - log('═══════════════════════════════════════════', name: 'CallCoordinator'); - return false; - } + return false; } - // STEP 2: Initialize SignalR with stored credentials (if not already connected) if (!_signalRService.isConnected) { - log('🔌 [INCOMING] Step 2: Initializing SignalR connection...', name: 'CallCoordinator'); final signalRConnected = await _signalRService.initialize( userId: userId!, authToken: authToken!, @@ -153,41 +100,24 @@ class CallCoordinator { ); if (!signalRConnected) { - log('❌ [INCOMING] SignalR connection failed', name: 'CallCoordinator'); - log('═══════════════════════════════════════════', name: 'CallCoordinator'); return false; } - - log('✅ [INCOMING] SignalR connected successfully', name: 'CallCoordinator'); - log(' Connection ID: ${_signalRService.connectionId}', name: 'CallCoordinator'); - } else { - log('✅ [INCOMING] SignalR already connected', name: 'CallCoordinator'); - log(' Connection ID: ${_signalRService.connectionId}', name: 'CallCoordinator'); } - // STEP 3: Initialize CallManager - log('🔧 [INCOMING] Step 3: Initializing CallManager...', name: 'CallCoordinator'); await _callManager.initialize( userId: userId!, authToken: authToken!, conversationId: conversationId, - moduleId: moduleId ?? '1002', // Default to ATOMS + moduleId: moduleId ?? '1002', referenceId: referenceId ?? '0', employeeNumber: myEmployeeNumber, ); - log('✅ [INCOMING] CallManager initialized successfully', name: 'CallCoordinator'); - log('✅ [INCOMING] Communication initialization complete', name: 'CallCoordinator'); - log(' ✅ SignalR: Connected', name: 'CallCoordinator'); - log(' ✅ CallManager: Ready', name: 'CallCoordinator'); - log(' ✅ Ready to receive incoming call', name: 'CallCoordinator'); - log('═══════════════════════════════════════════', name: 'CallCoordinator'); return true; } catch (e, stackTrace) { - log('❌ [INCOMING] Initialization failed: $e', - name: 'CallCoordinator', error: e, stackTrace: stackTrace); - log('═══════════════════════════════════════════', name: 'CallCoordinator'); + log('[CallCoordinator] ❌ initializeForIncomingCall failed: $e', + error: e, stackTrace: stackTrace); return false; } finally { _isInitializing = false; @@ -201,37 +131,22 @@ class CallCoordinator { required ChatParticipantModel participants, required String myEmployeeNumber, }) { - log('💾 [COORDINATOR] Caching credentials for future incoming calls', name: 'CallCoordinator'); - log(' User ID: ${loginResponse.userId}', name: 'CallCoordinator'); - log(' Employee Number: $myEmployeeNumber', name: 'CallCoordinator'); - _cachedLoginResponse = loginResponse; _cachedParticipants = participants; _cachedMyEmployeeNumber = myEmployeeNumber; - - log('✅ [COORDINATOR] Credentials cached successfully', name: 'CallCoordinator'); } /// Ensure communication is ready /// Used before any call operation bool isCommunicationReady() { - final signalRReady = _signalRService.isConnected; - - log('🔍 [COORDINATOR] Communication readiness check:', name: 'CallCoordinator'); - log(' SignalR: ${signalRReady ? "✅ Connected" : "❌ Not connected"}', name: 'CallCoordinator'); - log(' CallManager: ✅ Ready', name: 'CallCoordinator'); - - return signalRReady; + return _signalRService.isConnected; } /// Reset communication (for logout) Future reset() async { - log('🔄 [COORDINATOR] Resetting communication...', name: 'CallCoordinator'); _isInitializing = false; _cachedLoginResponse = null; _cachedParticipants = null; _cachedMyEmployeeNumber = null; - // Services will be reset via service_locator.resetServices() - log('✅ [COORDINATOR] Reset complete', name: 'CallCoordinator'); } } diff --git a/lib/modules/cx_module/chat/services/call_manager.dart b/lib/modules/cx_module/chat/services/call_manager.dart index 74efa141..a9d94582 100644 --- a/lib/modules/cx_module/chat/services/call_manager.dart +++ b/lib/modules/cx_module/chat/services/call_manager.dart @@ -2,7 +2,6 @@ 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'; @@ -21,13 +20,14 @@ import 'package:test_sa/modules/cx_module/chat/services/call_coordinator.dart'; class CallManager extends ChangeNotifier { static final CallManager _instance = CallManager._internal(); + factory CallManager() => _instance; - CallManager._internal() { - log('🏗️ [CallManager] Singleton instance created (hashCode: $hashCode)', name: 'CallManager'); - } + + CallManager._internal(); // Services - Retrieved from DI SignalRService get _signalRService => getIt(); + WebRTCService get _webrtcService => getIt(); final CallKitService _callKitService = CallKitService(); @@ -81,13 +81,21 @@ class CallManager extends ChangeNotifier { // Getters CallSession? get currentCall => _currentCall; + CallStatus get callStatus => _callStatus; + Duration get callDuration => _callDuration; + bool get isMuted => _isMuted; + bool get isSpeakerOn => _isSpeakerOn; + bool get isCameraOn => _isCameraOn; + bool get isPeerMuted => _isPeerMuted; + bool get isPeerCameraOn => _isPeerCameraOn; + bool get isCallInProgress => _callStatus != CallStatus.idle; // CRITICAL: Public getter for navigation pending flag @@ -103,39 +111,23 @@ class CallManager extends ChangeNotifier { String? employeeNumber, }) async { try { - log('🔧 [CallManager] Initializing...', name: 'CallManager'); - // CRITICAL FIX: Cache credentials for background call handling _cachedUserId = userId; _cachedAuthToken = authToken; _cachedEmployeeNumber = employeeNumber; // CRITICAL FIX: Don't re-initialize if already initialized with same USER credentials - // conversationId can change (different chats) - that's OK, just update it - // Only re-initialize if USER changes (userId, authToken, employeeNumber) - // Compare employee numbers case-insitively (backend sometimes changes case) - if (_userId == userId && - _authToken == authToken && - _myEmployeeNumber?.toLowerCase() == employeeNumber?.toLowerCase() && - _handlersRegistered) { - + if (_userId == userId && _authToken == authToken && _myEmployeeNumber?.toLowerCase() == employeeNumber?.toLowerCase() && _handlersRegistered) { // User is the same, just update conversation context without reconnecting final conversationChanged = _conversationId != conversationId; final moduleChanged = _moduleId != moduleId; final referenceChanged = _referenceId != referenceId; if (conversationChanged || moduleChanged || referenceChanged) { - log('🔄 [CallManager] Updating context (no reconnection needed)', name: 'CallManager'); - log(' Conversation: $_conversationId -> $conversationId', name: 'CallManager'); - log(' Module: $_moduleId -> $moduleId', name: 'CallManager'); - log(' Reference: $_referenceId -> $referenceId', name: 'CallManager'); - - // Update context _conversationId = conversationId; _moduleId = moduleId; _referenceId = referenceId; - // Update SignalR conversation only if needed if (conversationChanged && conversationId != null) { await _signalRService.initialize( userId: userId, @@ -143,27 +135,15 @@ class CallManager extends ChangeNotifier { conversationId: conversationId, ); } - } else { - log('✅ [CallManager] Already initialized with same credentials - skipping', name: 'CallManager'); } - // Update employee number if case changed (keep latest version) if (_myEmployeeNumber != employeeNumber) { - log('🔄 [CallManager] Updating employee number case: $_myEmployeeNumber -> $employeeNumber', name: 'CallManager'); _myEmployeeNumber = employeeNumber; } - log(' User ID: $userId', name: 'CallManager'); - log(' Employee Number: $employeeNumber', name: 'CallManager'); - log(' Conversation ID: $conversationId', name: 'CallManager'); - log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager'); return; } - log('🔄 [CallManager] User changed - full re-initialization', name: 'CallManager'); - log(' Previous User: $_userId -> New: $userId', name: 'CallManager'); - log(' Previous Employee: $_myEmployeeNumber -> New: $employeeNumber', name: 'CallManager'); - // Store module context _userId = userId; _authToken = authToken; @@ -192,40 +172,29 @@ class CallManager extends ChangeNotifier { if (!_handlersRegistered) { _registerCallHandlers(); } - - log('✅ [CallManager] Initialized successfully', name: 'CallManager'); - } catch (e, stackTrace) { - log('❌ [CallManager] Initialization error: $e\nStack: $stackTrace', - name: 'CallManager'); + log('❌ [CallManager] Initialization error: $e\nStack: $stackTrace', name: 'CallManager'); rethrow; } } /// Initialize CallKit service Future _initializeCallKit() async { - // CRITICAL: Prevent duplicate initialization if (_callKitInitialized) { - log('⚠️ [CallKit] Already initialized, skipping', name: 'CallManager'); return; } try { - log('🔧 [CallKit] Initializing CallKit service...', name: 'CallManager'); await _callKitService.initialize(); - // Setup CallKit callbacks - only ONCE _callKitService.onCallAccepted = _handleCallKitAccepted; _callKitService.onCallDeclined = _handleCallKitDeclined; _callKitService.onCallEnded = _handleCallKitEnded; _callKitService.onCallTimeout = _handleCallKitTimeout; _callKitInitialized = true; - log('✅ [CallKit] Initialized successfully (singleton callbacks set)', name: 'CallManager'); - } catch (e, stackTrace) { - log('❌ [CallKit] Initialization error: $e\nStack: $stackTrace', - name: 'CallManager'); + log('❌ [CallKit] Initialization error: $e\nStack: $stackTrace', name: 'CallManager'); } } @@ -246,7 +215,6 @@ class CallManager extends ChangeNotifier { _signalRService.on('OnError', _handleError); _handlersRegistered = true; - log('✅ [CallManager] Event handlers registered (13 events)', name: 'CallManager'); } /// Start outgoing call @@ -257,14 +225,7 @@ class CallManager extends ChangeNotifier { 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.'); @@ -272,37 +233,13 @@ class CallManager extends ChangeNotifier { 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) { @@ -310,13 +247,9 @@ class CallManager extends ChangeNotifier { } 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, @@ -327,12 +260,6 @@ class CallManager extends ChangeNotifier { 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: [ @@ -341,34 +268,21 @@ class CallManager extends ChangeNotifier { callType == CallType.video, ], ); - log('✅ [OUTGOING] CallUserAsync invoked successfully', name: 'CallManager'); - // CRITICAL FIX: Check if call was declined/cancelled while we were waiting if (_currentCall?.callId != callId || _callStatus == CallStatus.idle) { - log('⚠️ [OUTGOING] Call was cancelled/declined during setup - aborting', name: 'CallManager'); - log(' Current call ID: ${_currentCall?.callId}', name: 'CallManager'); - log(' Expected call ID: $callId', name: 'CallManager'); - log(' Current status: ${_callStatus.name}', name: 'CallManager'); return; } _updateCallStatus(CallStatus.outgoingRinging); - // Initialize WebRTC - log('🔧 [OUTGOING] Initializing WebRTC...', name: 'CallManager'); await _initializeWebRTC(); - log('✅ [OUTGOING] WebRTC initialized', name: 'CallManager'); - // CRITICAL FIX: Check again after WebRTC initialization if (_currentCall?.callId != callId || _callStatus == CallStatus.idle) { - log('⚠️ [OUTGOING] Call was cancelled/declined after WebRTC init - aborting', name: 'CallManager'); return; } - // Start timeout timer (60 seconds for outgoing calls) _callTimeoutTimer = Timer(const Duration(seconds: 60), () { if (_callStatus == CallStatus.outgoingRinging) { - log('⏱️ [OUTGOING] Call timeout - no answer after 60s', name: 'CallManager'); _cleanup(); final context = navigatorKey.currentContext; if (context != null) { @@ -376,19 +290,11 @@ class CallManager extends ChangeNotifier { } } }); - 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(); + log('❌ [OUTGOING] Error starting call: $e', name: 'CallManager', error: e, stackTrace: stackTrace); + await _cleanup(); final context = navigatorKey.currentContext; if (context != null) { @@ -406,13 +312,6 @@ class CallManager extends ChangeNotifier { 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: Store pending call data for background acceptance _pendingCallData = { 'callId': callId, 'callerId': callerId, @@ -424,19 +323,9 @@ class CallManager extends ChangeNotifier { 'callerEmployeeNumber': extraData?['callerEmployeeNumber'], 'timestamp': DateTime.now().toIso8601String(), }; - log('💾 [INCOMING] Pending call data stored for background acceptance', name: 'CallManager'); - // CRITICAL FIX: Auto-initialize CallManager if not already initialized - // This handles the case when app is in background and context is null if (_userId == null || _authToken == null) { - log('⚠️ [INCOMING] CallManager not initialized - attempting auto-initialization...', name: 'CallManager'); - - // CRITICAL FIX: Try cached credentials first (for background calls) if (_cachedUserId != null && _cachedAuthToken != null && _cachedEmployeeNumber != null) { - log('🔧 [INCOMING] Using cached credentials (app in background)', name: 'CallManager'); - log(' Cached User ID: $_cachedUserId', name: 'CallManager'); - log(' Cached Employee Number: $_cachedEmployeeNumber', name: 'CallManager'); - await initialize( userId: _cachedUserId!, authToken: _cachedAuthToken!, @@ -445,53 +334,34 @@ class CallManager extends ChangeNotifier { referenceId: extraData?['referenceId'] as String?, employeeNumber: _cachedEmployeeNumber, ); - - log('✅ [INCOMING] Auto-initialization successful (from cache)', name: 'CallManager'); - } - // Try to get credentials from ChatProvider (app in foreground) - else if (navigatorKey.currentContext != null) { + } else if (navigatorKey.currentContext != null) { try { final chatProvider = Provider.of(navigatorKey.currentContext!, listen: false); final chatLoginResponse = chatProvider.chatLoginResponse; - final sender = chatProvider.sender; // Get sender for employee number + final sender = chatProvider.sender; 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 + employeeNumber: sender?.employeeNumber, ); - - log('✅ [INCOMING] Auto-initialization successful (from ChatProvider)', name: 'CallManager'); } else { - log('❌ [INCOMING] No credentials available', name: 'CallManager'); return; } } catch (e) { - log('❌ [INCOMING] Auto-initialization failed: $e', name: 'CallManager'); return; } } else { - log('❌ [INCOMING] No cached credentials and no context available', name: 'CallManager'); - log(' This means user never logged into chat in this session', name: 'CallManager'); return; } } - // Check if already in call if (_callStatus != CallStatus.idle) { - log('⚠️ [INCOMING] Already in a call - declining with CALLER_BUSY', name: 'CallManager'); - - // Ensure SignalR is connected before declining if (!await _signalRService.ensureConnected()) { - log('❌ [INCOMING] Cannot decline - SignalR not connected', name: 'CallManager'); return; } @@ -500,23 +370,17 @@ class CallManager extends ChangeNotifier { 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, @@ -528,15 +392,11 @@ class CallManager extends ChangeNotifier { ); _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, @@ -544,47 +404,35 @@ class CallManager extends ChangeNotifier { 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), () { + _callTimeoutTimer = Timer(const Duration(seconds: 30), () async { if (_callStatus == CallStatus.incomingRinging) { - log('⏱️ [INCOMING] Call timeout - no answer after 30s', name: 'CallManager'); - _handleCallTimeout(); + await _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(); + log('❌ [INCOMING] Error handling incoming call: $e', name: 'CallManager', error: e, stackTrace: stackTrace); + await _cleanup(); } } /// Handle call timeout (for incoming calls) Future _handleCallTimeout() async { try { - 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, + _moduleId ?? '', ]); - log('✅ [TIMEOUT] CallMissedAsync invoked', name: 'CallManager'); } - _cleanup(); + await _cleanup(); } catch (e, stackTrace) { log('❌ [TIMEOUT] Error: $e', name: 'CallManager', error: e, stackTrace: stackTrace); - _cleanup(); + await _cleanup(); } } @@ -631,7 +479,7 @@ class CallManager extends ChangeNotifier { log('✅ [ACCEPT] CallManager initialized for background call', name: 'CallManager'); } else { log('❌ [ACCEPT] Cannot initialize - no cached credentials', name: 'CallManager'); - _cleanup(); + await _cleanup(); return; } } @@ -645,56 +493,32 @@ class CallManager extends ChangeNotifier { } 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'); // CRITICAL FIX: Check context availability final hasContext = navigatorKey.currentContext != null; - log('📱 [ACCEPT] 🔍 Context Check:', name: 'CallManager'); - log(' navigatorKey.currentContext != null: $hasContext', name: 'CallManager'); - log(' navigatorKey.currentState != null: ${navigatorKey.currentState != null}', name: 'CallManager'); log(' App lifecycle state: ${WidgetsBinding.instance.lifecycleState?.name ?? "NULL"}', name: 'CallManager'); if (hasContext) { - log(' Context available: TRUE', name: 'CallManager'); - log(' Context type: ${navigatorKey.currentContext.runtimeType}', name: 'CallManager'); log(' Context widget: ${navigatorKey.currentContext!.widget.runtimeType}', name: 'CallManager'); } else { log(' Context available: FALSE (background acceptance)', name: 'CallManager'); } if (hasContext) { - // Foreground: Check permissions now - log('🔍 [ACCEPT] Checking permissions (foreground)...', name: 'CallManager'); - if (!await _checkPermissions(_currentCall!.type)) { - log('❌ [ACCEPT] Permissions denied', name: 'CallManager'); - await declineCall('permission_denied'); - return; - } - log('✅ [ACCEPT] Permissions granted', name: 'CallManager'); + log('✅ [ACCEPT] App in foreground with context', name: 'CallManager'); } else { - // Background: Defer permission check until call screen is shown - log('⏭️ [ACCEPT] Skipping permission check (background call)', name: 'CallManager'); - log(' CallKit will bring app to foreground', name: 'CallManager'); - log(' Permissions will be requested when call screen is displayed', name: 'CallManager'); + log(' Lifecycle state: ${WidgetsBinding.instance.lifecycleState?.name ?? "null"}', name: 'CallManager'); } // CRITICAL: Ensure SignalR connected before accepting log('🔌 [ACCEPT] Ensuring SignalR connection...', name: 'CallManager'); - log(' Current SignalR state: ${_signalRService.connectionState}', name: 'CallManager'); - if (!await _signalRService.ensureConnected()) { log('❌ [ACCEPT] SignalR connection failed', name: 'CallManager'); - log(' Final state: ${_signalRService.connectionState}', name: 'CallManager'); - final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showSignalRNotConnected(context); } - _cleanup(); + await _cleanup(); return; } log('✅ [ACCEPT] SignalR connected', name: 'CallManager'); @@ -720,14 +544,42 @@ class CallManager extends ChangeNotifier { 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'); + // CRITICAL: Try parsing moduleId and referenceId as integers if they're numeric strings + final moduleIdValue = _moduleId ?? '0'; + final referenceIdValue = _referenceId ?? '0'; + + // Try to parse as int if possible (backend might expect int instead of string) + final moduleIdArg = int.tryParse(moduleIdValue) ?? moduleIdValue; + final referenceIdArg = int.tryParse(referenceIdValue) ?? referenceIdValue; + + log(' Module ID arg type: ${moduleIdArg.runtimeType} (value: $moduleIdArg)', name: 'CallManager'); + log(' Reference ID arg type: ${referenceIdArg.runtimeType} (value: $referenceIdArg)', name: 'CallManager'); + + try { + await _signalRService.invoke('AnswerCallAsync', args: [ + _myEmployeeNumber ?? '', + _currentCall!.peerId, + moduleIdArg, + // referenceIdArg, + // _conversationId ?? '', + ]); + log('✅ [ACCEPT] AnswerCallAsync invoked successfully', name: 'CallManager'); + } catch (answerError) { + log('❌ [ACCEPT] AnswerCallAsync FAILED on server', name: 'CallManager'); + // Cleanup and show user-friendly error + await _cleanup(); + + final context = navigatorKey.currentContext; + if (context != null) { + CallErrorHandler.showGenericError( + context, + 'Unable to accept call. The caller may have ended the call or there may be a connection issue.', + ); + } + + // Rethrow to be caught by outer catch block + rethrow; + } // Clear pending call data after successful accept _pendingCallData = null; @@ -771,16 +623,9 @@ class CallManager extends ChangeNotifier { } log('═══════════════════════════════════════════', name: 'CallManager'); - log('✅ [ACCEPT] ▶️ ACCEPT CALL COMPLETED', name: 'CallManager'); - log(' Call accepted successfully', name: 'CallManager'); - log(' WebRTC initialized and ready', name: 'CallManager'); - log(' Navigation scheduled/completed', name: 'CallManager'); - log(' Now waiting for caller to send offer via OnOfferAsync...', name: 'CallManager'); - log('═══════════════════════════════════════════', name: 'CallManager'); - } catch (e, stackTrace) { log('❌ [ACCEPT] ❌ ERROR in acceptCall(): $e\nStack: $stackTrace', name: 'CallManager'); - _cleanup(); + await _cleanup(); final context = navigatorKey.currentContext; if (context != null) { @@ -792,156 +637,81 @@ class CallManager extends ChangeNotifier { /// 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, + _moduleId ?? '', + // 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'); - + await _cleanup(); } catch (e, stackTrace) { - log('❌ [DECLINE] Error declining call: $e', - name: 'CallManager', error: e, stackTrace: stackTrace); - _cleanup(); + log('❌ [DECLINE] Error declining call: $e', name: 'CallManager', error: e, stackTrace: stackTrace); + await _cleanup(); } } /// Hang up call Future hangUp() async { try { - log('═══════════════════════════════════════════', name: 'CallManager'); - log('📞 [HANGUP] Hanging up...', name: 'CallManager'); - log(' Current call: ${_currentCall?.callId}', name: 'CallManager'); - log(' Current status: ${_callStatus.name}', name: 'CallManager'); - log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager'); - if (_currentCall == null) { - log('⚠️ [HANGUP] No call to hang up', name: 'CallManager'); return; } - // Store call info before cleanup final peerId = _currentCall!.peerId; - final callId = _currentCall!.callId; - - // CRITICAL FIX: Verify SignalR connection state - log('🔍 [HANGUP] Verifying SignalR connection...', name: 'CallManager'); try { - // Check if connected, if not try to reconnect if (!_signalRService.isConnected) { - log('⚠️ [HANGUP] SignalR not connected, attempting to reconnect...', name: 'CallManager'); final reconnected = await _signalRService.ensureConnected(); - if (!reconnected) { - log('❌ [HANGUP] SignalR reconnection failed', name: 'CallManager'); - } else { - log('✅ [HANGUP] SignalR reconnected successfully', name: 'CallManager'); + // Connection failed, continue with cleanup anyway } } - // Try to invoke HangUpAsync if connected if (_signalRService.isConnected) { - log('📤 [HANGUP] Invoking HangUpAsync...', name: 'CallManager'); - log(' From: ${_myEmployeeNumber ?? ""}', name: 'CallManager'); - log(' To: $peerId', name: 'CallManager'); - log(' Module ID: ${_moduleId ?? "1"}', name: 'CallManager'); - - // CRITICAL FIX: Backend HangUpAsync expects 3 parameters: - // 1. fromEmployeeNumber (string) - // 2. toEmployeeNumber (string) - // 3. moduleId (int or string) await _signalRService.invoke('HangUpAsync', args: [ _myEmployeeNumber ?? '', peerId, - _moduleId ?? '1', // Add moduleId as 3rd parameter + _moduleId ?? '', ]); - - log('✅ [HANGUP] HangUpAsync invoked successfully', name: 'CallManager'); - } else { - log('⚠️ [HANGUP] SignalR not connected after retry, skipping backend call', name: 'CallManager'); } } catch (e) { - log('❌ [HANGUP] Error invoking HangUpAsync: $e', name: 'CallManager'); // Continue with cleanup even if SignalR call fails } - // End CallKit call - log('📱 [HANGUP] Ending CallKit call...', name: 'CallManager'); - try { - await _callKitService.endCall(callId); - log('✅ [HANGUP] CallKit call ended', name: 'CallManager'); - } catch (e) { - log('⚠️ [HANGUP] Error ending CallKit: $e', name: 'CallManager'); - } - - // CRITICAL FIX: Don't manually navigate - let the call pages handle navigation - // Call pages listen to status changes and will pop when status becomes idle - log('🧭 [HANGUP] Navigation will be handled by call page listeners', name: 'CallManager'); - - // CRITICAL: Always cleanup to reset state for next call - log('🧹 [HANGUP] Starting cleanup...', name: 'CallManager'); - _cleanup(); + // try { + // await _callKitService.endCall(callId); + // } catch (e) { + // // Ignore CallKit errors + // } - // CRITICAL FIX: Verify SignalR is still connected after cleanup - log('🔍 [HANGUP] Post-cleanup SignalR verification...', name: 'CallManager'); - log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager'); - log(' SignalR connected: ${_signalRService.isConnected}', name: 'CallManager'); - log(' CallManager status: ${_callStatus.name}', name: 'CallManager'); - log(' Current call: ${_currentCall?.callId ?? "null"}', name: 'CallManager'); + await _cleanup(); if (!_signalRService.isConnected) { - log('⚠️ [HANGUP] SignalR disconnected after cleanup, attempting reconnect...', name: 'CallManager'); await _signalRService.ensureConnected(); - log(' Reconnect result: ${_signalRService.isConnected}', name: 'CallManager'); } - - log('✅ [HANGUP] Call ended successfully', name: 'CallManager'); - log(' SignalR ready for next call: ${_signalRService.isConnected}', name: 'CallManager'); - log('═══════════════════════════════════════════', name: 'CallManager'); - } catch (e, stackTrace) { - log('❌ [HANGUP] Error hanging up: $e', - name: 'CallManager', error: e, stackTrace: stackTrace); + log('❌ [HANGUP] Error hanging up: $e', name: 'CallManager', error: e, stackTrace: stackTrace); + await _cleanup(); - // CRITICAL FIX: Don't manually navigate in error case either - log('🧭 [HANGUP-ERROR] Navigation will be handled by call page listeners', name: 'CallManager'); - _cleanup(); - - // Try to ensure SignalR is ready for next call try { await _signalRService.ensureConnected(); } catch (reconnectError) { - log('❌ [HANGUP] Failed to reconnect SignalR: $reconnectError', name: 'CallManager'); + // Ignore reconnect errors } } } /// Toggle mute Future toggleMute() async { - log('🎤 [CONTROL] Toggling mute: $_isMuted -> ${!_isMuted}', name: 'CallManager'); - _isMuted = !_isMuted; _webrtcService.setMicrophoneMuted(_isMuted); @@ -953,26 +723,19 @@ class CallManager extends ChangeNotifier { } 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); @@ -984,140 +747,80 @@ class CallManager extends ChangeNotifier { } 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'); - log(' Current call type: ${_currentCall!.type.name}', name: 'CallManager'); - log(' WebRTC service hashCode: ${_webrtcService.hashCode}', name: 'CallManager'); - - // CRITICAL FIX: Create a completer to track initialization completion + // Reset completer for new initialization _webrtcInitCompleter = Completer(); try { - // CRITICAL FIX: Setup callbacks BEFORE initializing WebRTC - log('🔧 [WEBRTC] Setting up callbacks BEFORE initialization...', name: 'CallManager'); _setupWebRTCCallbacks(); - log('✅ [WEBRTC] Callbacks set up', name: 'CallManager'); - // Verify callback was actually set - log('🔍 [WEBRTC] Verifying callback assignment...', name: 'CallManager'); - log(' onIceConnectionStateChange is null?: ${_webrtcService.onIceConnectionStateChange == null}', name: 'CallManager'); + // Initialize with timeout protection + final initFuture = _currentCall!.type == CallType.audio + ? _webrtcService.initializeForAudioCall() + : _webrtcService.initializeForVideoCall(); + + // Add 10 second timeout for initialization + await initFuture.timeout( + const Duration(seconds: 10), + onTimeout: () { + throw TimeoutException('WebRTC initialization timed out'); + }, + ); if (_currentCall!.type == CallType.audio) { - log('🎵 [WEBRTC] Initializing for audio call', name: 'CallManager'); - await _webrtcService.initializeForAudioCall(); - - // CRITICAL FIX: Speaker is OFF by default in WebRTC service (earpiece mode) - // Update the CallManager state to reflect this _isSpeakerOn = false; - log('✅ [WEBRTC] Speaker state synchronized: OFF (earpiece mode)', name: 'CallManager'); notifyListeners(); - } else { - log('📹 [WEBRTC] Initializing for video call', name: 'CallManager'); - await _webrtcService.initializeForVideoCall(); } - // Verify callback is still set after initialization - log('🔍 [WEBRTC] Post-initialization callback verification...', name: 'CallManager'); - log(' onIceConnectionStateChange is null?: ${_webrtcService.onIceConnectionStateChange == null}', name: 'CallManager'); - log(' Peer connection initialized?: ${_webrtcService.isPeerConnectionInitialized}', name: 'CallManager'); - - log('✅ [WEBRTC] WebRTC initialized', name: 'CallManager'); - - // CRITICAL FIX: Signal that WebRTC initialization is complete + // Complete the completer if (!_webrtcInitCompleter!.isCompleted) { _webrtcInitCompleter!.complete(); - log('✅ [WEBRTC] Initialization completer signaled', name: 'CallManager'); } - // CRITICAL FIX: Process any pending offer that arrived during initialization + // Process pending offer if any if (_pendingOfferSdp != null) { - log('📥 [WEBRTC] Processing pending offer that arrived during initialization...', name: 'CallManager'); final offerSdp = _pendingOfferSdp!; _pendingOfferSdp = null; - - // Process the offer now that WebRTC is ready + + log('📥 [WEBRTC] Processing pending offer after initialization', name: 'CallManager'); + try { - log('🔧 [WEBRTC] Creating answer for pending offer...', name: 'CallManager'); final answer = await _webrtcService.createAnswer(offerSdp); - log('✅ [WEBRTC] Answer created for pending offer (length: ${answer.sdp?.length ?? 0})', name: 'CallManager'); - log('📤 [WEBRTC] Sending pending answer via AnswerOfferAsync...', name: 'CallManager'); await _signalRService.invoke('AnswerOfferAsync', args: [ _currentCall!.peerId, answer.sdp ?? '', _currentCall!.callId, ]); - log('✅ [WEBRTC] Pending answer sent successfully', name: 'CallManager'); + + log('✅ [WEBRTC] Pending offer processed successfully', name: 'CallManager'); } catch (e, stackTrace) { - log('❌ [WEBRTC] Error processing pending offer: $e\nStack: $stackTrace', - name: 'CallManager'); + log('❌ [WEBRTC] Error processing pending offer: $e\nStack: $stackTrace', name: 'CallManager'); } } } catch (e, stackTrace) { log('❌ [WEBRTC] Initialization error: $e\nStack: $stackTrace', name: 'CallManager'); - // Complete the completer with error if (!_webrtcInitCompleter!.isCompleted) { _webrtcInitCompleter!.completeError(e, stackTrace); } - + rethrow; } } void _setupWebRTCCallbacks() { - log('🔧 [WEBRTC] Setting up callbacks...', name: 'CallManager'); - log(' CallManager hashCode: $hashCode', name: 'CallManager'); - log(' WebRTC service hashCode: ${_webrtcService.hashCode}', name: 'CallManager'); - _webrtcService.onIceCandidate = (RTCIceCandidate candidate) { - log('🧊 [WEBRTC-CALLBACK] ICE candidate generated in CallManager', name: 'CallManager'); - log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', name: 'CallManager'); - final candidateJson = jsonEncode({ 'candidate': candidate.candidate, 'sdpMid': candidate.sdpMid, @@ -1132,105 +835,55 @@ class CallManager extends ChangeNotifier { }; _webrtcService.onRemoteStream = (MediaStream stream) { - log('📡 [WEBRTC-CALLBACK] Remote stream received in CallManager', name: 'CallManager'); - log(' Audio tracks: ${stream.getAudioTracks().length}', name: 'CallManager'); - log(' Video tracks: ${stream.getVideoTracks().length}', name: 'CallManager'); notifyListeners(); }; _webrtcService.onIceConnectionStateChange = (RTCIceConnectionState state) { - log('═══════════════════════════════════════════', name: 'CallManager'); - log('🔗 [WEBRTC-CALLBACK] ICE state change RECEIVED in CallManager!', name: 'CallManager'); - log(' State: ${state.toString()}', name: 'CallManager'); - log(' Current call status: ${_callStatus.name}', name: 'CallManager'); - log(' CallManager hashCode: $hashCode', name: 'CallManager'); - log('═══════════════════════════════════════════', name: 'CallManager'); - if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) { - log('✅ [WEBRTC-CALLBACK] ICE connection established - updating status to CONNECTED', name: 'CallManager'); _updateCallStatus(CallStatus.connected); _startCallDurationTimer(); - log('✅ [WEBRTC-CALLBACK] Status updated and timer started', name: 'CallManager'); } else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) { - log('❌ [WEBRTC-CALLBACK] ICE connection failed', name: 'CallManager'); - // CRITICAL FIX: Don't cleanup immediately - try to notify peer first _handleIceConnectionFailure(); } else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) { - log('⚠️ [WEBRTC-CALLBACK] ICE connection disconnected', name: 'CallManager'); - // CRITICAL FIX: Also handle disconnect gracefully _handleIceDisconnection(); - } else { - log('ℹ️ [WEBRTC-CALLBACK] ICE state: ${state.toString()}', name: 'CallManager'); } }; - - log('✅ [WEBRTC] Callbacks set up', name: 'CallManager'); - log(' onIceCandidate set?: ${_webrtcService.onIceCandidate != null}', name: 'CallManager'); - log(' onRemoteStream set?: ${_webrtcService.onRemoteStream != null}', name: 'CallManager'); - log(' onIceConnectionStateChange set?: ${_webrtcService.onIceConnectionStateChange != null}', name: 'CallManager'); } void _navigateToCallScreen() { - log('═══════════════════════════════════════════', name: 'CallManager'); - log('🧭 [NAV] ▶️ _navigateToCallScreen() CALLED', name: 'CallManager'); - log(' Timestamp: ${DateTime.now().toIso8601String()}', name: 'CallManager'); - final context = navigatorKey.currentContext; - log('🔍 [NAV] Checking preconditions...', name: 'CallManager'); - log(' navigatorKey.currentContext: ${context != null ? "EXISTS" : "NULL"}', name: 'CallManager'); - log(' _currentCall: ${_currentCall != null ? "EXISTS" : "NULL"}', name: 'CallManager'); if (context == null || _currentCall == null) { - log('❌ [NAV] Cannot navigate - missing requirements', name: 'CallManager'); - log(' Context: ${context == null ? "NULL" : "EXISTS"}', name: 'CallManager'); - log(' Current call: ${_currentCall == null ? "NULL" : "EXISTS (${_currentCall!.callId})"}', name: 'CallManager'); - log('═══════════════════════════════════════════', name: 'CallManager'); + log('⚠️ [NAV] Cannot navigate - context or call is null', name: 'CallManager'); return; } - log('✅ [NAV] Preconditions satisfied', name: 'CallManager'); - log(' Context type: ${context.runtimeType}', name: 'CallManager'); - log(' Context mounted: ${context.mounted}', name: 'CallManager'); - log(' Call ID: ${_currentCall!.callId}', name: 'CallManager'); - log(' Call type: ${_currentCall!.type.name}', name: 'CallManager'); - log(' Peer: ${_currentCall!.peerName}', name: 'CallManager'); - - final targetScreen = _currentCall!.type == CallType.audio ? 'AudioCallPage' : 'VideoCallPage'; - log('🎯 [NAV] Target screen: $targetScreen', name: 'CallManager'); - log('🚀 [NAV] Attempting Navigator.push...', name: 'CallManager'); - try { + // Check if we're already on a call screen to prevent duplicate navigation + final currentRoute = ModalRoute.of(context); + if (currentRoute != null && (currentRoute.settings.name == '/audio_call' || currentRoute.settings.name == '/video_call')) { + log('⚠️ [NAV] Already on call screen, skipping navigation', name: 'CallManager'); + return; + } + final route = MaterialPageRoute( - builder: (context) => _currentCall?.type == CallType.audio - ? const AudioCallPage() - : const VideoCallPage(), + settings: RouteSettings(name: _currentCall?.type == CallType.audio ? '/audio_call' : '/video_call'), + builder: (context) => _currentCall?.type == CallType.audio ? const AudioCallPage() : const VideoCallPage(), ); - log('✅ [NAV] MaterialPageRoute created successfully', name: 'CallManager'); - log(' Route settings: ${route.settings}', name: 'CallManager'); - log('📤 [NAV] Pushing route to navigator...', name: 'CallManager'); Navigator.of(context).push(route).then((result) { - log('✅ [NAV] Navigation completed!', name: 'CallManager'); - log(' Result: $result', name: 'CallManager'); - log(' Call screen should now be visible', name: 'CallManager'); + // Navigation completed successfully + log('✅ [NAV] Navigation completed', name: 'CallManager'); }).catchError((error, stackTrace) { log('❌ [NAV] Navigation future error: $error\nStack: $stackTrace', name: 'CallManager'); }); - - log('✅ [NAV] Navigator.push() called successfully (async)', name: 'CallManager'); - log(' Navigation is in progress...', name: 'CallManager'); } catch (e, stackTrace) { log('❌ [NAV] ❌ NAVIGATION EXCEPTION: $e\nStack: $stackTrace', name: 'CallManager'); - log(' This indicates a problem with the Navigator itself', name: 'CallManager'); } - - log('═══════════════════════════════════════════', name: 'CallManager'); } void _updateCallStatus(CallStatus newStatus) { if (_callStatus == newStatus) return; - - log('🔄 [STATUS] ${_callStatus.name} -> ${newStatus.name}', name: 'CallManager'); _callStatus = newStatus; notifyListeners(); } @@ -1239,25 +892,34 @@ class CallManager extends ChangeNotifier { _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'); - + Future _cleanup() async { _callTimeoutTimer?.cancel(); _callDurationTimer?.cancel(); - // Dispose WebRTC service (it's a singleton, just cleanup the peer connection) - _webrtcService.dispose(); + // Clear pending state + _navigationPending = false; + _pendingCallData = null; + _pendingOfferSdp = null; + _webrtcInitCompleter = null; + + try { + await _webrtcService.dispose(); + } catch (e) { + log('⚠️ [CLEANUP] WebRTC dispose error: $e', name: 'CallManager'); + } if (_currentCall != null) { - _callKitService.endCall(_currentCall!.callId); + try { + await _callKitService.endCall(_currentCall!.callId); + } catch (e) { + log('⚠️ [CLEANUP] CallKit end error: $e', name: 'CallManager'); + } } _currentCall = null; @@ -1268,15 +930,11 @@ class CallManager extends ChangeNotifier { _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(); + await _cleanup(); _callKitInitialized = false; _handlersRegistered = false; @@ -1284,55 +942,51 @@ class CallManager extends ChangeNotifier { _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'); + if (args == null || args.isEmpty) { + return; + } + + try { + final callData = args[0] as Map; + final callId = callData['callId']?.toString() ?? ''; + final callerEmployeeNumber = callData['sourceUserId']?.toString() ?? callData['employeeNumber']?.toString() ?? ''; + final callerName = callData['userName']?.toString() ?? callData['id']?.toString() ?? 'Unknown Caller'; + final isVideoCall = callData['isVideoCall'] as bool? ?? false; + + if (_currentCall?.callId == callId) { + return; + } + + _currentCall = CallSession( + callId: callId, + peerId: callerEmployeeNumber, + peerName: callerName, + type: isVideoCall ? CallType.video : CallType.audio, + direction: CallDirection.incoming, + startTime: DateTime.now(), + ); + + _updateCallStatus(CallStatus.incomingRinging); + } catch (e, stackTrace) { + log('❌ [EVENT] Failed to set up incoming call: $e\nStack: $stackTrace', name: 'CallManager'); + } } void _handleCallAccepted(List? args) async { - 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); } @@ -1340,46 +994,25 @@ class CallManager extends ChangeNotifier { _updateCallStatus(CallStatus.connecting); try { - // Ensure WebRTC is initialized before creating offer - // CALLER SIDE: Create and send offer - log('🔧 [EVENT] [CALLER] Creating SDP offer...', name: 'CallManager'); final offer = await _webrtcService.createOffer(); - log('✅ [EVENT] [CALLER] SDP offer created (length: ${offer.sdp?.length ?? 0})', name: 'CallManager'); - log(' Offer SDP type: ${offer.type}', name: 'CallManager'); - - log('📤 [EVENT] [CALLER] Sending offer via OfferAsync...', name: 'CallManager'); - log(' To peer: ${_currentCall!.peerId}', name: 'CallManager'); - log(' Call ID: ${_currentCall!.callId}', name: 'CallManager'); await _signalRService.invoke('OfferAsync', args: [ _currentCall!.peerId, offer.sdp ?? '', _currentCall!.callId, ]); - log('✅ [EVENT] [CALLER] Offer sent successfully via OfferAsync', name: 'CallManager'); - } catch (e, stackTrace) { - log('❌ [EVENT] Error handling call accepted: $e', - name: 'CallManager', error: e, stackTrace: stackTrace); - _cleanup(); + log('❌ [EVENT] Error handling call accepted: $e', name: 'CallManager', error: e, stackTrace: stackTrace); + await _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 + void _handleCallDeclined(List? args) async { 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') { @@ -1389,79 +1022,32 @@ class CallManager extends ChangeNotifier { } } - _cleanup(); + await _cleanup(); } void _handleHangUp(List? args) async { - log('═══════════════════════════════════════════', name: 'CallManager'); - log('📞 [EVENT] 🔴 OnHangUpAsync received', name: 'CallManager'); - log(' Args count: ${args?.length ?? 0}', name: 'CallManager'); - log(' Args: $args', name: 'CallManager'); - log(' Current call status: ${_callStatus.name}', name: 'CallManager'); - log(' Current call ID: ${_currentCall?.callId}', name: 'CallManager'); - log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager'); - - // CRITICAL FIX: Don't manually navigate - call pages listen to status and auto-pop when idle - // Avoid double navigation which causes navigation errors - log('🧭 [EVENT-HANGUP] Skipping manual navigation - call page will auto-pop on status change', name: 'CallManager'); - - // End CallKit call first - if (_currentCall != null) { - log('📱 [EVENT-HANGUP] Ending CallKit call...', name: 'CallManager'); - try { - await _callKitService.endCall(_currentCall!.callId); - log('✅ [EVENT-HANGUP] CallKit call ended', name: 'CallManager'); - } catch (e) { - log('⚠️ [EVENT-HANGUP] Error ending CallKit: $e', name: 'CallManager'); - } - } - - // Cleanup call resources - this will set status to idle, triggering call page navigation - log('🧹 [EVENT-HANGUP] Starting cleanup...', name: 'CallManager'); - _cleanup(); + // if (_currentCall != null) { + // try { + // await _callKitService.endCall(_currentCall!.callId); + // } catch (e) { + // // Ignore errors + // } + // } - // CRITICAL FIX: Verify SignalR is still connected after cleanup - log('🔍 [EVENT-HANGUP] Post-cleanup SignalR verification...', name: 'CallManager'); - log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager'); - log(' SignalR connected: ${_signalRService.isConnected}', name: 'CallManager'); - log(' CallManager status: ${_callStatus.name}', name: 'CallManager'); + await _cleanup(); - // CRITICAL: Ensure SignalR is ready for next call if (!_signalRService.isConnected) { - log('⚠️ [EVENT-HANGUP] SignalR disconnected after cleanup, attempting reconnect...', name: 'CallManager'); try { - final reconnected = await _signalRService.ensureConnected(); - log(' Reconnect result: $reconnected', name: 'CallManager'); - log(' SignalR state after reconnect: ${_signalRService.connectionState}', name: 'CallManager'); - - if (!reconnected) { - log('❌ [EVENT-HANGUP] Failed to reconnect SignalR!', name: 'CallManager'); - log(' This will prevent next call from working', name: 'CallManager'); - } else { - log('✅ [EVENT-HANGUP] SignalR reconnected successfully', name: 'CallManager'); - } + await _signalRService.ensureConnected(); } catch (e) { - log('❌ [EVENT-HANGUP] Error reconnecting SignalR: $e', name: 'CallManager'); + // Ignore errors } - } else { - log('✅ [EVENT-HANGUP] SignalR still connected after cleanup', name: 'CallManager'); } - - log('✅ [EVENT-HANGUP] Hangup event processed', name: 'CallManager'); - log(' Call page will auto-navigate when it detects idle status', name: 'CallManager'); - log(' SignalR ready for next call: ${_signalRService.isConnected}', name: 'CallManager'); - log('═══════════════════════════════════════════', name: 'CallManager'); } void _handleOffer(List? args) async { - log('═══════════════════════════════════════════', name: 'CallManager'); - log('📞 [EVENT] 📥 OnOfferAsync received', name: 'CallManager'); - log(' Args count: ${args?.length ?? 0}', name: 'CallManager'); - log(' Current status: ${_callStatus.name}', name: 'CallManager'); - log('═══════════════════════════════════════════', name: 'CallManager'); - if (args == null || args.isEmpty) { - log('⚠️ [EVENT] No offer data received', name: 'CallManager'); + log('⚠️ [EVENT] Offer received but args empty', name: 'CallManager'); return; } @@ -1472,67 +1058,55 @@ class CallManager extends ChangeNotifier { return; } - log(' Offer SDP length: ${offerSdp.length}', name: 'CallManager'); + log('📥 [EVENT] Offer received', name: 'CallManager'); - // CRITICAL FIX: Check if WebRTC initialization is in progress + // If WebRTC is still initializing, queue the offer if (_webrtcInitCompleter != null && !_webrtcInitCompleter!.isCompleted) { - log('⏳ [EVENT] WebRTC initialization in progress - queuing offer', name: 'CallManager'); + log('⏳ [EVENT] WebRTC still initializing, queuing offer', name: 'CallManager'); _pendingOfferSdp = offerSdp; - log('✅ [EVENT] Offer queued, will be processed after initialization completes', name: 'CallManager'); return; } - // CRITICAL FIX: Wait for WebRTC initialization if it exists + // Wait for initialization to complete with timeout if (_webrtcInitCompleter != null) { - log('⏳ [EVENT] Waiting for WebRTC initialization to complete...', name: 'CallManager'); try { - await _webrtcInitCompleter!.future; - log('✅ [EVENT] WebRTC initialization complete, proceeding with offer', name: 'CallManager'); + await _webrtcInitCompleter!.future.timeout( + const Duration(seconds: 5), + onTimeout: () { + throw TimeoutException('Waiting for WebRTC initialization timed out'); + }, + ); } catch (e) { - log('❌ [EVENT] WebRTC initialization failed: $e', name: 'CallManager'); + log('❌ [EVENT] Error waiting for WebRTC init: $e', name: 'CallManager'); return; } } // Verify peer connection is ready if (!_webrtcService.isPeerConnectionInitialized) { - log('❌ [EVENT] Peer connection not initialized - cannot process offer', name: 'CallManager'); - log(' This should not happen after waiting for completer!', name: 'CallManager'); + log('⚠️ [EVENT] PeerConnection not initialized, cannot process offer', name: 'CallManager'); return; } - log('🔧 [EVENT] Creating SDP answer...', name: 'CallManager'); + log('🔄 [EVENT] Creating answer for offer', 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'); + log('📤 [EVENT] Sending answer to peer', name: 'CallManager'); await _signalRService.invoke('AnswerOfferAsync', args: [ _currentCall!.peerId, answer.sdp ?? '', _currentCall!.callId, ]); - log('✅ [EVENT] Answer sent successfully via AnswerOfferAsync', name: 'CallManager'); + log('✅ [EVENT] Answer sent successfully', name: 'CallManager'); } catch (e, stackTrace) { - log('❌ [EVENT] Error handling offer: $e', - name: 'CallManager', error: e, stackTrace: 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.isPeerConnectionInitialized}', name: 'CallManager'); - log('═══════════════════════════════════════════', name: 'CallManager'); - if (args == null || args.isEmpty) { - log('⚠️ [EVENT] No answer data received', name: 'CallManager'); + log('⚠️ [EVENT] Answer received but args empty', name: 'CallManager'); return; } @@ -1543,31 +1117,22 @@ class CallManager extends ChangeNotifier { return; } - log(' Answer SDP length: ${answerSdp.length}', name: 'CallManager'); - - log('🔧 [EVENT] Setting remote answer...', name: 'CallManager'); + log('📥 [EVENT] Answer received, 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); + 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; } @@ -1578,137 +1143,99 @@ class CallManager extends ChangeNotifier { 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); + 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'); + void _handleCallKitAccepted(String callId) async { if (_currentCall?.callId == callId) { - acceptCall(); - } else { - log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager'); + await acceptCall(); + return; + } + + if (_currentCall == null) { + final pendingData = await PendingCallStorage.getPendingCall(); + + if (pendingData != null && pendingData['callId'] == callId) { + _pendingCallData = pendingData; + await acceptCall(); + return; + } } } - void _handleCallKitDeclined(String callId) { - log('❌ [CALLKIT] Call declined: $callId', name: 'CallManager'); + void _handleCallKitDeclined(String callId) async { if (_currentCall?.callId == callId) { - declineCall('user_declined'); - } else { - log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager'); + await declineCall('user_declined'); + return; + } + + if (_currentCall == null) { + final pendingData = await PendingCallStorage.getPendingCall(); + if (pendingData != null && pendingData['callId'] == callId) { + await PendingCallStorage.clearPendingCall(); + await PendingCallStorage.clearAcceptedCallMarker(); + } } } void _handleCallKitEnded(String callId) { - log('🔴 [CALLKIT] Call ended: $callId', name: 'CallManager'); if (_currentCall?.callId == callId) { hangUp(); - } else { - log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager'); } } void _handleCallKitTimeout(String callId) { - log('⏱️ [CALLKIT] Call timeout: $callId', name: 'CallManager'); if (_currentCall?.callId == callId) { _handleCallTimeout(); - } else { - log('⚠️ [CALLKIT] Call ID mismatch: expected ${_currentCall?.callId}, got $callId', name: 'CallManager'); } } // ==================== Additional SignalR Event Handlers ==================== void _handleUserOnline(List? args) { - log('📞 [EVENT] 🟢 OnUserOnlineAsync received', name: 'CallManager'); - log(' Args count: ${args?.length ?? 0}', name: 'CallManager'); - if (args != null && args.isNotEmpty) { - log(' User online: ${args[0]}', name: 'CallManager'); - } - log('═══════════════════════════════════════════', name: 'CallManager'); - - // Notify listeners in case UI needs to update user status notifyListeners(); } void _handleUserOffline(List? args) { - log('📞 [EVENT] 🔴 OnUserOfflineAsync received', name: 'CallManager'); - log(' Args count: ${args?.length ?? 0}', name: 'CallManager'); - if (args != null && args.isNotEmpty) { - log(' User offline: ${args[0]}', name: 'CallManager'); - } - log('═══════════════════════════════════════════', name: 'CallManager'); - - // Notify listeners in case UI needs to update user status notifyListeners(); } void _handleCallHistoryUpdated(List? args) { - log('📞 [EVENT] 📋 OnCallHistoryUpdated received', name: 'CallManager'); - log(' Args count: ${args?.length ?? 0}', name: 'CallManager'); - if (args != null && args.isNotEmpty) { - log(' Call history update: ${args[0]}', name: 'CallManager'); - } - log('═══════════════════════════════════════════', name: 'CallManager'); - - // Forward to ChatProvider if needed (call history is typically managed there) - // This is just logging for now + // Forward to ChatProvider if needed } void _handleError(List? args) { - log('═══════════════════════════════════════════', name: 'CallManager'); - log('📞 [EVENT] ❌ OnError received', name: 'CallManager'); - log(' Args count: ${args?.length ?? 0}', name: 'CallManager'); - if (args != null && args.isNotEmpty) { final error = args[0]?.toString() ?? 'Unknown error'; - log(' Error: $error', name: 'CallManager'); - // Show error to user if we have context final context = navigatorKey.currentContext; if (context != null) { CallErrorHandler.showGenericError(context, 'Call error: $error'); } } - log('═══════════════════════════════════════════', name: 'CallManager'); } // ==================== ICE Connection Failure Handlers ==================== /// Handle ICE connection failure Future _handleIceConnectionFailure() async { - log('❌ [ICE] Connection failed - attempting graceful hangup...', name: 'CallManager'); - try { - // Try to notify peer via HangUpAsync before cleanup if (_currentCall != null && await _signalRService.ensureConnected()) { - log('📤 [ICE] Notifying peer about connection failure...', name: 'CallManager'); await _signalRService.invoke('HangUpAsync', args: [ _myEmployeeNumber ?? '', _currentCall!.peerId, @@ -1716,11 +1243,10 @@ class CallManager extends ChangeNotifier { ]); } } catch (e) { - log('⚠️ [ICE] Failed to notify peer: $e', name: 'CallManager'); + // Ignore errors } - // Cleanup and show error to user - _cleanup(); + await _cleanup(); final context = navigatorKey.currentContext; if (context != null) { @@ -1733,52 +1259,28 @@ class CallManager extends ChangeNotifier { /// Handle ICE connection disconnection Future _handleIceDisconnection() async { - log('⚠️ [ICE] Connection disconnected - waiting for reconnection...', name: 'CallManager'); - - // Wait a few seconds to see if connection recovers await Future.delayed(const Duration(seconds: 5)); - // Check if still disconnected final state = _webrtcService.getIceConnectionState(); if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) { - log('❌ [ICE] Still disconnected after 5s - treating as failure', name: 'CallManager'); await _handleIceConnectionFailure(); - } else { - log('✅ [ICE] Connection recovered: $state', name: 'CallManager'); } } // ==================== PENDING CALL RESTORATION (Background/Terminated App) ==================== /// Check for pending calls on app resume - /// This is called when the app comes to foreground after background/terminated state Future checkForPendingCalls() async { try { - log('═══════════════════════════════════════════', name: 'CallManager'); - log('🔍 [MAIN ISOLATE] Checking for pending calls on app resume...', name: 'CallManager'); - - // Import the storage service final pendingCallData = await PendingCallStorage.getPendingCall(); if (pendingCallData == null) { - log('ℹ️ [MAIN ISOLATE] No pending call found', name: 'CallManager'); - log('═══════════════════════════════════════════', name: 'CallManager'); return; } - log('📞 [MAIN ISOLATE] Found pending call!', name: 'CallManager'); - log(' Call ID: ${pendingCallData['callId']}', name: 'CallManager'); - log(' Caller: ${pendingCallData['callerName']}', name: 'CallManager'); - log(' Is Video: ${pendingCallData['isVideoCall']}', name: 'CallManager'); - - // Check if call was accepted from background final acceptedCallId = await PendingCallStorage.getAcceptedCallId(); if (acceptedCallId == pendingCallData['callId']) { - log('✅ [MAIN ISOLATE] Call was ACCEPTED from background', name: 'CallManager'); - log(' Restoring call session and initializing services...', name: 'CallManager'); - - // Restore call session _currentCall = CallSession( callId: pendingCallData['callId'] as String, type: (pendingCallData['isVideoCall'] as bool) ? CallType.video : CallType.audio, @@ -1789,12 +1291,8 @@ class CallManager extends ChangeNotifier { startTime: DateTime.now(), ); - // Store pending data for acceptCall to use _pendingCallData = pendingCallData; - log('🔧 [MAIN ISOLATE] Initializing communication services via CallCoordinator...', name: 'CallManager'); - - // Use CallCoordinator to initialize SignalR and CallManager with stored credentials final coordinator = CallCoordinator(); final initialized = await coordinator.initializeForIncomingCall( callerEmployeeNumber: pendingCallData['callerEmployeeNumber'] as String? ?? pendingCallData['callerId'] as String, @@ -1804,95 +1302,46 @@ class CallManager extends ChangeNotifier { ); if (initialized) { - log('✅ [MAIN ISOLATE] Services initialized successfully', name: 'CallManager'); - - // Set navigation pending flag _navigationPending = true; - - log('✅ [MAIN ISOLATE] Call session restored, navigation pending', name: 'CallManager'); - log(' Call handlePendingNavigation() when navigator is ready', name: 'CallManager'); } else { - log('❌ [MAIN ISOLATE] Service initialization failed - cannot restore call', name: 'CallManager'); await PendingCallStorage.clearPendingCall(); await PendingCallStorage.clearAcceptedCallMarker(); } - } else { - log('ℹ️ [MAIN ISOLATE] Call not accepted yet (still ringing or declined)', name: 'CallManager'); } - - log('═══════════════════════════════════════════', name: 'CallManager'); } catch (e, stackTrace) { - log('❌ [MAIN ISOLATE] Error checking pending calls: $e\nStack: $stackTrace', - name: 'CallManager'); + log('❌ [MAIN ISOLATE] Error checking pending calls: $e\nStack: $stackTrace', name: 'CallManager'); } } /// Handle pending navigation when app resumes - /// This is called by the app lifecycle observer when the app reaches resumed state Future handlePendingNavigation() async { try { - log('═══════════════════════════════════════════', name: 'CallManager'); - log('🧭 [NAVIGATION] Handling pending navigation...', name: 'CallManager'); - log(' Navigation pending: $_navigationPending', name: 'CallManager'); - log(' Current call: ${_currentCall?.callId}', name: 'CallManager'); - log(' Call status: ${_callStatus.name}', name: 'CallManager'); - log(' Lifecycle state: ${WidgetsBinding.instance.lifecycleState?.name}', name: 'CallManager'); - - // Check if navigation is pending if (!_navigationPending) { - log('ℹ️ [NAVIGATION] No pending navigation', name: 'CallManager'); - log('═══════════════════════════════════════════', name: 'CallManager'); return; } - // Check if we have a current call if (_currentCall == null) { - log('⚠️ [NAVIGATION] Navigation pending but no current call', name: 'CallManager'); _navigationPending = false; - log('═══════════════════════════════════════════', name: 'CallManager'); return; } - // Check if navigator is ready final hasContext = navigatorKey.currentContext != null; final hasState = navigatorKey.currentState != null; - log('🔍 [NAVIGATION] Navigator readiness check:', name: 'CallManager'); - log(' currentContext != null: $hasContext', name: 'CallManager'); - log(' currentState != null: $hasState', name: 'CallManager'); - if (!hasContext || !hasState) { - log('⚠️ [NAVIGATION] Navigator not ready yet - will retry', name: 'CallManager'); - log(' This method will be called again when app fully resumes', name: 'CallManager'); - log('═══════════════════════════════════════════', name: 'CallManager'); return; } - log('✅ [NAVIGATION] Navigator is ready', name: 'CallManager'); - - // Wait for first frame to be rendered - log('⏳ [NAVIGATION] Waiting for first frame...', name: 'CallManager'); await WidgetsBinding.instance.endOfFrame; - log('✅ [NAVIGATION] First frame rendered', name: 'CallManager'); - // Clear navigation pending flag first to prevent duplicate navigation _navigationPending = false; - // Navigate to call screen - log('🚀 [NAVIGATION] Navigating to call screen...', name: 'CallManager'); _navigateToCallScreen(); - log('✅ [NAVIGATION] Navigation completed successfully', name: 'CallManager'); - - // Clear pending call data from storage await PendingCallStorage.clearPendingCall(); await PendingCallStorage.clearAcceptedCallMarker(); - log('🗑️ [NAVIGATION] Cleared pending call data from storage', name: 'CallManager'); - - log('═══════════════════════════════════════════', name: 'CallManager'); } catch (e, stackTrace) { - log('❌ [NAVIGATION] Error handling pending navigation: $e', - name: 'CallManager', error: e, stackTrace: stackTrace); + log('❌ [NAVIGATION] Error handling pending navigation: $e', name: 'CallManager', error: e, stackTrace: stackTrace); _navigationPending = false; } } diff --git a/lib/modules/cx_module/chat/services/pending_call_storage.dart b/lib/modules/cx_module/chat/services/pending_call_storage.dart index 6942f8ca..b178d621 100644 --- a/lib/modules/cx_module/chat/services/pending_call_storage.dart +++ b/lib/modules/cx_module/chat/services/pending_call_storage.dart @@ -20,12 +20,6 @@ class PendingCallStorage { String? conversationId, }) async { try { - log('═══════════════════════════════════════════', name: 'PendingCallStorage'); - log('💾 [STORAGE] Saving pending call data', name: 'PendingCallStorage'); - log(' Call ID: $callId', name: 'PendingCallStorage'); - log(' Caller: $callerName ($callerEmployeeNumber)', name: 'PendingCallStorage'); - log(' Is Video: $isVideoCall', name: 'PendingCallStorage'); - final prefs = await SharedPreferences.getInstance(); final callData = { @@ -42,8 +36,6 @@ class PendingCallStorage { await prefs.setString(_pendingCallKey, jsonEncode(callData)); - log('✅ [STORAGE] Pending call data saved successfully', name: 'PendingCallStorage'); - log('═══════════════════════════════════════════', name: 'PendingCallStorage'); } catch (e, stackTrace) { log('❌ [STORAGE] Failed to save pending call: $e', name: 'PendingCallStorage', error: e, stackTrace: stackTrace); @@ -57,20 +49,10 @@ class PendingCallStorage { final dataString = prefs.getString(_pendingCallKey); if (dataString == null) { - log('ℹ️ [STORAGE] No pending call data found', name: 'PendingCallStorage'); return null; } final callData = jsonDecode(dataString) as Map; - - log('═══════════════════════════════════════════', name: 'PendingCallStorage'); - log('📥 [STORAGE] Retrieved pending call data', name: 'PendingCallStorage'); - log(' Call ID: ${callData['callId']}', name: 'PendingCallStorage'); - log(' Caller: ${callData['callerName']} (${callData['callerEmployeeNumber']})', name: 'PendingCallStorage'); - log(' Is Video: ${callData['isVideoCall']}', name: 'PendingCallStorage'); - log(' Timestamp: ${callData['timestamp']}', name: 'PendingCallStorage'); - log('═══════════════════════════════════════════', name: 'PendingCallStorage'); - return callData; } catch (e, stackTrace) { log('❌ [STORAGE] Failed to get pending call: $e', @@ -84,7 +66,6 @@ class PendingCallStorage { try { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_pendingCallKey); - log('🗑️ [STORAGE] Pending call data cleared', name: 'PendingCallStorage'); } catch (e, stackTrace) { log('❌ [STORAGE] Failed to clear pending call: $e', name: 'PendingCallStorage', error: e, stackTrace: stackTrace); @@ -96,7 +77,6 @@ class PendingCallStorage { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_callAcceptedKey, callId); - log('✅ [STORAGE] Marked call $callId as accepted from background', name: 'PendingCallStorage'); } catch (e) { log('❌ [STORAGE] Failed to mark call as accepted: $e', name: 'PendingCallStorage'); } @@ -118,7 +98,6 @@ class PendingCallStorage { try { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_callAcceptedKey); - log('🗑️ [STORAGE] Cleared accepted call marker', name: 'PendingCallStorage'); } catch (e) { log('❌ [STORAGE] Failed to clear accepted marker: $e', name: 'PendingCallStorage'); } diff --git a/lib/modules/cx_module/chat/services/signalr_service.dart b/lib/modules/cx_module/chat/services/signalr_service.dart index a73ae217..e2a3cd0d 100644 --- a/lib/modules/cx_module/chat/services/signalr_service.dart +++ b/lib/modules/cx_module/chat/services/signalr_service.dart @@ -10,9 +10,7 @@ class SignalRService { // Singleton instance - DO NOT instantiate directly, use DI (Provider) static final SignalRService _instance = SignalRService._internal(); factory SignalRService() => _instance; - SignalRService._internal() { - log('🏗️ [SIGNALR] SignalRService singleton created (hashCode: ${hashCode})', name: 'SignalRService'); - } + SignalRService._internal(); // Single HubConnection instance - ONLY created and managed by this service HubConnection? _hubConnection; @@ -52,49 +50,24 @@ class SignalRService { /// Used for background/terminated app states when credentials aren't in memory /// Returns true if successfully connected, false otherwise Future initializeFromStorage() async { - log('═══════════════════════════════════════════', name: 'SignalRService'); - log('🔄 [SIGNALR] Initializing from stored credentials...', name: 'SignalRService'); - log(' Context: Background/Terminated app state', name: 'SignalRService'); - try { - // Step 1: Read credentials from SharedPreferences - log('📖 [SIGNALR] Reading credentials from SharedPreferences...', name: 'SignalRService'); final credentials = await AuthStorage.getCredentials(); if (credentials == null) { - log('❌ [SIGNALR] No stored credentials found', name: 'SignalRService'); - log(' Cannot initialize SignalR without credentials', name: 'SignalRService'); - log(' User must be logged in first', name: 'SignalRService'); - log('═══════════════════════════════════════════', name: 'SignalRService'); return false; } - log('✅ [SIGNALR] Credentials retrieved from storage', name: 'SignalRService'); - log(' User ID: ${credentials.userId}', name: 'SignalRService'); - log(' Token length: ${credentials.accessToken.length} chars', name: 'SignalRService'); - - // Step 2: Initialize SignalR using stored credentials - log('🔌 [SIGNALR] Initializing connection with stored credentials...', name: 'SignalRService'); final success = await initialize( userId: credentials.userId, authToken: credentials.accessToken, - conversationId: null, // No conversation for background calls + conversationId: null, ); - if (success) { - log('✅ [SIGNALR] Successfully initialized from storage', name: 'SignalRService'); - log(' Connection ID: ${_hubConnection?.connectionId}', name: 'SignalRService'); - } else { - log('❌ [SIGNALR] Failed to initialize from storage', name: 'SignalRService'); - } - - log('═══════════════════════════════════════════', name: 'SignalRService'); return success; } catch (e, stackTrace) { log('❌ [SIGNALR] Error initializing from storage: $e', name: 'SignalRService', error: e, stackTrace: stackTrace); - log('═══════════════════════════════════════════', name: 'SignalRService'); return false; } } @@ -106,54 +79,33 @@ class SignalRService { required String authToken, String? conversationId, }) async { - log('═══════════════════════════════════════════', name: 'SignalRService'); - log('🔌 [SIGNALR] initialize() called', name: 'SignalRService'); - log(' Instance hashCode: ${hashCode}', name: 'SignalRService'); - log(' User ID: $userId', name: 'SignalRService'); - log(' Conversation ID: ${conversationId ?? "none"}', name: 'SignalRService'); - - // THREAD SAFETY: If already initializing, wait for current initialization if (_isInitializing && _initializationCompleter != null) { - log('⏳ [SIGNALR] Already initializing, waiting for completion...', name: 'SignalRService'); return await _initializationCompleter!.future; } - // If already connected with same credentials, reuse connection if (isConnected && _userId == userId && _authToken == authToken) { - log('✅ [SIGNALR] Already connected with same credentials', name: 'SignalRService'); - log(' Connection ID: ${_hubConnection?.connectionId}', name: 'SignalRService'); - - // Join new conversation if provided and different if (conversationId != null && conversationId != _currentConversationId) { await _joinConversation(conversationId); } - log('═══════════════════════════════════════════', name: 'SignalRService'); return true; } - // Start new initialization _isInitializing = true; _initializationCompleter = Completer(); try { - // Store credentials _userId = userId; _authToken = authToken; _currentConversationId = conversationId; - // Dispose existing connection if any await _disposeConnection(); - // Create new connection - log('🔧 [SIGNALR] Creating new HubConnection...', name: 'SignalRService'); - log(' URL: ${URLs.chatHubUrlChat}', name: 'SignalRService'); - final httpOp = HttpConnectionOptions( skipNegotiation: false, logMessageContent: kDebugMode, - transport: HttpTransportType.WebSockets, // Use WebSockets - requestTimeout: 30000, // 30 seconds timeout + transport: HttpTransportType.WebSockets, + requestTimeout: 30000, ); _hubConnection = HubConnectionBuilder() @@ -164,14 +116,8 @@ class SignalRService { .withAutomaticReconnect(retryDelays: [2000, 5000, 10000, 20000]) .build(); - log('✅ [SIGNALR] HubConnection created (hashCode: ${_hubConnection.hashCode})', name: 'SignalRService'); - - // Setup reconnection handlers BEFORE starting connection _setupReconnectionHandlers(); - // Start connection with timeout - log('🔌 [SIGNALR] Starting connection...', name: 'SignalRService'); - try { final startFuture = _hubConnection!.start(); if (startFuture != null) { @@ -181,49 +127,34 @@ class SignalRService { throw TimeoutException('SignalR connection timeout after 30 seconds'); }, ); - } else { - log('⚠️ [SIGNALR] start() returned null, checking state...', name: 'SignalRService'); } } catch (e) { - log('❌ [SIGNALR] Connection start failed: $e', name: 'SignalRService'); rethrow; } - // Verify connection state if (_hubConnection!.state != HubConnectionState.Connected) { throw Exception('SignalR failed to connect. State: ${_hubConnection!.state}'); } - log('✅ [SIGNALR] Connection established', name: 'SignalRService'); - log(' Connection ID: ${_hubConnection!.connectionId}', name: 'SignalRService'); - log(' Connection State: ${_hubConnection!.state}', name: 'SignalRService'); - - // Emit connected state _connectionStateController.add(HubConnectionState.Connected); - // Join conversation if provided if (conversationId != null) { await _joinConversation(conversationId); } - // Re-register all event handlers _reregisterAllHandlers(); _isInitializing = false; _initializationCompleter?.complete(true); - log('═══════════════════════════════════════════', name: 'SignalRService'); return true; } catch (e, stackTrace) { log('❌ [SIGNALR] Error initializing connection: $e', name: 'SignalRService', error: e, stackTrace: stackTrace); - log(' User ID: $userId', name: 'SignalRService'); - log(' URL: ${URLs.chatHubUrlChat}', name: 'SignalRService'); _isInitializing = false; _initializationCompleter?.complete(false); - log('═══════════════════════════════════════════', name: 'SignalRService'); return false; } @@ -234,28 +165,21 @@ class SignalRService { if (_hubConnection == null) return; _hubConnection!.onclose(({Exception? error}) { - log('🔴 [SIGNALR] Connection closed: $error', name: 'SignalRService'); _connectionStateController.add(HubConnectionState.Disconnected); }); _hubConnection!.onreconnecting(({Exception? error}) { - log('🟡 [SIGNALR] Reconnecting: $error', name: 'SignalRService'); _connectionStateController.add(HubConnectionState.Reconnecting); }); _hubConnection!.onreconnected(({String? connectionId}) async { - log('🟢 [SIGNALR] Reconnected: $connectionId', name: 'SignalRService'); _connectionStateController.add(HubConnectionState.Connected); - // Rejoin conversation if we had one if (_currentConversationId != null) { await _joinConversation(_currentConversationId!); } - // Re-register all event handlers _reregisterAllHandlers(); - - log('✅ [SIGNALR] Reconnection complete', name: 'SignalRService'); }); } @@ -263,13 +187,11 @@ class SignalRService { Future _joinConversation(String conversationId) async { try { if (_hubConnection?.state != HubConnectionState.Connected) { - log('⚠️ [SIGNALR] Cannot join conversation - not connected', name: 'SignalRService'); return; } await _hubConnection!.invoke("JoinConversation", args: [conversationId]); _currentConversationId = conversationId; - log('✅ [SIGNALR] Joined conversation: $conversationId', name: 'SignalRService'); } catch (e) { log('❌ [SIGNALR] Error joining conversation: $e', name: 'SignalRService'); } @@ -278,34 +200,23 @@ class SignalRService { /// Register an event handler /// Events are stored and automatically re-registered after reconnect void on(String eventName, Function(List?) handler) { - log('🔧 [SIGNALR] Registering handler for: $eventName', name: 'SignalRService'); - - // Store handler for re-registration after reconnect if (!_eventHandlers.containsKey(eventName)) { _eventHandlers[eventName] = []; } - // Check for duplicate handler if (_eventHandlers[eventName]!.contains(handler)) { - log('⚠️ [SIGNALR] Handler already registered for: $eventName', name: 'SignalRService'); return; } _eventHandlers[eventName]!.add(handler); - // Register with SignalR if connected if (_hubConnection != null) { _hubConnection!.on(eventName, handler); - log('✅ [SIGNALR] Handler registered for: $eventName', name: 'SignalRService'); - } else { - log('⚠️ [SIGNALR] Handler stored but not registered (not connected): $eventName', name: 'SignalRService'); } } /// Unregister an event handler void off(String eventName, [Function(List?)? handler]) { - log('🔧 [SIGNALR] Unregistering handler for: $eventName', name: 'SignalRService'); - if (handler != null) { _eventHandlers[eventName]?.remove(handler); if (_eventHandlers[eventName]?.isEmpty ?? false) { @@ -315,7 +226,6 @@ class SignalRService { _eventHandlers.remove(eventName); } - // Unregister from SignalR if connected if (_hubConnection != null) { _hubConnection!.off(eventName, method: handler); } @@ -325,20 +235,14 @@ class SignalRService { void _reregisterAllHandlers() { if (_hubConnection == null) return; - log('🔄 [SIGNALR] Re-registering ${_eventHandlers.length} event types...', name: 'SignalRService'); - - int totalHandlers = 0; for (final entry in _eventHandlers.entries) { final eventName = entry.key; final handlers = entry.value; for (final handler in handlers) { _hubConnection!.on(eventName, handler); - totalHandlers++; } } - - log('✅ [SIGNALR] $totalHandlers event handlers re-registered', name: 'SignalRService'); } /// Invoke a SignalR method @@ -347,49 +251,47 @@ class SignalRService { throw Exception('SignalR not connected. Current state: ${_hubConnection?.state}'); } - log('📤 [SIGNALR] Invoking: $methodName', name: 'SignalRService'); - if (kDebugMode && args != null && args.isNotEmpty) { - log(' Args: ${args.take(3)}${args.length > 3 ? "..." : ""}', name: 'SignalRService'); - } + try { + final result = await _hubConnection!.invoke(methodName, args: args); + return result; + } catch (e, stackTrace) { + log('❌ [SIGNALR] Error invoking $methodName', name: 'SignalRService'); + log(' Error type: ${e.runtimeType}', name: 'SignalRService'); + log(' Error message: $e', name: 'SignalRService'); + log(' Connection state: ${_hubConnection?.state}', name: 'SignalRService'); + log(' Connection ID: ${_hubConnection?.connectionId}', name: 'SignalRService'); + if (args != null && args.isNotEmpty) { + log(' Args that failed: $args', name: 'SignalRService'); + } + log(' Stack trace: $stackTrace', name: 'SignalRService'); - return await _hubConnection!.invoke(methodName, args: args); + throw Exception('SignalR invoke failed: $methodName - $e'); + } } /// Ensure connection is ready (connect if needed) /// Thread-safe: Multiple callers will wait for the same connection task Future ensureConnected() async { - // If already connected, we're good if (isConnected) { return true; } - // CRITICAL FIX: If currently reconnecting, wait for it to complete - // Don't dispose the connection while it's reconnecting! if (_hubConnection?.state == HubConnectionState.Reconnecting) { - log('⏳ [SIGNALR] Already reconnecting, waiting for completion...', name: 'SignalRService'); - - // Wait up to 10 seconds for reconnection to complete final startTime = DateTime.now(); while (_hubConnection?.state == HubConnectionState.Reconnecting) { await Future.delayed(const Duration(milliseconds: 500)); - // Timeout after 10 seconds if (DateTime.now().difference(startTime).inSeconds > 10) { - log('⏱️ [SIGNALR] Reconnection timeout, forcing new connection', name: 'SignalRService'); break; } } - // Check if reconnection succeeded if (isConnected) { - log('✅ [SIGNALR] Reconnection completed successfully', name: 'SignalRService'); return true; } } - // If we have credentials, try to reconnect if (_userId != null && _authToken != null) { - log('🔄 [SIGNALR] Not connected, attempting to reconnect...', name: 'SignalRService'); return await initialize( userId: _userId!, authToken: _authToken!, @@ -397,7 +299,6 @@ class SignalRService { ); } - log('❌ [SIGNALR] Cannot reconnect - no credentials stored', name: 'SignalRService'); return false; } @@ -405,10 +306,8 @@ class SignalRService { Future _disposeConnection() async { try { if (_hubConnection != null) { - log('🔌 [SIGNALR] Disposing existing connection...', name: 'SignalRService'); await _hubConnection!.stop(); _hubConnection = null; - log('✅ [SIGNALR] Connection disposed', name: 'SignalRService'); } } catch (e) { log('⚠️ [SIGNALR] Error disposing connection: $e', name: 'SignalRService'); @@ -417,9 +316,6 @@ class SignalRService { /// Reset service (for logout) Future reset() async { - log('═══════════════════════════════════════════', name: 'SignalRService'); - log('🔄 [SIGNALR] Resetting service...', name: 'SignalRService'); - await _disposeConnection(); _eventHandlers.clear(); @@ -428,9 +324,6 @@ class SignalRService { _currentConversationId = null; _isInitializing = false; _initializationCompleter = null; - - log('✅ [SIGNALR] Service reset complete', name: 'SignalRService'); - log('═══════════════════════════════════════════', name: 'SignalRService'); } /// Disconnect from SignalR (alias for reset) diff --git a/lib/new_views/pages/land_page/land_page.dart b/lib/new_views/pages/land_page/land_page.dart index a840a9b7..32675528 100644 --- a/lib/new_views/pages/land_page/land_page.dart +++ b/lib/new_views/pages/land_page/land_page.dart @@ -234,14 +234,7 @@ class _LandPageState extends State { final myEmployeeNumber = user!.username!; // Use username as employee number final userId = user.userID?.toString() ?? user.username!; - dev.log('═══════════════════════════════════════════', name: 'LandPage'); dev.log('🚀 [LAND PAGE] Initializing calling system...', name: 'LandPage'); - dev.log(' User: ${user.username}', name: 'LandPage'); - dev.log(' User ID: $userId', name: 'LandPage'); - dev.log(' Employee Number: $myEmployeeNumber', name: 'LandPage'); - - // Step 1: Get chat credentials - dev.log('🔑 [LAND PAGE] Fetching chat credentials...', name: 'LandPage'); final chatLoginResponse = await ChatApiClient().getChatLoginToken( 1002, // moduleId - using default 0, // referenceId - using default for app-level init @@ -269,11 +262,9 @@ class _LandPageState extends State { ); if (savedCredentials) { - dev.log('✅ [LAND PAGE] Credentials saved successfully', name: 'LandPage'); dev.log(' Background calls will now work', name: 'LandPage'); } else { dev.log('⚠️ [LAND PAGE] Failed to save credentials', name: 'LandPage'); - dev.log(' Background calls may not work', name: 'LandPage'); } // Step 2: Initialize SignalR Service (SINGLETON - only once) @@ -292,11 +283,6 @@ class _LandPageState extends State { } dev.log('✅ [LAND PAGE] SignalR Service initialized', name: 'LandPage'); - dev.log(' Connection State: ${signalRService.connectionState}', name: 'LandPage'); - dev.log(' Connection ID: ${signalRService.connectionId ?? "NULL"}', name: 'LandPage'); - - // Step 3: Initialize CallManager (registers call event handlers) - dev.log('📞 [LAND PAGE] Initializing CallManager...', name: 'LandPage'); await CallManager().initialize( userId: chatLoginResponse.userId.toString(), authToken: chatLoginResponse.token!, @@ -307,13 +293,6 @@ class _LandPageState extends State { ); dev.log('✅ [LAND PAGE] CallManager initialized', name: 'LandPage'); - dev.log('═══════════════════════════════════════════', name: 'LandPage'); - dev.log('✅ [LAND PAGE] Calling system ready!', name: 'LandPage'); - dev.log(' ✅ SignalR: Connected', name: 'LandPage'); - dev.log(' ✅ CallManager: Ready', name: 'LandPage'); - dev.log(' ✅ WebRTC: Will initialize on first call', name: 'LandPage'); - dev.log(' ✅ Calls work from ANY screen now', name: 'LandPage'); - dev.log('═══════════════════════════════════════════', name: 'LandPage'); } catch (e, stackTrace) { dev.log('❌ [LAND PAGE] Error initializing calling system: $e',