calling basic workflow complete

ui_ux_rollout_merge_audio_video_call
WaseemAbbasi22 2 months ago
parent d5a21a05ca
commit 012314616a

@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
@ -17,9 +18,10 @@
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- CallKit/ConnectionService permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" />
@ -141,5 +143,15 @@
</intent-filter>
</receiver>
<!-- Override flutter_callkit_incoming service to fix Android 14+ crash -->
<service
android:name="com.hiennv.flutter_callkit_incoming.CallkitNotificationService"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="phoneCall|microphone|camera"
android:permission="${applicationId}.PERMISSION_CALL"
android:stopWithTask="false"
tools:replace="android:foregroundServiceType" />
</application>
</manifest>

@ -46,6 +46,11 @@
<key>LSApplicationQueriesSchemes</key>
<array>
<string>tel</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>voip</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
@ -53,6 +58,9 @@
<string>Recognize Speech</string>
<key>NSMicrophoneUsageDescription</key>
<string>To Record Audio </string>
<key>NSCameraUsageDescription</key>
<string>Camera access is required for video calls.</string>
<key>NSCalendarsUsageDescription</key>
<string>This app requires access to your calendar to help you manage events and schedule appointments.</string>
<key>NSContactsUsageDescription</key>

@ -25,26 +25,15 @@ import 'package:flutter_callkit_incoming/entities/entities.dart';
@pragma('vm:entry-point')
Future<void> 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<void> 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<void> 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<void> 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('<EFBFBD><EFBFBD><EFBFBD>══════════════════════════════════════════', 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<String, dynamic> remoteData = Map<String, dynamic>.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<String, dynamic>) {
Map<String, dynamic> 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?;

@ -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<void> 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<void> _registerServices() async {
// 1. SignalR Service
if (!getIt.isRegistered<SignalRService>()) {
getIt.registerLazySingleton<SignalRService>(() => SignalRService());
log(' ✅ SignalRService registered as singleton', name: 'ServiceLocator');
} else {
log(' ⚠️ SignalRService already registered, skipping', name: 'ServiceLocator');
}
@ -33,7 +27,6 @@ Future<void> _registerServices() async {
// 2. WebRTC Service
if (!getIt.isRegistered<WebRTCService>()) {
getIt.registerLazySingleton<WebRTCService>(() => WebRTCService());
log(' ✅ WebRTCService registered as singleton', name: 'ServiceLocator');
} else {
log(' ⚠️ WebRTCService already registered, skipping', name: 'ServiceLocator');
}
@ -41,7 +34,6 @@ Future<void> _registerServices() async {
// 3. CallManager (depends on SignalRService, WebRTCService)
if (!getIt.isRegistered<CallManager>()) {
getIt.registerLazySingleton<CallManager>(() => CallManager());
log(' ✅ CallManager registered as singleton', name: 'ServiceLocator');
} else {
log(' ⚠️ CallManager already registered, skipping', name: 'ServiceLocator');
}
@ -49,7 +41,6 @@ Future<void> _registerServices() async {
// 4. CallNotificationService (depends on CallManager)
if (!getIt.isRegistered<CallNotificationService>()) {
getIt.registerLazySingleton<CallNotificationService>(() => 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<void> resetServices() async {
if (getIt.isRegistered<SignalRService>()) {
await getIt<SignalRService>().reset();
}
log('✅ [DI] All services reset', name: 'ServiceLocator');
} catch (e) {
log('❌ [DI] Error resetting services: $e', name: 'ServiceLocator');
}

@ -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<void> _configureLocalTimeZone() async {
return;
}
}
///Temporary function to request camera and microphone permissions for call functionality testing
Future<void> requestCallPermissions() async {
await [
Permission.camera,
Permission.microphone,
].request();
}
final navigatorKey = GlobalKey<NavigatorState>();

@ -49,23 +49,6 @@ class _CMDetailPageState extends State<CMDetailPage> {
super.initState();
_requestProvider = Provider.of<CMDetailProvider>(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<ChatProvider>(context, listen: false);
final signalRService = getIt<SignalRService>();
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();
});
}

@ -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<AudioCallPage> {
_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<void> _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() {

@ -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<void> 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<String, dynamic>? 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: <String, dynamic>{'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<void> 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<void> 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<void> 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<List<dynamic>> 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<void> 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');
}

@ -44,36 +44,24 @@ class WebRTCService {
bool get isPeerConnectionInitialized => _peerConnection != null && _isFullyInitialized;
/// Wait for WebRTC initialization to complete
/// Returns immediately if already initialized
Future<void> 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<void> initializeForAudioCall() async {
// CRITICAL FIX: Create completer and mark as not initialized
_initializationCompleter = Completer<void>();
_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<void> initializeForVideoCall() async {
try {
if (kDebugMode) {
log('🔧 [WebRTC] Initializing video call', name: 'WebRTCService');
}
_initializationCompleter = Completer<void>();
_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<void> 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');
}

@ -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<bool> 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<bool> 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<bool> 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<bool> 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<bool> isMicrophoneGranted() async {
final status = await Permission.microphone.status;
return status.isGranted;
}
/// Check if camera permission is granted (without requesting)
static Future<bool> 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<void> _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<void> 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();
}
}
}

@ -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<VideoCallPage> {
@override
void initState() {
super.initState();
_callManager = CallManager();
_callManager = getIt<CallManager>();
// _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<void> _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() {

@ -190,8 +190,8 @@ class _ChatPageState extends State<ChatPage> {
@override
void dispose() {
final chatProvider = Provider.of<ChatProvider>(context, listen: false);
chatProvider.chatHubConnection?.stop();
// final chatProvider = Provider.of<ChatProvider>(context, listen: false);
// chatProvider.chatHubConnection?.stop();
playerController.dispose();
recorderController.dispose();
super.dispose();

@ -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<void> 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<void> 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<Object?>? args) async {
log('📞 [CALL HISTORY] OnCallHistoryUpdated received', name: 'ChatProvider');
try {
if (sender != null && recipient != null) {
userChatHistory = await ChatApiClient().loadChatHistory(

@ -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<bool> 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<void> 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');
}
}

File diff suppressed because it is too large Load Diff

@ -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<String, dynamic>;
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');
}

@ -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<bool> 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<bool>();
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: <int>[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<void> _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<Object?>?) 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<Object?>?)? 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<bool> 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<void> _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<void> 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)

@ -234,14 +234,7 @@ class _LandPageState extends State<LandPage> {
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<LandPage> {
);
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<LandPage> {
}
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<LandPage> {
);
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',

Loading…
Cancel
Save