inprogess

ui_ux_rollout_merge_audio_video_call
WaseemAbbasi22 4 weeks ago
parent 79b6d4fa2b
commit 518ab82814

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources> </resources>

@ -23,20 +23,43 @@ import 'package:test_sa/modules/cx_module/chat/call/services/call_notification_s
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async { Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
try { try {
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
log('📱 [BACKGROUND] Push notification received', name: 'FirebaseNotificationManger'); log('📱 [BACKGROUND] Push notification received', name: 'FirebaseNotificationManger');
log('📱 [BACKGROUND] Data: ${message.data}', 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?; final notificationType = message.data['notificationType'] as String? ??
message.data['type'] as String?; // Backend uses 'type'
final transactionType = message.data['transactionType'] 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 // Handle incoming call notifications
if (notificationType == 'incoming_call' || transactionType == 'call') { if (notificationType == 'incoming_call' || transactionType == 'call') {
log('📞 [BACKGROUND] Incoming call notification detected', name: 'FirebaseNotificationManger'); log('📞 [BACKGROUND] Incoming call notification detected', name: 'FirebaseNotificationManger');
log('📞 [BACKGROUND] Call data:', name: 'FirebaseNotificationManger');
log(' - callId: ${message.data['callId']}', name: 'FirebaseNotificationManger');
log(' - callerId: ${message.data['callerId']}', name: 'FirebaseNotificationManger');
log(' - calleeEmployeeNumber: ${message.data['calleeEmployeeNumber']}', name: 'FirebaseNotificationManger');
log(' - callerName: ${message.data['callerName']}', name: 'FirebaseNotificationManger');
log(' - callerUserName: ${message.data['callerUserName']}', name: 'FirebaseNotificationManger');
log(' - callerEmployeeNumber: ${message.data['callerEmployeeNumber']}', name: 'FirebaseNotificationManger');
log(' - isVideoCall: ${message.data['isVideoCall']}', name: 'FirebaseNotificationManger');
log(' - callType: ${message.data['callType']}', name: 'FirebaseNotificationManger');
log(' - applicationId: ${message.data['applicationId']}', name: 'FirebaseNotificationManger');
await CallNotificationService().handleIncomingCallNotification( await CallNotificationService().handleIncomingCallNotification(
notificationData: message.data, notificationData: message.data,
source: 'push', source: 'push',
); );
} else {
log('📱 [BACKGROUND] Non-call notification type', name: 'FirebaseNotificationManger');
} }
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
} catch (e, stackTrace) { } catch (e, stackTrace) {
log('❌ [BACKGROUND] Error handling background message: $e', log('❌ [BACKGROUND] Error handling background message: $e',
name: 'FirebaseNotificationManger', error: e, stackTrace: stackTrace); name: 'FirebaseNotificationManger', error: e, stackTrace: stackTrace);
@ -86,11 +109,14 @@ class FirebaseNotificationManger {
} }
static void _onMessageReceived(h_push.RemoteMessage remoteMessage) { static void _onMessageReceived(h_push.RemoteMessage remoteMessage) {
print("onMessageReceivedStream:${remoteMessage.toMap()}"); 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) { static void _onMessageReceiveError(Object error) {
print("onMessageReceivedStream:${error.toString()}"); log('❌ [HUAWEI] Message receive error: ${error.toString()}', name: 'FirebaseNotificationManger');
} }
static Future<bool> isGoogleServicesAvailable() async { static Future<bool> isGoogleServicesAvailable() async {
@ -107,21 +133,48 @@ class FirebaseNotificationManger {
} }
static void handleMessage(context, Map<String, dynamic> messageData) { static void handleMessage(context, Map<String, dynamic> messageData) {
// NEW: Check if this is a call notification first log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
final notificationType = messageData['notificationType'] as String?; log('📱 [HANDLE_MESSAGE] Processing notification', name: 'FirebaseNotificationManger');
log('📱 [HANDLE_MESSAGE] Full message data: $messageData', name: 'FirebaseNotificationManger');
// FIXED: Check if this is a call notification first - CHECK BOTH 'notificationType' AND 'type'
final notificationType = messageData['notificationType'] as String? ??
messageData['type'] as String?; // Backend uses 'type'
final transactionType = messageData['transactionType'] as String?; 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') { if (notificationType == 'incoming_call' || transactionType == 'call') {
log('📞 [FOREGROUND] Incoming call notification detected', name: 'FirebaseNotificationManger'); 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');
// Let CallNotificationService handle it // Let CallNotificationService handle it
CallNotificationService().handleIncomingCallNotification( CallNotificationService().handleIncomingCallNotification(
notificationData: messageData, notificationData: messageData,
source: 'push', source: 'push',
); );
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
return; 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) { if (messageData["requestType"] != null && messageData["requestNumber"] != null) {
Widget? serviceClass; Widget? serviceClass;
@ -230,52 +283,82 @@ class FirebaseNotificationManger {
// } // }
Navigator.of(context).push(MaterialPageRoute(builder: (_) => serviceClass!)); Navigator.of(context).push(MaterialPageRoute(builder: (_) => serviceClass!));
} }
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
} }
static initialized(BuildContext context) async { static initialized(BuildContext context) async {
log('🔧 [INIT] Initializing FirebaseNotificationManger', name: 'FirebaseNotificationManger');
//TOD0 add platform check here also //TOD0 add platform check here also
if (!(await isGoogleServicesAvailable()) && Platform.isAndroid) { if (!(await isGoogleServicesAvailable()) && Platform.isAndroid) {
log('📱 [INIT] Using Huawei Push Services', name: 'FirebaseNotificationManger');
// NEW: Handle Huawei initial notification // NEW: Handle Huawei initial notification
var initialNotification = await h_push.Push.getInitialNotification(); var initialNotification = await h_push.Push.getInitialNotification();
if (initialNotification != null) { 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); 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 // Check if it's a call notification
final notificationType = remoteData['notificationType'] as String?; final notificationType = remoteData['notificationType'] as String?;
log('📱 [HUAWEI_INITIAL] Notification Type: $notificationType', name: 'FirebaseNotificationManger');
if (notificationType == 'incoming_call') { if (notificationType == 'incoming_call') {
log('📞 [HUAWEI_INITIAL] Processing call notification', name: 'FirebaseNotificationManger');
await CallNotificationService().handleIncomingCallNotification( await CallNotificationService().handleIncomingCallNotification(
notificationData: remoteData, notificationData: remoteData,
source: 'push', source: 'push',
); );
} else { } else {
log('📱 [HUAWEI_INITIAL] Processing regular notification', name: 'FirebaseNotificationManger');
handleMessage(context, remoteData); handleMessage(context, remoteData);
} }
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
} else {
log('📱 [HUAWEI_INITIAL] No initial notification', name: 'FirebaseNotificationManger');
} }
h_push.Push.onNotificationOpenedApp.listen((message) { h_push.Push.onNotificationOpenedApp.listen((message) {
try { 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>) { if (message is Map<String, dynamic>) {
Map<String, dynamic> remoteData = message; Map<String, dynamic> remoteData = message;
remoteData = remoteData["extras"]; remoteData = remoteData["extras"];
log('📱 [HUAWEI_TAP] Extras data: $remoteData', name: 'FirebaseNotificationManger');
// Check if it's a call notification // Check if it's a call notification
final notificationType = remoteData['notificationType'] as String?; final notificationType = remoteData['notificationType'] as String?;
log('📱 [HUAWEI_TAP] Notification Type: $notificationType', name: 'FirebaseNotificationManger');
if (notificationType == 'incoming_call') { if (notificationType == 'incoming_call') {
log('📞 [HUAWEI_TAP] Processing call notification', name: 'FirebaseNotificationManger');
CallNotificationService().handleIncomingCallNotification( CallNotificationService().handleIncomingCallNotification(
notificationData: remoteData, notificationData: remoteData,
source: 'push', source: 'push',
); );
} else { } else {
log('📱 [HUAWEI_TAP] Processing regular notification', name: 'FirebaseNotificationManger');
handleMessage(context, remoteData); handleMessage(context, remoteData);
} }
} }
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
} catch (ex) { } catch (ex) {
print("parsingError:$ex"); log('❌ [HUAWEI_TAP] Parsing error: $ex', name: 'FirebaseNotificationManger', error: ex);
} }
}, onError: (e) => print("onNotificationOpenedApp Error${e.toString()}")); }, onError: (e) => log('❌ [HUAWEI_TAP] Error: ${e.toString()}', name: 'FirebaseNotificationManger'));
return; return;
} }
log('📱 [INIT] Using Firebase Cloud Messaging (FCM)', name: 'FirebaseNotificationManger');
NotificationSettings settings; NotificationSettings settings;
try { try {
@ -288,11 +371,14 @@ class FirebaseNotificationManger {
provisional: false, provisional: false,
sound: true, sound: true,
); );
log('✅ [INIT] Notification permissions granted: ${settings.authorizationStatus}', name: 'FirebaseNotificationManger');
} catch (error) { } catch (error) {
log('❌ [INIT] Permission request failed: $error', name: 'FirebaseNotificationManger', error: error);
return; return;
} }
if (settings.authorizationStatus != AuthorizationStatus.authorized) { if (settings.authorizationStatus != AuthorizationStatus.authorized) {
log('⚠️ [INIT] Notifications not authorized', name: 'FirebaseNotificationManger');
return; return;
} }
@ -301,33 +387,70 @@ class FirebaseNotificationManger {
FirebaseMessaging.instance.getInitialMessage().then((initialMessage) { FirebaseMessaging.instance.getInitialMessage().then((initialMessage) {
if (initialMessage != null) { 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');
// NEW: Check if it's a call notification // NEW: Check if it's a call notification
final notificationType = initialMessage.data['notificationType'] as String?; final notificationType = initialMessage.data['notificationType'] as String?;
log('📱 [FCM_INITIAL] Notification Type: $notificationType', name: 'FirebaseNotificationManger');
if (notificationType == 'incoming_call') { if (notificationType == 'incoming_call') {
log('📞 [FCM_INITIAL] Processing call notification', name: 'FirebaseNotificationManger');
CallNotificationService().handleIncomingCallNotification( CallNotificationService().handleIncomingCallNotification(
notificationData: initialMessage.data, notificationData: initialMessage.data,
source: 'push', source: 'push',
); );
} else { } else {
log('📱 [FCM_INITIAL] Processing regular notification', name: 'FirebaseNotificationManger');
handleMessage(context, initialMessage.data); handleMessage(context, initialMessage.data);
} }
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
} else {
log('📱 [FCM_INITIAL] No initial message', name: 'FirebaseNotificationManger');
} }
}); });
FirebaseMessaging.onMessage.listen((RemoteMessage message) { FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// NEW: Check if it's a call notification log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
final notificationType = message.data['notificationType'] as String?; log('📱 [FCM_FOREGROUND] Message received', name: 'FirebaseNotificationManger');
log('📱 [FCM_FOREGROUND] Message ID: ${message.messageId}', name: 'FirebaseNotificationManger');
log('📱 [FCM_FOREGROUND] Data: ${message.data}', name: 'FirebaseNotificationManger');
log('📱 [FCM_FOREGROUND] Title: ${message.notification?.title}', name: 'FirebaseNotificationManger');
log('📱 [FCM_FOREGROUND] Body: ${message.notification?.body}', name: 'FirebaseNotificationManger');
// FIXED: Check if it's a call notification - CHECK BOTH 'notificationType' AND 'type'
final notificationType = message.data['notificationType'] as String? ??
message.data['type'] as String?; // Backend uses 'type'
log('📱 [FCM_FOREGROUND] Notification Type: $notificationType', name: 'FirebaseNotificationManger');
if (notificationType == 'incoming_call') { if (notificationType == 'incoming_call') {
log('📞 [FOREGROUND] Incoming call via FCM', name: 'FirebaseNotificationManger'); log('📞 [FOREGROUND] Incoming call via FCM', name: 'FirebaseNotificationManger');
log('📞 [FCM_FOREGROUND] Call data:', name: 'FirebaseNotificationManger');
log(' - callId: ${message.data['callId']}', name: 'FirebaseNotificationManger');
log(' - callerId: ${message.data['callerId']}', name: 'FirebaseNotificationManger');
log(' - calleeEmployeeNumber: ${message.data['calleeEmployeeNumber']}', name: 'FirebaseNotificationManger');
log(' - callerName: ${message.data['callerName']}', name: 'FirebaseNotificationManger');
log(' - callerUserName: ${message.data['callerUserName']}', name: 'FirebaseNotificationManger');
log(' - callerEmployeeNumber: ${message.data['callerEmployeeNumber']}', name: 'FirebaseNotificationManger');
log(' - isVideoCall: ${message.data['isVideoCall']}', name: 'FirebaseNotificationManger');
log(' - callType: ${message.data['callType']}', name: 'FirebaseNotificationManger');
log(' - applicationId: ${message.data['applicationId']}', name: 'FirebaseNotificationManger');
CallNotificationService().handleIncomingCallNotification( CallNotificationService().handleIncomingCallNotification(
notificationData: message.data, notificationData: message.data,
source: 'push', source: 'push',
); );
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
return; return;
} }
// ...existing code... log('📱 [FCM_FOREGROUND] Non-call notification', name: 'FirebaseNotificationManger');
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
if (Platform.isAndroid) { if (Platform.isAndroid) {
if (message.data["notificationType"] != 'NurseConfirmArrive') { if (message.data["notificationType"] != 'NurseConfirmArrive') {
NotificationManger.showNotification( NotificationManger.showNotification(
@ -342,20 +465,37 @@ class FirebaseNotificationManger {
}); });
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
log('📱 [FCM_TAP] Notification tapped', name: 'FirebaseNotificationManger');
log('📱 [FCM_TAP] Message ID: ${message.messageId}', name: 'FirebaseNotificationManger');
log('📱 [FCM_TAP] Data: ${message.data}', name: 'FirebaseNotificationManger');
log('📱 [FCM_TAP] Title: ${message.notification?.title}', name: 'FirebaseNotificationManger');
log('📱 [FCM_TAP] Body: ${message.notification?.body}', name: 'FirebaseNotificationManger');
// NEW: Check if it's a call notification // NEW: Check if it's a call notification
final notificationType = message.data['notificationType'] as String?; final notificationType = message.data['notificationType'] as String?;
log('📱 [FCM_TAP] Notification Type: $notificationType', name: 'FirebaseNotificationManger');
if (notificationType == 'incoming_call') { if (notificationType == 'incoming_call') {
log('📞 [NOTIFICATION TAP] Incoming call notification tapped', name: 'FirebaseNotificationManger'); log('📞 [NOTIFICATION TAP] Incoming call notification tapped', name: 'FirebaseNotificationManger');
log('📞 [FCM_TAP] Call data:', name: 'FirebaseNotificationManger');
log(' - callId: ${message.data['callId']}', name: 'FirebaseNotificationManger');
log(' - callerId: ${message.data['callerId']}', name: 'FirebaseNotificationManger');
log(' - callerName: ${message.data['callerName']}', name: 'FirebaseNotificationManger');
CallNotificationService().handleIncomingCallNotification( CallNotificationService().handleIncomingCallNotification(
notificationData: message.data, notificationData: message.data,
source: 'push', source: 'push',
); );
} else { } else {
log('📱 [FCM_TAP] Processing regular notification', name: 'FirebaseNotificationManger');
handleMessage(context, message.data); handleMessage(context, message.data);
} }
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
}); });
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
log('✅ [INIT] FirebaseNotificationManger initialized successfully', name: 'FirebaseNotificationManger');
} }
} }

@ -172,6 +172,8 @@ void main() async {
} else { } else {
await Firebase.initializeApp(); await Firebase.initializeApp();
} }
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.white, systemNavigationBarColor: Colors.white,

@ -25,33 +25,69 @@ class CallNotificationService {
try { try {
log('═══════════════════════════════════════════', name: 'CallNotificationService'); log('═══════════════════════════════════════════', name: 'CallNotificationService');
log('📞 [INCOMING CALL] Notification received from: $source', name: 'CallNotificationService'); log('📞 [INCOMING CALL] Notification received from: $source', name: 'CallNotificationService');
log('📞 [INCOMING CALL] Data: $notificationData', name: 'CallNotificationService'); log('📞 [INCOMING CALL] Raw data: $notificationData', name: 'CallNotificationService');
// Extract call information // Extract call information - FIXED: Match actual backend field names
final notificationType = notificationData['notificationType'] as String?; final notificationType = notificationData['notificationType'] as String? ??
notificationData['type'] as String?; // Backend uses 'type'
final transactionType = notificationData['transactionType'] as String?; final transactionType = notificationData['transactionType'] as String?;
log('📞 [INCOMING CALL] Notification Type: $notificationType', name: 'CallNotificationService');
log('📞 [INCOMING CALL] Transaction Type: $transactionType', name: 'CallNotificationService');
// Verify this is a call notification // Verify this is a call notification
if (notificationType != 'incoming_call' && transactionType != 'call') { if (notificationType != 'incoming_call' && transactionType != 'call') {
log('⚠️ [INCOMING CALL] Not a call notification, ignoring', name: 'CallNotificationService'); log('⚠️ [INCOMING CALL] Not a call notification, ignoring', name: 'CallNotificationService');
log(' Expected: notificationType=incoming_call OR transactionType=call', name: 'CallNotificationService');
log(' Got: notificationType=$notificationType, transactionType=$transactionType', name: 'CallNotificationService');
return; return;
} }
// FIXED: Extract fields matching actual backend format
final callId = notificationData['callId'] as String? ?? final callId = notificationData['callId'] as String? ??
notificationData['sessionId'] as String?; notificationData['sessionId'] as String? ??
DateTime.now().millisecondsSinceEpoch.toString(); // Generate if missing
final callerId = notificationData['callerId'] as String? ?? final callerId = notificationData['callerId'] as String? ??
notificationData['calleeEmployeeNumber'] as String? ?? // Backend field
notificationData['sourceUserId'] as String?; notificationData['sourceUserId'] as String?;
final callerName = notificationData['callerName'] as String? ?? final callerName = notificationData['callerName'] as String? ??
notificationData['callerUserName'] as String? ?? // Backend field
notificationData['userName'] as String? ?? notificationData['userName'] as String? ??
'Unknown Caller'; 'Unknown Caller';
final isVideoCall = notificationData['isVideoCall'] as bool? ??
(notificationData['callType'] == 'video'); final callerEmployeeNumber = notificationData['callerEmployeeNumber'] as String?;
final moduleId = notificationData['moduleId'] as String?;
// FIXED: Handle isVideoCall as String from backend
final isVideoCall = () {
final value = notificationData['isVideoCall'];
if (value is bool) {
return value;
} else if (value is String) {
return value.toLowerCase() == 'true';
} else if (notificationData['callType'] == 'video') {
return true;
}
return false;
}();
final moduleId = notificationData['moduleId'] as String? ??
notificationData['applicationId']?.toString();
final referenceId = notificationData['referenceId'] as String?; final referenceId = notificationData['referenceId'] as String?;
final conversationId = notificationData['conversationId'] as String?; final conversationId = notificationData['conversationId'] as String?;
if (callId == null || callerId == null) { log('📞 [INCOMING CALL] Extracted data:', name: 'CallNotificationService');
log('❌ [INCOMING CALL] Missing required data (callId or callerId)', name: 'CallNotificationService'); log(' Call ID: $callId', name: 'CallNotificationService');
log(' Caller ID: $callerId', name: 'CallNotificationService');
log(' Caller Name: $callerName', name: 'CallNotificationService');
log(' Caller Employee Number: $callerEmployeeNumber', name: 'CallNotificationService');
log(' Is Video: $isVideoCall', name: 'CallNotificationService');
log(' Module ID: $moduleId', name: 'CallNotificationService');
if (callerId == null) {
log('❌ [INCOMING CALL] Missing callerId/calleeEmployeeNumber', name: 'CallNotificationService');
log(' Available fields: ${notificationData.keys.toList()}', name: 'CallNotificationService');
return; return;
} }
@ -69,15 +105,13 @@ class CallNotificationService {
} }
log('✅ [INCOMING CALL] Valid call notification', name: 'CallNotificationService'); log('✅ [INCOMING CALL] Valid call notification', name: 'CallNotificationService');
log(' Call ID: $callId', name: 'CallNotificationService');
log(' Caller: $callerName ($callerId)', name: 'CallNotificationService');
log(' Video: $isVideoCall', name: 'CallNotificationService');
// Store pending call data // Store pending call data
_pendingCallData = { _pendingCallData = {
'callId': callId, 'callId': callId,
'callerId': callerId, 'callerId': callerId,
'callerName': callerName, 'callerName': callerName,
'callerEmployeeNumber': callerEmployeeNumber,
'isVideoCall': isVideoCall, 'isVideoCall': isVideoCall,
'moduleId': moduleId, 'moduleId': moduleId,
'referenceId': referenceId, 'referenceId': referenceId,
@ -86,6 +120,7 @@ class CallNotificationService {
}; };
// Show CallKit/ConnectionService immediately // Show CallKit/ConnectionService immediately
log('📱 [INCOMING CALL] Showing CallKit UI...', name: 'CallNotificationService');
await _showNativeIncomingCallUI( await _showNativeIncomingCallUI(
callId: callId, callId: callId,
callerName: callerName, callerName: callerName,
@ -144,6 +179,8 @@ class CallNotificationService {
backgroundColor: '#0955fa', backgroundColor: '#0955fa',
backgroundUrl: '', backgroundUrl: '',
actionColor: '#4CAF50', actionColor: '#4CAF50',
textAccept: 'Accept',
textDecline: 'Decline',
incomingCallNotificationChannelName: 'Incoming Calls', incomingCallNotificationChannelName: 'Incoming Calls',
missedCallNotificationChannelName: 'Missed Calls', missedCallNotificationChannelName: 'Missed Calls',
), ),

@ -43,7 +43,8 @@ import 'call/services/callkit_service.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'call/call_error_handler.dart'; import 'call/call_error_handler.dart';
import 'call/services/call_notification_service.dart'; import 'call/services/call_notification_service.dart';
import 'services/call_manager.dart'; // NEW: Import CallManager import 'services/call_manager.dart';
import 'services/signalr_service.dart'; // ADD: Import SignalRService
HubConnection? chatHubConnection; HubConnection? chatHubConnection;
@ -351,6 +352,16 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
await chatHubConnection!.invoke("JoinConversation", args: [conversationID]); await chatHubConnection!.invoke("JoinConversation", args: [conversationID]);
log('✅ Joined conversation: $conversationID', name: 'ChatProvider'); log('✅ Joined conversation: $conversationID', name: 'ChatProvider');
// CRITICAL: Share this connection with SignalRService for CallManager
log('🔗 [SIGNALR] Sharing connection with CallManager...', name: 'ChatProvider');
SignalRService().useExistingConnection(
chatHubConnection!,
userId: chatLoginResponse!.userId.toString(),
authToken: chatLoginResponse!.token ?? '',
conversationId: conversationID,
);
log('✅ [SIGNALR] Connection shared with CallManager', name: 'ChatProvider');
// Register ALL event handlers (chat + call) // Register ALL event handlers (chat + call)
_registerAllEventHandlers(); _registerAllEventHandlers();
@ -876,7 +887,12 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
return; return;
} }
// Initialize CallManager if not already initialized // 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 { try {
await CallManager().initialize( await CallManager().initialize(
userId: chatLoginResponse!.userId.toString(), userId: chatLoginResponse!.userId.toString(),
@ -886,11 +902,27 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
referenceId: referenceID?.toString(), referenceId: referenceID?.toString(),
employeeNumber: sender!.employeeNumber, employeeNumber: sender!.employeeNumber,
); );
} catch (e) { log('✅ [ChatProvider] CallManager initialized successfully', name: 'ChatProvider');
log('⚠️ [ChatProvider] CallManager already initialized or error: $e', name: 'ChatProvider'); } catch (e, stackTrace) {
log('❌ [ChatProvider] CallManager initialization failed: $e',
name: 'ChatProvider', error: e, stackTrace: stackTrace);
// CRITICAL: Do NOT proceed if initialization failed
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(
context,
'Unable to initialize calling system. Please try again.'
);
}
return;
} }
// Delegate to CallManager // Delegate to CallManager
log('📤 [ChatProvider] Delegating to CallManager.startCall()', name: 'ChatProvider');
log(' To: ${recipient.employeeNumber} (${recipient.userName})', name: 'ChatProvider');
log(' Type: ${callType.name}', name: 'ChatProvider');
await CallManager().startCall( await CallManager().startCall(
peerId: recipient.employeeNumber ?? '', peerId: recipient.employeeNumber ?? '',
peerName: recipient.userName ?? 'Unknown', peerName: recipient.userName ?? 'Unknown',

@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/main.dart'; import 'package:test_sa/main.dart';
import 'package:test_sa/modules/cx_module/chat/call/audio_call_page.dart'; import 'package:test_sa/modules/cx_module/chat/call/audio_call_page.dart';
import 'package:test_sa/modules/cx_module/chat/call/video_call_page.dart'; import 'package:test_sa/modules/cx_module/chat/call/video_call_page.dart';
@ -11,6 +12,7 @@ import 'package:test_sa/modules/cx_module/chat/call/services/callkit_service.dar
import 'package:test_sa/modules/cx_module/chat/call/call_error_handler.dart'; import 'package:test_sa/modules/cx_module/chat/call/call_error_handler.dart';
import 'package:test_sa/modules/cx_module/chat/model/call_session.dart'; import 'package:test_sa/modules/cx_module/chat/model/call_session.dart';
import 'package:test_sa/modules/cx_module/chat/services/signalr_service.dart'; import 'package:test_sa/modules/cx_module/chat/services/signalr_service.dart';
import 'package:test_sa/modules/cx_module/chat/chat_provider.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -46,6 +48,8 @@ class CallManager extends ChangeNotifier {
bool _handlersRegistered = false; bool _handlersRegistered = false;
// Module context (for SignalR invocations) // Module context (for SignalR invocations)
String? _userId;
String? _authToken;
String? _moduleId; String? _moduleId;
String? _referenceId; String? _referenceId;
String? _conversationId; String? _conversationId;
@ -79,6 +83,8 @@ class CallManager extends ChangeNotifier {
log(' Conversation ID: ${conversationId ?? "none"}', name: 'CallManager'); log(' Conversation ID: ${conversationId ?? "none"}', name: 'CallManager');
// Store module context // Store module context
_userId = userId;
_authToken = authToken;
_moduleId = moduleId; _moduleId = moduleId;
_referenceId = referenceId; _referenceId = referenceId;
_conversationId = conversationId; _conversationId = conversationId;
@ -155,6 +161,15 @@ class CallManager extends ChangeNotifier {
_handlersRegistered = true; _handlersRegistered = true;
log('✅ [HANDLERS] All call handlers registered', name: 'CallManager'); log('✅ [HANDLERS] All call handlers registered', name: 'CallManager');
log(' - OnIncomingCallAsync', name: 'CallManager');
log(' - OnCallAcceptedAsync ⭐', name: 'CallManager');
log(' - OnCallDeclinedAsync', name: 'CallManager');
log(' - OnHangUpAsync ⭐', name: 'CallManager');
log(' - OnOfferAsync ⭐', name: 'CallManager');
log(' - OnAnswerOfferAsync ⭐', name: 'CallManager');
log(' - OnIceCandidateAsync ⭐', name: 'CallManager');
log(' - OnAudioToggle', name: 'CallManager');
log(' - OnCameraToggle', name: 'CallManager');
} }
/// Start outgoing call /// Start outgoing call
@ -168,16 +183,31 @@ class CallManager extends ChangeNotifier {
log('═══════════════════════════════════════════', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [OUTGOING] Starting ${callType.name} call to $peerName ($peerId)', name: 'CallManager'); log('📞 [OUTGOING] Starting ${callType.name} call to $peerName ($peerId)', name: 'CallManager');
// Check if already in call // CRITICAL: Verify CallManager is initialized
if (_callStatus != CallStatus.idle) { if (_userId == null || _authToken == null) {
log('⚠️ [OUTGOING] Already in a call - CALLER_BUSY', name: 'CallManager'); log('❌ [OUTGOING] CallManager not initialized!', name: 'CallManager');
log(' _userId: ${_userId ?? "NULL"}', name: 'CallManager');
log(' _authToken: ${_authToken != null ? "SET" : "NULL"}', name: 'CallManager');
final context = navigatorKey.currentContext; final context = navigatorKey.currentContext;
if (context != null) { if (context != null) {
CallErrorHandler.showCallAlreadyInProgress(context); CallErrorHandler.showGenericError(context, 'Call system not ready. Please try again.');
} }
return; return;
} }
// CRITICAL: Verify handlers are registered
if (!_handlersRegistered) {
log('❌ [OUTGOING] Call handlers not registered!', name: 'CallManager');
log(' Attempting to register handlers now...', name: 'CallManager');
_registerCallHandlers();
}
log('✅ [OUTGOING] CallManager initialized', name: 'CallManager');
log(' User ID: $_userId', name: 'CallManager');
log(' Employee Number: $_myEmployeeNumber', name: 'CallManager');
log(' Handlers registered: $_handlersRegistered', name: 'CallManager');
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
// Update status // Update status
_updateCallStatus(CallStatus.checkingPermissions); _updateCallStatus(CallStatus.checkingPermissions);
log('🔍 [OUTGOING] Checking permissions...', name: 'CallManager'); log('🔍 [OUTGOING] Checking permissions...', name: 'CallManager');
@ -290,6 +320,51 @@ class CallManager extends ChangeNotifier {
log(' Caller: $callerName ($callerId)', name: 'CallManager'); log(' Caller: $callerName ($callerId)', name: 'CallManager');
log(' Video: $isVideoCall', name: 'CallManager'); log(' Video: $isVideoCall', name: 'CallManager');
// CRITICAL FIX: Auto-initialize CallManager if not already initialized
if (_userId == null || _authToken == null) {
log('⚠️ [INCOMING] CallManager not initialized - attempting auto-initialization...', name: 'CallManager');
// Try to get credentials from ChatProvider
try {
final chatProvider = Provider.of<ChatProvider>(navigatorKey.currentContext!, listen: false);
final chatLoginResponse = chatProvider.chatLoginResponse;
final sender = chatProvider.sender; // Get sender for employee number
if (chatLoginResponse != null) {
log('🔧 [INCOMING] Found credentials in ChatProvider', name: 'CallManager');
log(' User ID: ${chatLoginResponse.userId}', name: 'CallManager');
log(' Employee Number: ${sender?.employeeNumber ?? "NOT AVAILABLE"}', name: 'CallManager');
await initialize(
userId: chatLoginResponse.userId.toString(),
authToken: chatLoginResponse.token ?? '',
conversationId: extraData?['conversationId'] as String?,
moduleId: extraData?['moduleId'] as String?,
referenceId: extraData?['referenceId'] as String?,
employeeNumber: sender?.employeeNumber, // Get from sender participant
);
log('✅ [INCOMING] Auto-initialization successful', name: 'CallManager');
} else {
log('❌ [INCOMING] No credentials available in ChatProvider', name: 'CallManager');
// Show error to user
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(context, 'Unable to receive call. Please login to chat first.');
}
return;
}
} catch (e) {
log('❌ [INCOMING] Auto-initialization failed: $e', name: 'CallManager');
// Show error to user
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showGenericError(context, 'Unable to receive call. Please open chat first.');
}
return;
}
}
// Check if already in call // Check if already in call
if (_callStatus != CallStatus.idle) { if (_callStatus != CallStatus.idle) {
log('⚠️ [INCOMING] Already in a call - declining with CALLER_BUSY', name: 'CallManager'); log('⚠️ [INCOMING] Already in a call - declining with CALLER_BUSY', name: 'CallManager');
@ -313,6 +388,10 @@ class CallManager extends ChangeNotifier {
log('🔌 [INCOMING] Ensuring SignalR connection...', name: 'CallManager'); log('🔌 [INCOMING] Ensuring SignalR connection...', name: 'CallManager');
if (!await _signalRService.ensureConnected()) { if (!await _signalRService.ensureConnected()) {
log('❌ [INCOMING] SignalR connection failed - cannot handle call', name: 'CallManager'); log('❌ [INCOMING] SignalR connection failed - cannot handle call', name: 'CallManager');
final context = navigatorKey.currentContext;
if (context != null) {
CallErrorHandler.showSignalRNotConnected(context);
}
return; return;
} }
log('✅ [INCOMING] SignalR connected', name: 'CallManager'); log('✅ [INCOMING] SignalR connected', name: 'CallManager');
@ -394,12 +473,22 @@ class CallManager extends ChangeNotifier {
try { try {
log('═══════════════════════════════════════════', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager');
log('✅ [ACCEPT] Accepting call...', name: 'CallManager'); log('✅ [ACCEPT] Accepting call...', name: 'CallManager');
log('🔍 [ACCEPT] CODE VERSION: 2026-07-19-v3-DEBUG', name: 'CallManager');
if (_currentCall == null || _callStatus != CallStatus.incomingRinging) { if (_currentCall == null || _callStatus != CallStatus.incomingRinging) {
log('⚠️ [ACCEPT] No incoming call to accept', name: 'CallManager'); log('⚠️ [ACCEPT] No incoming call to accept', name: 'CallManager');
log(' Current call: ${_currentCall?.callId}', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
return; return;
} }
log('📊 [ACCEPT] Current call state:', name: 'CallManager');
log(' Call ID: ${_currentCall!.callId}', name: 'CallManager');
log(' Peer ID: ${_currentCall!.peerId}', name: 'CallManager');
log(' Peer Name: ${_currentCall!.peerName}', name: 'CallManager');
log(' Call Type: ${_currentCall!.type.name}', name: 'CallManager');
log(' Direction: ${_currentCall!.direction.name}', name: 'CallManager');
// Check permissions // Check permissions
log('🔍 [ACCEPT] Checking permissions...', name: 'CallManager'); log('🔍 [ACCEPT] Checking permissions...', name: 'CallManager');
if (!await _checkPermissions(_currentCall!.type)) { if (!await _checkPermissions(_currentCall!.type)) {
@ -411,20 +500,31 @@ class CallManager extends ChangeNotifier {
// Ensure SignalR connected // Ensure SignalR connected
log('🔌 [ACCEPT] Ensuring SignalR connection...', name: 'CallManager'); log('🔌 [ACCEPT] Ensuring SignalR connection...', name: 'CallManager');
log(' Current SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
if (!await _signalRService.ensureConnected()) { if (!await _signalRService.ensureConnected()) {
log('❌ [ACCEPT] SignalR not connected', name: 'CallManager'); log('❌ [ACCEPT] SignalR connection failed', name: 'CallManager');
log(' Final state: ${_signalRService.connectionState}', name: 'CallManager');
_cleanup(); _cleanup();
return; return;
} }
log('✅ [ACCEPT] SignalR connected', name: 'CallManager'); log('✅ [ACCEPT] SignalR connected', name: 'CallManager');
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
_updateCallStatus(CallStatus.connecting); _updateCallStatus(CallStatus.connecting);
// Cancel timeout timer // Cancel timeout timer
_callTimeoutTimer?.cancel(); _callTimeoutTimer?.cancel();
log('✅ [ACCEPT] Timeout timer cancelled', name: 'CallManager');
// Invoke AnswerCallAsync // Invoke AnswerCallAsync
log('📤 [ACCEPT] Invoking AnswerCallAsync...', name: 'CallManager'); log('📤 [ACCEPT] Invoking AnswerCallAsync...', name: 'CallManager');
log(' My Employee Number: ${_myEmployeeNumber ?? "NULL"}', name: 'CallManager');
log(' Peer ID: ${_currentCall!.peerId}', name: 'CallManager');
log(' Module ID: ${_moduleId ?? "0"}', name: 'CallManager');
log(' Reference ID: ${_referenceId ?? "0"}', name: 'CallManager');
log(' Conversation ID: ${_conversationId ?? ""}', name: 'CallManager');
await _signalRService.invoke('AnswerCallAsync', args: [ await _signalRService.invoke('AnswerCallAsync', args: [
_myEmployeeNumber ?? '', _myEmployeeNumber ?? '',
_currentCall!.peerId, _currentCall!.peerId,
@ -432,18 +532,31 @@ class CallManager extends ChangeNotifier {
_referenceId ?? '0', _referenceId ?? '0',
_conversationId ?? '', _conversationId ?? '',
]); ]);
log('✅ [ACCEPT] AnswerCallAsync invoked', name: 'CallManager'); log('✅ [ACCEPT] AnswerCallAsync invoked successfully', name: 'CallManager');
log(' Waiting for backend to process...', name: 'CallManager');
// Initialize WebRTC // Initialize WebRTC
log('🔧 [ACCEPT] Initializing WebRTC...', name: 'CallManager'); log('🔧 [ACCEPT] Initializing WebRTC...', name: 'CallManager');
log(' Call type: ${_currentCall!.type.name}', name: 'CallManager');
await _initializeWebRTC(); await _initializeWebRTC();
log('✅ [ACCEPT] WebRTC initialized', name: 'CallManager'); log('✅ [ACCEPT] WebRTC initialized', name: 'CallManager');
log(' WebRTC service: ${_webrtcService != null ? "Created" : "NULL"}', name: 'CallManager');
// Navigate to call screen // Navigate to call screen
log('🧭 [ACCEPT] Navigating to call screen...', name: 'CallManager');
_navigateToCallScreen(); _navigateToCallScreen();
log('✅ [ACCEPT] Navigated to call screen', name: 'CallManager'); log('✅ [ACCEPT] Navigated to call screen', name: 'CallManager');
log('✅ [ACCEPT] Call accepted successfully', name: 'CallManager'); log('✅ [ACCEPT] Call accepted successfully', name: 'CallManager');
log(' [ACCEPT] Now waiting for caller to send offer via OnOfferAsync...', name: 'CallManager');
log(' [ACCEPT] Expected sequence:', name: 'CallManager');
log(' 1. Caller receives OnCallAcceptedAsync', name: 'CallManager');
log(' 2. Caller creates offer and sends OfferAsync', name: 'CallManager');
log(' 3. We receive OnOfferAsync', name: 'CallManager');
log(' 4. We create answer and send AnswerOfferAsync', name: 'CallManager');
log(' 5. Caller receives OnAnswerOfferAsync', name: 'CallManager');
log(' 6. ICE candidates exchange', name: 'CallManager');
log(' 7. Connection established', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager');
} catch (e, stackTrace) { } catch (e, stackTrace) {
@ -762,8 +875,11 @@ class CallManager extends ChangeNotifier {
// ==================== SignalR Event Handlers ==================== // ==================== SignalR Event Handlers ====================
void _handleIncomingCall(List<Object?>? args) { void _handleIncomingCall(List<Object?>? args) {
log('📞 [EVENT] OnIncomingCallAsync received', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] ⚠️ OnIncomingCallAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Args: $args', name: 'CallManager'); log(' Args: $args', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
// Note: Incoming calls are now handled by CallNotificationService // Note: Incoming calls are now handled by CallNotificationService
// This event is kept for chat screen integration // This event is kept for chat screen integration
@ -771,30 +887,71 @@ class CallManager extends ChangeNotifier {
} }
void _handleCallAccepted(List<Object?>? args) async { void _handleCallAccepted(List<Object?>? args) async {
log('📞 [EVENT] OnCallAcceptedAsync received', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] ✅ OnCallAcceptedAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Args: $args', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager'); log(' Current status: ${_callStatus.name}', name: 'CallManager');
log(' Current call: ${_currentCall?.callId}', name: 'CallManager');
log(' Call direction: ${_currentCall?.direction.name}', name: 'CallManager');
log(' Peer: ${_currentCall?.peerId}', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
// CRITICAL: Only the CALLER should create an offer
// The RECEIVER should wait for OnOfferAsync
if (_currentCall?.direction != CallDirection.outgoing) {
log('⚠️ [EVENT] This is an INCOMING call - receiver should NOT create offer', name: 'CallManager');
log(' Current direction: ${_currentCall?.direction.name}', name: 'CallManager');
log(' ✅ Receiver will wait for OnOfferAsync from caller', name: 'CallManager');
return;
}
if (_callStatus != CallStatus.outgoingRinging) { // FIXED: Accept event in both connecting and outgoingRinging states
log('⚠️ [EVENT] Not in outgoing ringing state, ignoring', name: 'CallManager'); // Backend might send OnCallAcceptedAsync very quickly before status changes to outgoingRinging
if (_callStatus != CallStatus.connecting && _callStatus != CallStatus.outgoingRinging) {
log('⚠️ [EVENT] Not in valid state for creating offer (current: ${_callStatus.name})', name: 'CallManager');
log(' Expected: connecting OR outgoingRinging', name: 'CallManager');
return; return;
} }
// If we're still in connecting, update to outgoingRinging first
if (_callStatus == CallStatus.connecting) {
log('🔄 [EVENT] Status was connecting, updating to outgoingRinging first', name: 'CallManager');
_updateCallStatus(CallStatus.outgoingRinging);
}
_callTimeoutTimer?.cancel(); _callTimeoutTimer?.cancel();
_updateCallStatus(CallStatus.connecting); _updateCallStatus(CallStatus.connecting);
try { try {
// Create and send offer // Ensure WebRTC is initialized before creating offer
log('🔧 [EVENT] Creating SDP offer...', name: 'CallManager'); if (_webrtcService == null) {
log('⚠️ [EVENT] WebRTC not initialized yet, waiting...', name: 'CallManager');
// Wait a bit for WebRTC to initialize (it should be initializing in parallel)
await Future.delayed(const Duration(milliseconds: 500));
if (_webrtcService == null) {
log('❌ [EVENT] WebRTC still not initialized after wait', name: 'CallManager');
return;
}
}
// CALLER SIDE: Create and send offer
log('🔧 [EVENT] [CALLER] Creating SDP offer...', name: 'CallManager');
final offer = await _webrtcService!.createOffer(); final offer = await _webrtcService!.createOffer();
log('✅ [EVENT] SDP offer created', name: 'CallManager'); log('✅ [EVENT] [CALLER] SDP offer created (length: ${offer.sdp?.length ?? 0})', name: 'CallManager');
log(' Offer SDP type: ${offer.type}', name: 'CallManager');
log('📤 [EVENT] [CALLER] Sending offer via OfferAsync...', name: 'CallManager');
log(' To peer: ${_currentCall!.peerId}', name: 'CallManager');
log(' Call ID: ${_currentCall!.callId}', name: 'CallManager');
log('📤 [EVENT] Sending offer via OfferAsync...', name: 'CallManager');
await _signalRService.invoke('OfferAsync', args: [ await _signalRService.invoke('OfferAsync', args: [
_currentCall!.peerId, _currentCall!.peerId,
offer.sdp ?? '', offer.sdp ?? '',
_currentCall!.callId, _currentCall!.callId,
]); ]);
log('✅ [EVENT] Offer sent successfully', name: 'CallManager'); log('✅ [EVENT] [CALLER] Offer sent successfully via OfferAsync', name: 'CallManager');
} catch (e, stackTrace) { } catch (e, stackTrace) {
log('❌ [EVENT] Error handling call accepted: $e', log('❌ [EVENT] Error handling call accepted: $e',
@ -804,8 +961,11 @@ class CallManager extends ChangeNotifier {
} }
void _handleCallDeclined(List<Object?>? args) { void _handleCallDeclined(List<Object?>? args) {
log('📞 [EVENT] OnCallDeclinedAsync received', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] ❌ OnCallDeclinedAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Args: $args', name: 'CallManager'); log(' Args: $args', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
// Extract decline reason if available // Extract decline reason if available
String? reason; String? reason;
@ -828,13 +988,24 @@ class CallManager extends ChangeNotifier {
} }
void _handleHangUp(List<Object?>? args) { void _handleHangUp(List<Object?>? args) {
log('📞 [EVENT] OnHangUpAsync received', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] 🔴 OnHangUpAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Args: $args', name: 'CallManager'); log(' Args: $args', name: 'CallManager');
log(' Current call status: ${_callStatus.name}', name: 'CallManager');
log(' Current call ID: ${_currentCall?.callId}', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
_cleanup(); _cleanup();
} }
void _handleOffer(List<Object?>? args) async { void _handleOffer(List<Object?>? args) async {
log('📞 [EVENT] OnOfferAsync received', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] 📥 OnOfferAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
log(' WebRTC initialized: ${_webrtcService != null}', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
if (args == null || args.isEmpty) { if (args == null || args.isEmpty) {
log('⚠️ [EVENT] No offer data received', name: 'CallManager'); log('⚠️ [EVENT] No offer data received', name: 'CallManager');
@ -843,11 +1014,13 @@ class CallManager extends ChangeNotifier {
try { try {
final offerSdp = args[0] as String?; final offerSdp = args[0] as String?;
if (offerSdp == null) { if (offerSdp == null || offerSdp.isEmpty) {
log('⚠️ [EVENT] Offer SDP is null', name: 'CallManager'); log('⚠️ [EVENT] Offer SDP is null or empty', name: 'CallManager');
return; return;
} }
log(' Offer SDP length: ${offerSdp.length}', name: 'CallManager');
if (_webrtcService == null) { if (_webrtcService == null) {
log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager'); log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager');
return; return;
@ -855,15 +1028,19 @@ class CallManager extends ChangeNotifier {
log('🔧 [EVENT] Creating SDP answer...', name: 'CallManager'); log('🔧 [EVENT] Creating SDP answer...', name: 'CallManager');
final answer = await _webrtcService!.createAnswer(offerSdp); final answer = await _webrtcService!.createAnswer(offerSdp);
log('✅ [EVENT] SDP answer created', name: 'CallManager'); log('✅ [EVENT] SDP answer created (length: ${answer.sdp?.length ?? 0})', name: 'CallManager');
log(' Answer SDP type: ${answer.type}', name: 'CallManager');
log('📤 [EVENT] Sending answer via AnswerOfferAsync...', name: 'CallManager'); log('📤 [EVENT] Sending answer via AnswerOfferAsync...', name: 'CallManager');
log(' To peer: ${_currentCall!.peerId}', name: 'CallManager');
log(' Call ID: ${_currentCall!.callId}', name: 'CallManager');
await _signalRService.invoke('AnswerOfferAsync', args: [ await _signalRService.invoke('AnswerOfferAsync', args: [
_currentCall!.peerId, _currentCall!.peerId,
answer.sdp ?? '', answer.sdp ?? '',
_currentCall!.callId, _currentCall!.callId,
]); ]);
log('✅ [EVENT] Answer sent successfully', name: 'CallManager'); log('✅ [EVENT] Answer sent successfully via AnswerOfferAsync', name: 'CallManager');
} catch (e, stackTrace) { } catch (e, stackTrace) {
log('❌ [EVENT] Error handling offer: $e', log('❌ [EVENT] Error handling offer: $e',
@ -872,7 +1049,12 @@ class CallManager extends ChangeNotifier {
} }
void _handleAnswer(List<Object?>? args) async { void _handleAnswer(List<Object?>? args) async {
log('📞 [EVENT] OnAnswerOfferAsync received', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager');
log('📞 [EVENT] 📥 OnAnswerOfferAsync received', name: 'CallManager');
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
log(' Current status: ${_callStatus.name}', name: 'CallManager');
log(' WebRTC initialized: ${_webrtcService != null}', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
if (args == null || args.isEmpty) { if (args == null || args.isEmpty) {
log('⚠️ [EVENT] No answer data received', name: 'CallManager'); log('⚠️ [EVENT] No answer data received', name: 'CallManager');
@ -881,11 +1063,13 @@ class CallManager extends ChangeNotifier {
try { try {
final answerSdp = args[0] as String?; final answerSdp = args[0] as String?;
if (answerSdp == null) { if (answerSdp == null || answerSdp.isEmpty) {
log('⚠️ [EVENT] Answer SDP is null', name: 'CallManager'); log('⚠️ [EVENT] Answer SDP is null or empty', name: 'CallManager');
return; return;
} }
log(' Answer SDP length: ${answerSdp.length}', name: 'CallManager');
if (_webrtcService == null) { if (_webrtcService == null) {
log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager'); log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager');
return; return;
@ -894,6 +1078,7 @@ class CallManager extends ChangeNotifier {
log('🔧 [EVENT] Setting remote answer...', name: 'CallManager'); log('🔧 [EVENT] Setting remote answer...', name: 'CallManager');
await _webrtcService!.setRemoteAnswer(answerSdp); await _webrtcService!.setRemoteAnswer(answerSdp);
log('✅ [EVENT] Remote answer set successfully', name: 'CallManager'); log('✅ [EVENT] Remote answer set successfully', name: 'CallManager');
log(' Now waiting for ICE connection to establish...', name: 'CallManager');
} catch (e, stackTrace) { } catch (e, stackTrace) {
log('❌ [EVENT] Error handling answer: $e', log('❌ [EVENT] Error handling answer: $e',
@ -902,7 +1087,7 @@ class CallManager extends ChangeNotifier {
} }
void _handleIceCandidate(List<Object?>? args) async { void _handleIceCandidate(List<Object?>? args) async {
log('📞 [EVENT] OnIceCandidateAsync received', name: 'CallManager'); log('📞 [EVENT] 🧊 OnIceCandidateAsync received', name: 'CallManager');
if (args == null || args.isEmpty) { if (args == null || args.isEmpty) {
log('⚠️ [EVENT] No ICE candidate data received', name: 'CallManager'); log('⚠️ [EVENT] No ICE candidate data received', name: 'CallManager');
@ -929,6 +1114,7 @@ class CallManager extends ChangeNotifier {
); );
log('🧊 [EVENT] Adding remote ICE candidate...', name: 'CallManager'); log('🧊 [EVENT] Adding remote ICE candidate...', name: 'CallManager');
log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', name: 'CallManager');
await _webrtcService!.addIceCandidate(candidate); await _webrtcService!.addIceCandidate(candidate);
log('✅ [EVENT] Remote ICE candidate added', name: 'CallManager'); log('✅ [EVENT] Remote ICE candidate added', name: 'CallManager');
@ -939,7 +1125,7 @@ class CallManager extends ChangeNotifier {
} }
void _handleAudioToggle(List<Object?>? args) { void _handleAudioToggle(List<Object?>? args) {
log('📞 [EVENT] OnAudioToggle received', name: 'CallManager'); log('📞 [EVENT] 🎤 OnAudioToggle received', name: 'CallManager');
log(' Peer muted: $_isPeerMuted -> ${!_isPeerMuted}', name: 'CallManager'); log(' Peer muted: $_isPeerMuted -> ${!_isPeerMuted}', name: 'CallManager');
_isPeerMuted = !_isPeerMuted; _isPeerMuted = !_isPeerMuted;
@ -947,7 +1133,7 @@ class CallManager extends ChangeNotifier {
} }
void _handleCameraToggle(List<Object?>? args) { void _handleCameraToggle(List<Object?>? args) {
log('📞 [EVENT] OnCameraToggle received', name: 'CallManager'); log('📞 [EVENT] 📹 OnCameraToggle received', name: 'CallManager');
log(' Peer camera: $_isPeerCameraOn -> ${!_isPeerCameraOn}', name: 'CallManager'); log(' Peer camera: $_isPeerCameraOn -> ${!_isPeerCameraOn}', name: 'CallManager');
_isPeerCameraOn = !_isPeerCameraOn; _isPeerCameraOn = !_isPeerCameraOn;
@ -992,4 +1178,3 @@ class CallManager extends ChangeNotifier {
} }
} }
} }

@ -35,6 +35,29 @@ class SignalRService {
/// Get current connection state /// Get current connection state
HubConnectionState? get connectionState => _hubConnection?.state; HubConnectionState? get connectionState => _hubConnection?.state;
/// Use an existing HubConnection (from ChatProvider)
/// This prevents creating duplicate connections
void useExistingConnection(HubConnection existingConnection, {
String? userId,
String? authToken,
String? conversationId,
}) {
log('═══════════════════════════════════════════', name: 'SignalRService');
log('🔌 [SIGNALR] Using existing HubConnection', name: 'SignalRService');
log(' Connection ID: ${existingConnection.connectionId}', name: 'SignalRService');
log(' User ID: ${userId ?? "not set"}', name: 'SignalRService');
log(' Conversation ID: ${conversationId ?? "not set"}', name: 'SignalRService');
log('═══════════════════════════════════════════', name: 'SignalRService');
_hubConnection = existingConnection;
_userId = userId;
_authToken = authToken;
_currentConversationId = conversationId;
// Re-register all stored event handlers on this connection
_reregisterAllHandlers();
}
/// Initialize SignalR connection with authentication /// Initialize SignalR connection with authentication
Future<bool> initialize({ Future<bool> initialize({
required String userId, required String userId,
@ -274,4 +297,3 @@ class SignalRService {
log('✅ [SIGNALR] Service reset complete', name: 'SignalRService'); log('✅ [SIGNALR] Service reset complete', name: 'SignalRService');
} }
} }

Loading…
Cancel
Save