import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'dart:typed_data'; import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart'; import 'package:intl/intl.dart'; import 'package:just_audio/just_audio.dart' as JustAudio; import 'package:just_audio/just_audio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:signalr_netcore/hub_connection.dart'; import 'package:signalr_netcore/signalr_client.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart'; import 'package:test_sa/controllers/api_routes/urls.dart'; import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/main.dart'; import 'package:test_sa/modules/cx_module/chat/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/model/chat_login_response_model.dart'; import 'package:uuid/uuid.dart'; import 'package:flutter/material.dart' as Material; import 'package:flutter/material.dart'; // NEW - for WidgetsBinding and showDialog import 'api_client.dart'; import 'chat_api_client.dart'; import 'model/chat_attachment_model.dart'; import 'model/chat_participant_model.dart'; import 'model/get_search_user_chat_model.dart'; import 'model/get_single_user_chat_list_model.dart'; import 'model/unread_message_model.dart'; import 'model/user_chat_history_model.dart'; import 'model/call_session.dart'; import 'call/call_debug_helper.dart'; import 'call/incoming_call_dialog.dart'; import 'call/services/webrtc_service.dart'; import 'call/services/callkit_service.dart'; // Add CallKit service import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'call/call_error_handler.dart'; HubConnection? chatHubConnection; class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { bool isTyping = false; bool chatLoginTokenLoading = false; ChatLoginResponse? chatLoginResponse; bool chatParticipantLoading = false; ChatParticipantModel? chatParticipantModel; bool userChatHistoryLoading = false; // UserChatHistoryModel? userChatHistory; List? userChatHistory; bool messageIsSending = false; List chatResponseList = []; Participants? sender; Participants? recipient; late String receiverID; late int moduleID; int? referenceID; // === NEW: CALL STATE VARIABLES === CallStatus callStatus = CallStatus.idle; CallSession? currentCall; Duration callDuration = Duration.zero; Timer? _callDurationTimer; Timer? _callTimeoutTimer; // Call control state bool isMuted = false; bool isSpeakerOn = false; bool isCameraOn = true; bool isPeerMuted = false; bool isPeerCameraOn = true; // NEW: Call handlers registration status bool _callHandlersRegistered = false; bool get areCallHandlersRegistered => _callHandlersRegistered; bool get isCallInProgress => callStatus != CallStatus.idle; // NEW: WebRTC service instance WebRTCService? _webrtcService; // Public getter for WebRTC service (for video renderers access) WebRTCService? get webrtcService => _webrtcService; // NEW: CallKit service instance final CallKitService _callKitService = CallKitService(); bool _callKitInitialized = false; // NEW: Remote stream for audio playback MediaStream? _remoteMediaStream; MediaStream? get remoteMediaStream => _remoteMediaStream; // NEW: Ringtone player for outgoing calls final JustAudio.AudioPlayer _ringingPlayer = JustAudio.AudioPlayer(); bool _isRingingPlaying = false; /// OPTIMIZATION: Improved connection disposal to prevent memory leaks /// This properly handles errors and ensures connection is always cleaned up Future _disposeConnection() async { try { if (chatHubConnection != null) { await chatHubConnection!.stop(); if (kDebugMode) { print('๐Ÿ”Œ SignalR connection closed successfully'); } } } catch (e) { if (kDebugMode) { print('โš ๏ธ Error closing SignalR connection: $e'); } // Don't rethrow - we still want to clean up } finally { chatHubConnection = null; } } /// Reset provider state and properly dispose SignalR connection Future reset() async { // OPTIMIZATION: Use async/await for proper cleanup await _disposeConnection(); chatLoginTokenLoading = false; chatParticipantLoading = false; userChatHistoryLoading = false; chatLoginResponse = null; chatParticipantModel = null; userChatHistory = null; sender = null; recipient = null; ChatApiClient().chatLoginResponse = null; } /// OPTIMIZATION: Override dispose to ensure SignalR connection cleanup /// This prevents connection leaks when provider is removed from widget tree @override void dispose() { _disposeConnection().then((_) { if (kDebugMode) { print('โœ… ChatProvider disposed'); } }).catchError((error) { if (kDebugMode) { print('โš ๏ธ Error during ChatProvider disposal: $error'); } }); super.dispose(); } // Future getUserAutoLoginToken(int moduleId, int requestId, String title, String myId, String assigneeEmployeeNumber) async { // reset(); // chatLoginTokenLoading = true; // notifyListeners(); // chatLoginResponse = await ChatApiClient().getChatLoginToken(moduleId, requestId, title, myId); // chatLoginTokenLoading = false; // chatParticipantLoading = true; // notifyListeners(); // // loadParticipants(moduleId, requestId); // if (chatLoginResponse != null) { // await loadChatHistory(moduleId, requestId, myId, assigneeEmployeeNumber); // } // } Future getUserAutoLoginTokenSilent(int moduleId, int requestId, String title, String myId, String assigneeEmployeeNumber, {bool isMounted = true}) async { // OPTIMIZATION: Use async reset for proper cleanup await reset(); receiverID = assigneeEmployeeNumber; chatLoginTokenLoading = true; if (isMounted) { 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('๐Ÿ“‹ Available participants: ${chatParticipantModel?.participants?.map((p) => '${p.employeeNumber} (${p.userName})').toList()}'); log('๐Ÿ” Looking for myId: $myId'); log('๐Ÿ” Looking for assigneeEmployeeNumber: $assigneeEmployeeNumber'); // Use case-insensitive matching and handle not found gracefully try { sender = chatParticipantModel?.participants?.firstWhere((participant) => participant.employeeNumber?.toLowerCase() == myId.toLowerCase()); log('โœ… sender i got is ${sender?.toJson()}'); } catch (e) { log('โš ๏ธ Sender NOT FOUND for myId: $myId. Error: $e'); sender = null; } try { recipient = chatParticipantModel?.participants?.firstWhere((participant) => participant.employeeNumber?.toLowerCase() == assigneeEmployeeNumber.toLowerCase()); log('โœ… recipient i got is ${recipient?.toJson()}'); } catch (e) { log('โš ๏ธ Recipient NOT FOUND for assigneeEmployeeNumber: $assigneeEmployeeNumber. Error: $e'); recipient = null; } } catch (ex) { if (kDebugMode) { print('โš ๏ธ Error in getUserAutoLoginTokenSilent: $ex'); } log('โŒ EXCEPTION in getUserAutoLoginTokenSilent: $ex'); } chatLoginTokenLoading = false; if (isMounted) { notifyListeners(); } } // Future getUserLoadChatHistory(int moduleId, int requestId, String myId, String assigneeEmployeeNumber) async { // await loadChatHistory(moduleId, requestId, myId, assigneeEmployeeNumber); // } // Future loadParticipants(int moduleId, int requestId, String myId, String assigneeEmployeeNumber) async { // userChatHistoryLoading = true; // notifyListeners(); // try { // chatParticipantModel = await ChatApiClient().loadParticipants(moduleId, requestId, assigneeEmployeeNumber); // } catch (ex) { // userChatHistoryLoading = false; // notifyListeners(); // return; // } // // try { // sender = chatParticipantModel?.participants?.singleWhere((participant) => participant.employeeNumber == myId); // recipient = chatParticipantModel?.participants?.singleWhere((participant) => participant.employeeNumber == assigneeEmployeeNumber); // } catch (e) {} // } Future connectToHub(int moduleId, int requestId, String myId, String assigneeEmployeeNumber, bool readOnly, {bool isMounted = true}) async { userChatHistoryLoading = true; if (isMounted) { notifyListeners(); } moduleID = moduleId; referenceID = requestId; // OPTIMIZATION: Wrap connection in try-catch to prevent leaks on error if (!readOnly) { try { await buildHubConnection(chatParticipantModel!.id!.toString()); } catch (e) { if (kDebugMode) { print('โš ๏ธ Failed to build hub connection: $e'); } // Continue loading chat history even if hub connection fails } } userChatHistory = null; userChatHistory = await ChatApiClient().loadChatHistory(moduleId, requestId, myId, assigneeEmployeeNumber); chatResponseList = userChatHistory ?? []; chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!)); userChatHistoryLoading = false; if (isMounted) { notifyListeners(); } } // Future loadChatHistory(int moduleId, int requestId, String myId, String assigneeEmployeeNumber) async { // userChatHistoryLoading = true; // notifyListeners(); // userChatHistory = await ChatApiClient().loadChatHistory(moduleId, requestId, myId, assigneeEmployeeNumber); // chatResponseList = userChatHistory ?? []; // chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!)); // userChatHistoryLoading = false; // notifyListeners(); // } Future invokeSendMessage(Object object) async { messageIsSending = true; notifyListeners(); bool returnStatus = false; try { await chatHubConnection!.invoke("AddChatUserAsync", args: [object]); returnStatus = true; } catch (ex) {} messageIsSending = false; notifyListeners(); return returnStatus; } // Future sendTextMessage(String message) async { // messageIsSending = true; // notifyListeners(); // bool returnStatus = false; // // ChatResponse? chatResponse = await ChatApiClient().sendTextMessage(message, chatParticipantModel!.id!); // if (chatResponse != null) { // returnStatus = true; // // chatResponseList.add(chatResponse); // try { // chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!)); // } catch (ex) {} // } // messageIsSending = false; // notifyListeners(); // return returnStatus; // } // List? uGroups = [], searchGroups = []; // Future getUserAutoLoginToken() async { // userLoginToken.UserAutoLoginModel userLoginResponse = await ChatApiClient().getUserLoginToken(); // // if (userLoginResponse.StatusCode == 500) { // disbaleChatForThisUser = true; // notifyListeners(); // } // // if (userLoginResponse.response != null) { // // AppState().setchatUserDetails = userLoginResponse; // } else { // // AppState().setchatUserDetails = userLoginResponse; // userLoginResponse.errorResponses!.first.fieldName.toString() + " Erorr".showToast; // disbaleChatForThisUser = true; // notifyListeners(); // } // } /// OPTIMIZATION: Improved hub connection with error handling /// Prevents connection leaks if initialization fails Future buildHubConnection(String conversationID) async { try { // Dispose existing connection if any await _disposeConnection(); chatHubConnection = await getHubConnection(); await chatHubConnection!.start(); // NEW: Log connection details for diagnostics log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•', name: 'ChatProvider'); log('๐Ÿ”Œ SignalR Hub Connection: Started', name: 'ChatProvider'); log('๐Ÿ”‘ Connection ID: ${chatHubConnection!.connectionId ?? "NULL"}', name: 'ChatProvider'); log('๐Ÿ‘ค User ID: ${chatLoginResponse!.userId}', name: 'ChatProvider'); log('๐Ÿ’ฌ Conversation ID: $conversationID', name: 'ChatProvider'); log('๐Ÿ“ž My Employee Number: ${sender?.employeeNumber ?? "NOT SET"}', name: 'ChatProvider'); log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•', name: 'ChatProvider'); if (kDebugMode) { print("๐Ÿ”Œ SignalR Hub Connection: Started"); } await chatHubConnection!.invoke("JoinConversation", args: [conversationID]); log('โœ… Joined conversation: $conversationID', name: 'ChatProvider'); // Register ALL event handlers (chat + call) _registerAllEventHandlers(); //group On message // chatHubConnection.on("OnDeliveredGroupChatHistoryAsync", onGroupMsgReceived); } catch (e) { log('โŒ [CONNECTION] Error building SignalR connection: $e', name: 'ChatProvider'); if (kDebugMode) { print('โš ๏ธ Error building SignalR connection: $e'); } // Clean up on error await _disposeConnection(); rethrow; // Rethrow so caller knows about the error } } /// Register all event handlers (chat + call) - called on initial connection and after reconnect void _registerAllEventHandlers() { if (chatHubConnection == null) { log('โš ๏ธ Cannot register event handlers - connection is null', name: 'ChatProvider'); return; } log('๐Ÿ”ง [EVENT HANDLERS] Registering all event handlers...', name: 'ChatProvider'); // Register chat event handlers chatHubConnection!.on("ReceiveMessage", onMsgReceived1); chatHubConnection!.on("OnMessageReceivedAsync", onMsgReceived); chatHubConnection!.on("OnSubmitChatAsync", onSubmitChatAsync); chatHubConnection!.on("OnTypingAsync", OnTypingAsync); chatHubConnection!.on("OnStopTypingAsync", OnStopTypingAsync); chatHubConnection!.on("OnSeenChatUserAsync", onSeenUserChatAsync); chatHubConnection!.on("OnAckSeenAsync", onAckSeenAsync); // Register call history event handler chatHubConnection!.on("OnCallHistoryUpdated", _onCallHistoryUpdated); // Register call event handlers _registerCallHandlers(); log('โœ… [EVENT HANDLERS] All event handlers registered successfully', name: 'ChatProvider'); } Future getHubConnection() async { if (kDebugMode) { print('๐Ÿ”ง Creating new SignalR hub connection...'); } HubConnection hub; HttpConnectionOptions httpOp = HttpConnectionOptions( skipNegotiation: false, logMessageContent: true, ); hub = HubConnectionBuilder() .withUrl("${URLs.chatHubUrlChat}?UserId=${chatLoginResponse!.userId}&source=Desktop&access_token=${chatLoginResponse!.token}", options: httpOp) .withAutomaticReconnect(retryDelays: [2000, 5000, 10000, 20000]).build(); // NEW: Add global event logging in debug mode AND re-register handlers on reconnect if (kDebugMode) { // Log connection state changes hub.onclose(({Exception? error}) { log('๐Ÿ”ด [SignalR] Connection closed: $error', name: 'ChatProvider'); }); hub.onreconnecting(({Exception? error}) { log('๐ŸŸก [SignalR] Reconnecting: $error', name: 'ChatProvider'); }); hub.onreconnected(({String? connectionId}) async { log('๐ŸŸข [SignalR] Reconnected: $connectionId', name: 'ChatProvider'); log('๐Ÿ”„ [SignalR] Re-registering all event handlers after reconnection...', name: 'ChatProvider'); // CRITICAL: Re-register all event handlers after reconnection _registerAllEventHandlers(); log('โœ… [SignalR] Event handlers re-registered after reconnection', name: 'ChatProvider'); }); print('โœ… SignalR hub connection created with debug logging and auto-reregister'); } else { // Production: still need to re-register handlers on reconnect hub.onreconnected(({String? connectionId}) async { _registerAllEventHandlers(); }); print('โœ… SignalR hub connection created'); } return hub; } void registerEvents() { // chatHubConnection.on("OnUpdateUserStatusAsync", changeStatus); // chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); // chatHubConnection.on("OnSubmitChatAsync", OnSubmitChatAsync); // chatHubConnection.on("OnUserTypingAsync", onUserTyping); chatHubConnection?.on("OnUserCountAsync", userCountAsync); // chatHubConnection.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow); // chatHubConnection.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered); // chatHubConnection.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus); // chatHubConnection.on("OnGetGroupUserStatusAsync", getGroupUserStatus); // // {"type":1,"target":"","arguments":[[{"id":217869,"userName":"Sultan.Khan","email":"Sultan.Khan@cloudsolutions.com.sa","phone":null,"title":"Sultan.Khan","userStatus":1,"image":null,"unreadMessageCount":0,"userAction":3,"isPin":false,"isFav":false,"isAdmin":false,"rKey":null,"totalCount":0,"isHuaweiDevice":false,"deviceToken":null},{"id":15153,"userName":"Tamer.Fanasheh","email":"Tamer.F@cloudsolutions.com.sa","phone":null,"title":"Tamer Fanasheh","userStatus":2,"image":null,"unreadMessageCount":0,"userAction":3,"isPin":false,"isFav":false,"isAdmin":true,"rKey":null,"totalCount":0,"isHuaweiDevice":false,"deviceToken":null}]]} if (kDebugMode) { // logger.i("All listeners registered"); } } // Future getUserRecentChats() async { // ChatUserModel recentChat = await ChatApiClient().getRecentChats(); // ChatUserModel favUList = await ChatApiClient().getFavUsers(); // // userGroups = await ChatApiClient().getGroupsByUserId(); // if (favUList.response != null && recentChat.response != null) { // favUsersList = favUList.response!; // favUsersList.sort((ChatUser a, ChatUser b) => a.userName!.toLowerCase().compareTo(b.userName!.toLowerCase())); // for (dynamic user in recentChat.response!) { // for (dynamic favUser in favUList.response!) { // if (user.id == favUser.id) { // user.isFav = favUser.isFav; // } // } // } // } // pChatHistory = recentChat.response ?? []; // uGroups = userGroups.groupresponse ?? []; // pChatHistory!.sort((ChatUser a, ChatUser b) => a.userName!.toLowerCase().compareTo(b.userName!.toLowerCase())); // searchedChats = pChatHistory; // isLoading = false; // await invokeUserChatHistoryNotDeliveredAsync(userId: int.parse(AppState().chatDetails!.response!.id.toString())); // sort(); // notifyListeners(); // if (searchedChats!.isNotEmpty || favUsersList.isNotEmpty) { // getUserImages(); // } // } Future invokeUserChatHistoryNotDeliveredAsync({required int userId}) async { await chatHubConnection!.invoke("GetUserChatHistoryNotDeliveredAsync", args: [userId]); return ""; } // void getSingleUserChatHistory({required int senderUID, required int receiverUID, required bool loadMore, bool isNewChat = false}) async { // isLoading = true; // if (isNewChat) userChatHistory = []; // if (!loadMore) paginationVal = 0; // isChatScreenActive = true; // receiverID = receiverUID; // Response response = await ChatApiClient().getSingleUserChatHistory(senderUID: senderUID, receiverUID: receiverUID, loadMore: loadMore, paginationVal: paginationVal); // if (response.statusCode == 204) { // if (isNewChat) { // userChatHistory = []; // } else if (loadMore) {} // } else { // if (loadMore) { // List temp = getSingleUserChatModel(response.body).reversed.toList(); // userChatHistory.addAll(temp); // } else { // userChatHistory = getSingleUserChatModel(response.body).reversed.toList(); // } // } // isLoading = false; // notifyListeners(); // // if (isChatScreenActive && receiverUID == receiverID) { // markRead(userChatHistory, receiverUID); // } // // generateConvId(); // } // // void generateConvId() async { // Uuid uuid = const Uuid(); // chatCID = uuid.v4(); // } void markRead(List data, String receiverID) { for (SingleUserChatModel element in data) { // if (AppState().chatDetails!.response!.id! == element.targetUserId) { if (element.isSeen != null) { if (!element.isSeen!) { element.isSeen = true; dynamic data = [ { "userChatHistoryId": element.userChatHistoryId, "TargetUserId": element.currentUserId == receiverID ? element.currentUserId : element.targetUserId, "isDelivered": true, "isSeen": true, } ]; updateUserChatHistoryStatusAsync(data); notifyListeners(); } // } } } } Future resetCount({ required int moduleId, required int referenceNo, String? userId, }) async { try { return await ChatApiClient().resetCountApi(moduleId, referenceNo, userId); } catch (e, stack) { debugPrint('resetCount error: $e'); rethrow; } } void updateUserChatHistoryStatusAsync(List data) { try { chatHubConnection!.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); } catch (e) { throw e; } } void updateUserChatHistoryOnMsg(List data) { try { chatHubConnection!.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); } catch (e) { throw e; } } // List getSingleUserChatModel(String str) => List.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x))); List getSingleUserChatModel(String str) { final dynamic decodedJson = json.decode(str); // Check if the decoded JSON is already a List if (decodedJson is List) { return List.from(decodedJson.map((x) => SingleUserChatModel.fromJson(x))); } // If it's a Map (a single object), wrap it in a list else if (decodedJson is Map) { return [SingleUserChatModel.fromJson(decodedJson)]; } // Handle unexpected types else { throw const FormatException('Expected a JSON object or a list of JSON objects.'); } } // List getGroupChatHistoryAsync(String str) => // List.from(json.decode(str).map((x) => groupchathistory.GetGroupChatHistoryAsync.fromJson(x))); // Future uploadAttachments(String userId, File file, String fileSource) async { dynamic result; try { Map jsonData = { "IsContextual": true.toString(), "ModuleCode": moduleID.toString(), "ReferenceId": referenceID.toString(), "ReferenceType": "ticket", "ConversationId": chatParticipantModel!.id.toString(), "TargetUserId": receiverID, "SendMessage": true.toString(), }; Object? response = await ChatApiClient().uploadMedia(userId, file, fileSource, jsonData: jsonData); if (response != null) { result = response; } else { result = []; } } catch (e) { throw e; } return result; } Future> getUnReadMessages(String employeeId) async { // employeeId = "FMEngineer"; try { Response response = await ApiClient().getJsonForResponse( "${URLs.unreadMessages}?retrieveAll=true", headers: {'x-api-key': URLs.chatApiKey, 'x-employee-number': employeeId}, ); if (response.statusCode == 200) { List data = jsonDecode(response.body); return data.map((elemet) => UnReadMessage.fromJson(elemet)).toList(); } else { return []; } } catch (error) { return []; } } // void updateUserChatStatus(List? args) { // dynamic items = args!.toList(); // for (var cItem in items[0]) { // for (SingleUserChatModel chat in userChatHistory) { // if (cItem["contantNo"].toString() == chat.contantNo.toString()) { // chat.isSeen = cItem["isSeen"]; // chat.isDelivered = cItem["isDelivered"]; // } // } // } // notifyListeners(); // } void getGroupUserStatus(List? args) { //note: need to implement this function... print(args); } Future markMessageAsRead(int messageId) async { final senderId = sender?.userId; if (senderId == null) return; chatHubConnection?.invoke( "SendMessageReadAsync", args: [messageId, senderId], ); } void onChatSeen(List? args) { dynamic items = args!.toList(); // for (var user in searchedChats!) { // if (user.id == items.first["id"]) { // user.userStatus = items.first["userStatus"]; // } // } // notifyListeners(); } void userCountAsync(List? args) { dynamic items = args!.toList(); // logger.d(items); //logger.d("---------------------------------User Count Async -------------------------------------"); //logger.d(items); // for (var user in searchedChats!) { // if (user.id == items.first["id"]) { // user.userStatus = items.first["userStatus"]; // } // } // notifyListeners(); } // void updateChatHistoryWindow(List? args) { // dynamic items = args!.toList(); // if (kDebugMode) { // logger.i("---------------------------------Update Chat History Windows Async -------------------------------------"); // } // logger.d(items); // // for (var user in searchedChats!) { // // if (user.id == items.first["id"]) { // // user.userStatus = items.first["userStatus"]; // // } // // } // // notifyListeners(); // } // void chatNotDelivered(List? args) { // dynamic items = args!.toList(); // for (dynamic item in items[0]) { // for (ChatUser element in searchedChats!) { // if (element.id == item["currentUserId"]) { // int? val = element.unreadMessageCount ?? 0; // element.unreadMessageCount = val! + 1; // } // } // } // notifyListeners(); // } // // void changeStatus(List? args) { // dynamic items = args!.toList(); // for (ChatUser user in searchedChats!) { // if (user.id == items.first["id"]) { // user.userStatus = items.first["userStatus"]; // } // } // if (teamMembersList.isNotEmpty) { // for (ChatUser user in teamMembersList!) { // if (user.id == items.first["id"]) { // user.userStatus = items.first["userStatus"]; // } // } // } // // notifyListeners(); // } // // void filter(String value) async { // List? tmp = []; // if (value.isEmpty || value == "") { // tmp = pChatHistory; // } else { // for (ChatUser element in pChatHistory!) { // if (element.userName!.toLowerCase().contains(value.toLowerCase())) { // tmp.add(element); // } // } // } // searchedChats = tmp; // notifyListeners(); // } Timer? timer; Future OnTypingAsync(List? parameters) async { // String empId = parameters!.first as String; isTyping = true; notifyListeners(); if (timer?.isActive ?? false) { timer!.cancel(); } timer = Timer(const Duration(milliseconds: 2500), () { isTyping = false; notifyListeners(); }); } Future OnStopTypingAsync(List? parameters) async { if (timer?.isActive ?? false) { timer!.cancel(); } isTyping = false; notifyListeners(); } Future onSeenUserChatAsync(List? parameters) async { try { if (parameters == null || parameters.isEmpty) { log('onSeenUserChatAsync: parameters are null or empty'); return; } final parm = parameters.first; if (parm is! List || parm.isEmpty) { log('onSeenUserChatAsync: parm is not a valid list'); return; } final firstItem = parm.first; if (firstItem is! Map) { log('onSeenUserChatAsync: firstItem is not a Map'); return; } await chatHubConnection!.invoke( "AckSeenAsync", args: [ firstItem['currentUserId'], [firstItem['userChatHistoryId']], ], ); } catch (e, stackTrace) { log('onSeenUserChatAsync error: $e'); log('StackTrace: $stackTrace'); } } Future onAckSeenAsync(List? parameters) async { try { if (parameters == null || parameters.isEmpty) { log('onAckSeenAsync: parameters are null or empty'); return; } final parm = parameters.first; log('parm onAckSeenAsync $parm'); if (chatResponseList.isEmpty) { log('onAckSeenAsync: chatResponseList is empty'); return; } log('last list item id ${chatResponseList.first.toJson()}'); chatResponseList.first.isSeen = true; notifyListeners(); } catch (e, stackTrace) { log('onAckSeenAsync error: $e'); log('StackTrace: $stackTrace'); } } Future onSubmitChatAsync(List? parameters) async { if (kDebugMode) print("OnSubmitChatAsync:$parameters"); if (parameters == null || parameters.isEmpty) return; try { for (dynamic msg in parameters) { var data = getSingleUserChatModel(jsonEncode(msg)); if (kDebugMode) print('Parsed message: $data'); } } catch (e) { if (kDebugMode) print('Error in OnSubmitChatAsync: $e'); } } Future onMsgReceived1(List? parameters) async { print("onMsgReceived1:$parameters"); } Future onMsgReceived(List? parameters) async { List data = []; print("OnMessageReceivedAsync:$parameters"); for (dynamic msg in parameters!) { data = getSingleUserChatModel(jsonEncode(msg)); // ...existing code... } // ...existing code... userChatHistory?.insert(0, data.first); notifyListeners(); // ...existing code... } // ==================== CALL INFRASTRUCTURE ==================== // All call-related methods are placed at the end to avoid modifying existing chat logic /// Start a new audio or video call Future startCall(Participants recipient, CallType callType) async { log('๐Ÿ”ต [CALL] startCall() invoked', name: 'ChatProvider'); log('๐Ÿ”ต [CALL] Current callStatus: $callStatus', name: 'ChatProvider'); // Get context for error dialogs final context = navigatorKey.currentContext; if (context == null) { log('โŒ [CALL] No context available', name: 'ChatProvider'); return; } // Check if already in a call if (callStatus != CallStatus.idle) { log('โš ๏ธ [CALL] Cannot start call - already in a call (status: $callStatus)', name: 'ChatProvider'); CallErrorHandler.showCallAlreadyInProgress(context); return; } if (sender == null) { log('โŒ [CALL] Cannot start call - sender is null', name: 'ChatProvider'); CallErrorHandler.showGenericError(context, 'Unable to start call. Please try again.'); return; } log('๐Ÿ”ต [CALL] Sender: ${sender!.employeeNumber}', name: 'ChatProvider'); callStatus = CallStatus.checkingPermissions; notifyListeners(); log('๐Ÿ”ต [CALL] Status changed to: checkingPermissions', name: 'ChatProvider'); try { // Generate unique call ID final callId = const Uuid().v4(); final isVideo = callType == CallType.video; log('๐Ÿ”ต [CALL] Generated callId: $callId', name: 'ChatProvider'); log('๐Ÿ”ต [CALL] Call type: ${isVideo ? "VIDEO" : "AUDIO"}', name: 'ChatProvider'); // Request permissions log('๐Ÿ”ต [CALL] Requesting microphone permission...', name: 'ChatProvider'); final micStatus = await Permission.microphone.request(); log('๐Ÿ”ต [CALL] Microphone permission status: ${micStatus.name}', name: 'ChatProvider'); if (!micStatus.isGranted) { callStatus = CallStatus.idle; currentCall = null; notifyListeners(); log('โŒ [CALL] Microphone permission denied - aborting call', name: 'ChatProvider'); CallErrorHandler.showMicrophonePermissionDenied(context); return; } if (isVideo) { log('๐Ÿ”ต [CALL] Requesting camera permission...', name: 'ChatProvider'); final cameraStatus = await Permission.camera.request(); log('๐Ÿ”ต [CALL] Camera permission status: ${cameraStatus.name}', name: 'ChatProvider'); if (!cameraStatus.isGranted) { callStatus = CallStatus.idle; currentCall = null; notifyListeners(); log('โŒ [CALL] Camera permission denied - aborting call', name: 'ChatProvider'); CallErrorHandler.showCameraPermissionDenied(context); return; } } // Check SignalR connection before proceeding if (chatHubConnection == null || chatHubConnection!.state != HubConnectionState.Connected) { callStatus = CallStatus.idle; currentCall = null; notifyListeners(); log('โŒ [CALL] SignalR not connected', name: 'ChatProvider'); CallErrorHandler.showSignalRNotConnected(context); return; } // Create call session log('๐Ÿ”ต [CALL] Creating CallSession...', name: 'ChatProvider'); currentCall = CallSession( callId: callId, type: callType, direction: CallDirection.outgoing, peerId: recipient.employeeNumber ?? '', peerName: recipient.userName ?? 'Unknown', peerAvatar: recipient.image, startTime: DateTime.now(), ); log('โœ… [CALL] CallSession created - peerId: ${currentCall!.peerId}, peerName: ${currentCall!.peerName}', name: 'ChatProvider'); // Invoke SignalR CallUserAsync log('๐Ÿ”ต [CALL] Invoking CallUserAsync with args:', name: 'ChatProvider'); log(' - source: ${sender!.employeeNumber}', name: 'ChatProvider'); log(' - target: ${recipient.employeeNumber}', name: 'ChatProvider'); log(' - isVideoCall: $isVideo', name: 'ChatProvider'); try { await chatHubConnection?.invoke( 'CallUserAsync', args: [ sender!.employeeNumber ?? '', recipient.employeeNumber ?? '', isVideo, ], ); log('โœ… [CALL] CallUserAsync invoked successfully', name: 'ChatProvider'); } catch (e) { log('โŒ [CALL] Failed to invoke CallUserAsync: $e', name: 'ChatProvider'); callStatus = CallStatus.idle; currentCall = null; notifyListeners(); CallErrorHandler.showGenericError(context, 'Failed to start call. Please check your connection and try again.'); return; } callStatus = CallStatus.outgoingRinging; notifyListeners(); log('๐Ÿ”ต [CALL] Status changed to: outgoingRinging', name: 'ChatProvider'); // TODO: Play outgoing ringtone (commented for now) // await _playOutgoingRingtone(); // Initialize WebRTC now but DON'T send offer yet - wait for call to be accepted log('๐Ÿ”ง [CALL] Initializing WebRTC (offer will be sent after accept)...', name: 'ChatProvider'); try { _webrtcService = WebRTCService(); _setupWebRTCCallbacks(); if (callType == CallType.audio) { await _webrtcService!.initializeForAudioCall(); } else { await _webrtcService!.initializeForVideoCall(); // Use video initialization for video calls } log('โœ… [CALL] WebRTC initialized, waiting for peer to accept...', name: 'ChatProvider'); } catch (e, stackTrace) { log('โŒ [CALL] WebRTC initialization failed: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); // Cleanup and notify peer await chatHubConnection?.invoke('HangUpAsync', args: [ sender!.employeeNumber ?? '', recipient.employeeNumber ?? '', moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ]).catchError((err) { log('โŒ [CALL] Failed to send hangup after WebRTC init failure: $err', name: 'ChatProvider'); }); callStatus = CallStatus.idle; currentCall = null; notifyListeners(); CallErrorHandler.showWebRTCInitFailed(context); return; } // Navigate to call screen if (context != null && context.mounted) { log('๐Ÿ”ต [CALL] Navigating to call screen...', name: 'ChatProvider'); // Double check currentCall is still valid before navigation if (currentCall == null) { log('โŒ [CALL] Cannot navigate - currentCall is null', name: 'ChatProvider'); return; } Navigator.of(context).push( MaterialPageRoute( builder: (context) => currentCall!.type == CallType.audio ? const AudioCallPage() : const VideoCallPage(), ), ).then((_) { log('โœ… [CALL] Navigation completed', name: 'ChatProvider'); }).catchError((e) { log('โŒ [CALL] Navigation error: $e', name: 'ChatProvider'); }); log('โœ… [CALL] Navigated to call screen', name: 'ChatProvider'); } else { log('โš ๏ธ [CALL] No context for navigation - will try alternative approach', name: 'ChatProvider'); // Alternative: Use WidgetsBinding to schedule navigation after frame WidgetsBinding.instance.addPostFrameCallback((_) { // Wait for app to fully come to foreground Future.delayed(const Duration(milliseconds: 500), () { final ctx = navigatorKey.currentContext; if (ctx != null && ctx.mounted) { log('๐Ÿ”ต [CALL] Navigating via post-frame callback...', name: 'ChatProvider'); Navigator.of(ctx).push( MaterialPageRoute( builder: (context) => currentCall!.type == CallType.audio ? const AudioCallPage() : const VideoCallPage(), ), ).then((_) { log('โœ… [CALL] Post-frame navigation completed', name: 'ChatProvider'); }).catchError((e) { log('โŒ [CALL] Post-frame navigation error: $e', name: 'ChatProvider'); }); } else { log('โŒ [CALL] Still no context available - navigation failed', name: 'ChatProvider'); // Last resort: dismiss CallKit and clean up _callKitService.endCall(currentCall!.callId); _teardownCall(); } }); }); } // Wait for caller to send SDP offer via OnOfferAsync event log('โณ [CALL] Waiting for SDP offer from caller...', name: 'ChatProvider'); } catch (e, stackTrace) { log('โŒ [CALL] Error starting call: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); if (kDebugMode) { print('โŒ Error starting call: $e'); } callStatus = CallStatus.idle; currentCall = null; notifyListeners(); // Show appropriate error dialog if (context.mounted) { if (e.toString().contains('WebRTC') || e.toString().contains('getUserMedia')) { CallErrorHandler.showWebRTCInitFailed(context); } else if (e.toString().contains('SignalR') || e.toString().contains('connection')) { CallErrorHandler.showSignalRNotConnected(context); } else { CallErrorHandler.showGenericError(context, 'Failed to start call. Please try again.'); } } } } /// Play outgoing ringtone Future _playOutgoingRingtone() async { if (_isRingingPlaying) { log('โš ๏ธ [RINGTONE] Already playing', name: 'ChatProvider'); return; } try { log('๐Ÿ”” [RINGTONE] Starting outgoing ringtone...', name: 'ChatProvider'); await _ringingPlayer.setAsset('assets/audio/outgoing_ringtone.mp3'); await _ringingPlayer.setLoopMode(LoopMode.one); // Loop the ringtone await _ringingPlayer.play(); _isRingingPlaying = true; log('โœ… [RINGTONE] Outgoing ringtone playing', name: 'ChatProvider'); if (kDebugMode) { print('๐Ÿ”” Outgoing ringtone playing'); } } catch (e) { log('โŒ [RINGTONE] Error playing ringtone: $e', name: 'ChatProvider'); if (kDebugMode) { print('โŒ Error playing ringtone: $e'); } } } /// Stop outgoing ringtone Future _stopOutgoingRingtone() async { if (!_isRingingPlaying) { return; } try { log('๐Ÿ”• [RINGTONE] Stopping outgoing ringtone...', name: 'ChatProvider'); await _ringingPlayer.stop(); await _ringingPlayer.pause(); await _ringingPlayer.seek(Duration.zero); _isRingingPlaying = false; log('โœ… [RINGTONE] Ringtone stopped', name: 'ChatProvider'); if (kDebugMode) { print('๐Ÿ”• Ringtone stopped'); } } catch (e) { log('โš ๏ธ [RINGTONE] Error stopping ringtone: $e', name: 'ChatProvider'); // Force stop even on error _isRingingPlaying = false; } } /// Toggle microphone mute/unmute Future toggleMute() async { log('๐ŸŽค [CALL CONTROL] toggleMute() called', name: 'ChatProvider'); log('๐ŸŽค [CALL CONTROL] Current mute state: $isMuted', name: 'ChatProvider'); if (_webrtcService == null) { log('โš ๏ธ [CALL CONTROL] WebRTC service is null', name: 'ChatProvider'); return; } // Toggle local mute state isMuted = !isMuted; notifyListeners(); log('โœ… [CALL CONTROL] Mute state changed to: $isMuted', name: 'ChatProvider'); // Update WebRTC audio track _webrtcService!.setMicrophoneMuted(isMuted); // Notify peer via SignalR if (chatHubConnection?.state == HubConnectionState.Connected && sender != null && currentCall != null) { try { log('๐Ÿ”” [CALL CONTROL] Notifying peer of mute toggle...', name: 'ChatProvider'); await chatHubConnection!.invoke( 'AudioToggle', args: [sender!.employeeNumber ?? '', currentCall!.peerId], ); log('โœ… [CALL CONTROL] Peer notified of mute toggle', name: 'ChatProvider'); } catch (e) { log('โŒ [CALL CONTROL] Error notifying peer of mute: $e', name: 'ChatProvider'); } } if (kDebugMode) { print('๐ŸŽค Microphone ${isMuted ? "muted" : "unmuted"}'); } } /// Toggle speakerphone on/off Future toggleSpeaker() async { log('๐Ÿ”Š [CALL CONTROL] toggleSpeaker() called', name: 'ChatProvider'); log('๐Ÿ”Š [CALL CONTROL] Current speaker state: $isSpeakerOn', name: 'ChatProvider'); if (_webrtcService == null) { log('โš ๏ธ [CALL CONTROL] WebRTC service is null', name: 'ChatProvider'); return; } // Toggle speaker state isSpeakerOn = !isSpeakerOn; notifyListeners(); log('โœ… [CALL CONTROL] Speaker state changed to: $isSpeakerOn', name: 'ChatProvider'); // Update audio output await _webrtcService!.setSpeakerphoneEnabled(isSpeakerOn); if (kDebugMode) { print('๐Ÿ”Š Speakerphone ${isSpeakerOn ? "on" : "off"}'); } } /// Toggle camera on/off (video calls only) Future toggleCamera() async { log('๐Ÿ“น [CALL CONTROL] toggleCamera() called', name: 'ChatProvider'); log('๐Ÿ“น [CALL CONTROL] Current camera state: $isCameraOn', name: 'ChatProvider'); if (_webrtcService == null) { log('โš ๏ธ [CALL CONTROL] WebRTC service is null', name: 'ChatProvider'); return; } if (currentCall?.type != CallType.video) { log('โš ๏ธ [CALL CONTROL] Not a video call', name: 'ChatProvider'); return; } // Toggle local camera state isCameraOn = !isCameraOn; notifyListeners(); log('โœ… [CALL CONTROL] Camera state changed to: $isCameraOn', name: 'ChatProvider'); // Update WebRTC video track _webrtcService!.setCameraEnabled(isCameraOn); // Notify peer via SignalR if (chatHubConnection?.state == HubConnectionState.Connected && sender != null && currentCall != null) { try { log('๐Ÿ”” [CALL CONTROL] Notifying peer of camera toggle...', name: 'ChatProvider'); await chatHubConnection!.invoke( 'CameraToggle', args: [sender!.employeeNumber ?? '', currentCall!.peerId], ); log('โœ… [CALL CONTROL] Peer notified of camera toggle', name: 'ChatProvider'); } catch (e) { log('โŒ [CALL CONTROL] Error notifying peer of camera toggle: $e', name: 'ChatProvider'); } } if (kDebugMode) { print('๐Ÿ“น Camera ${isCameraOn ? "on" : "off"}'); } } /// Switch between front and rear camera (video calls only) Future switchCamera() async { log('๐Ÿ”„ [CALL CONTROL] switchCamera() called', name: 'ChatProvider'); if (_webrtcService == null) { log('โš ๏ธ [CALL CONTROL] WebRTC service is null', name: 'ChatProvider'); return; } if (currentCall?.type != CallType.video) { log('โš ๏ธ [CALL CONTROL] Not a video call', name: 'ChatProvider'); return; } if (!isCameraOn) { log('โš ๏ธ [CALL CONTROL] Camera is off, cannot switch', name: 'ChatProvider'); return; } try { await _webrtcService!.switchCamera(); log('โœ… [CALL CONTROL] Camera switched', name: 'ChatProvider'); if (kDebugMode) { print('๐Ÿ”„ Camera switched'); } } catch (e) { log('โŒ [CALL CONTROL] Error switching camera: $e', name: 'ChatProvider'); if (kDebugMode) { print('โŒ Error switching camera: $e'); } } } /// End the current call (hang up) Future hangUp() async { log('๐Ÿ“ž [CALL CONTROL] hangUp() called', name: 'ChatProvider'); log('๐Ÿ“ž [CALL CONTROL] Current status: $callStatus', name: 'ChatProvider'); log('๐Ÿ“ž [CALL CONTROL] Current call: ${currentCall?.callId}', name: 'ChatProvider'); if (currentCall == null) { log('โš ๏ธ [CALL CONTROL] No active call to hang up', name: 'ChatProvider'); return; } // End CallKit UI first try { await _callKitService.endCall(currentCall!.callId); log('โœ… [CallKit] Native call UI ended', name: 'ChatProvider'); } catch (e) { log('โš ๏ธ [CallKit] Error ending native UI: $e', name: 'ChatProvider'); } // Stop ringtone if playing await _stopOutgoingRingtone(); try { // Invoke SignalR HangUpAsync if (chatHubConnection?.state == HubConnectionState.Connected && sender != null) { log('๐Ÿ”” [CALL CONTROL] Invoking HangUpAsync with args:', name: 'ChatProvider'); log(' - source: ${sender!.employeeNumber}', name: 'ChatProvider'); log(' - target: ${currentCall!.peerId}', name: 'ChatProvider'); log(' - moduleCode: $moduleID', name: 'ChatProvider'); log(' - referenceId: $referenceID', name: 'ChatProvider'); log(' - conversationId: ${chatParticipantModel?.id}', name: 'ChatProvider'); await chatHubConnection!.invoke( 'HangUpAsync', args: [ sender!.employeeNumber ?? '', currentCall!.peerId, moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], ); log('โœ… [CALL CONTROL] HangUpAsync invoked successfully', name: 'ChatProvider'); } } catch (e, stackTrace) { log('โŒ [CALL CONTROL] Error invoking HangUpAsync: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); if (kDebugMode) { print('โš ๏ธ Error hanging up call: $e'); } } // Teardown call resources _teardownCall(); if (kDebugMode) { print('๐Ÿ“ž Call ended'); } } /// Handle call timeout (60s no answer) void _handleCallTimeout() { log('โฑ๏ธ [CALL] _handleCallTimeout() - Call timeout after 60s', name: 'ChatProvider'); log('๐Ÿ”ต [CALL] Current status: $callStatus', name: 'ChatProvider'); if (kDebugMode) { print('โฑ๏ธ Call timeout - no answer'); } // Invoke CallMissedAsync log('๐Ÿ”ต [CALL] Invoking CallMissedAsync with args:', name: 'ChatProvider'); log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider'); log(' - target: ${currentCall?.peerId}', name: 'ChatProvider'); log(' - moduleCode: $moduleID', name: 'ChatProvider'); log(' - referenceId: ${referenceID?.toString() ?? "null"}', name: 'ChatProvider'); log(' - conversationId: ${chatParticipantModel?.id?.toString() ?? "null"}', name: 'ChatProvider'); chatHubConnection?.invoke( 'CallMissedAsync', args: [ sender?.employeeNumber ?? '', currentCall?.peerId ?? '', moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], ).then((_) { log('โœ… [CALL] CallMissedAsync invoked successfully', name: 'ChatProvider'); }).catchError((e) { log('โŒ [CALL] Error invoking CallMissedAsync: $e', name: 'ChatProvider'); }); _teardownCall(); } /// Clean up call resources void _teardownCall() { log('๐Ÿงน [CALL] _teardownCall() - Cleaning up call resources', name: 'ChatProvider'); log('๐Ÿ”ต [CALL] Previous status: $callStatus', name: 'ChatProvider'); log('๐Ÿ”ต [CALL] Call duration: ${callDuration.inSeconds}s', name: 'ChatProvider'); // End CallKit UI if we have an active call if (currentCall != null) { try { _callKitService.endCall(currentCall!.callId); log('โœ… [CallKit] Native call UI ended in teardown', name: 'ChatProvider'); } catch (e) { log('โš ๏ธ [CallKit] Error ending native UI in teardown: $e', name: 'ChatProvider'); } } // Stop outgoing ringtone if playing _stopOutgoingRingtone(); _callTimeoutTimer?.cancel(); log('๐Ÿ”ต [CALL] Timeout timer cancelled', name: 'ChatProvider'); _callDurationTimer?.cancel(); log('๐Ÿ”ต [CALL] Duration timer cancelled', name: 'ChatProvider'); // Dispose WebRTC service to release media resources if (_webrtcService != null) { log('๐Ÿ”ง [CALL] Disposing WebRTC service...', name: 'ChatProvider'); _webrtcService!.dispose().then((_) { log('โœ… [CALL] WebRTC service disposed', name: 'ChatProvider'); }).catchError((e) { log('โš ๏ธ [CALL] Error disposing WebRTC service: $e', name: 'ChatProvider'); }); _webrtcService = null; } callStatus = CallStatus.idle; currentCall = null; callDuration = Duration.zero; isMuted = false; isSpeakerOn = false; isCameraOn = true; isPeerMuted = false; isPeerCameraOn = true; _remoteMediaStream = null; notifyListeners(); log('โœ… [CALL] Call resources cleaned up - status reset to idle', name: 'ChatProvider'); if (kDebugMode) { print('๐Ÿงน Call resources cleaned up'); } } /// Start call duration timer when connected void _startCallDurationTimer() { log('โฑ๏ธ [CALL] _startCallDurationTimer() - Starting call duration counter', name: 'ChatProvider'); _callDurationTimer?.cancel(); callDuration = Duration.zero; _callDurationTimer = Timer.periodic(const Duration(seconds: 1), (timer) { callDuration = Duration(seconds: callDuration.inSeconds + 1); notifyListeners(); }); log('โœ… [CALL] Duration timer started', name: 'ChatProvider'); if (kDebugMode) { print('โฑ๏ธ Call duration timer started'); } } /// Accept incoming call Future acceptCall() async { log('๐Ÿ”ต [CALL] acceptCall() called', name: 'ChatProvider'); log('๐Ÿ”ต [CALL] Current call: ${currentCall?.callId}', name: 'ChatProvider'); log('๐Ÿ”ต [CALL] Current status: $callStatus', name: 'ChatProvider'); if (currentCall == null || callStatus != CallStatus.incomingRinging) { log('โš ๏ธ [CALL] Cannot accept - no incoming call or wrong status', name: 'ChatProvider'); return; } // IMPORTANT: Dismiss CallKit UI first try { await _callKitService.setCallConnected(currentCall!.callId); log('โœ… [CallKit] Native UI updated to connected state', name: 'ChatProvider'); } catch (e) { log('โš ๏ธ [CallKit] Error updating to connected state: $e', name: 'ChatProvider'); } // Get context - we'll retry if null BuildContext? context = navigatorKey.currentContext; try { // Check microphone permission log('๐Ÿ”ต [CALL] Checking microphone permission...', name: 'ChatProvider'); final micStatus = await Permission.microphone.request(); if (!micStatus.isGranted) { log('โŒ [CALL] Microphone permission denied - declining call', name: 'ChatProvider'); await declineCall('permission_denied'); if (context != null && context.mounted) { CallErrorHandler.showMicrophonePermissionDenied(context); } return; } // Check camera permission for video calls if (currentCall!.type == CallType.video) { log('๐Ÿ”ต [CALL] Checking camera permission...', name: 'ChatProvider'); final cameraStatus = await Permission.camera.request(); if (!cameraStatus.isGranted) { log('โŒ [CALL] Camera permission denied - declining call', name: 'ChatProvider'); await declineCall('permission_denied'); if (context != null && context.mounted) { CallErrorHandler.showCameraPermissionDenied(context); } return; } } // Cancel timeout timer _callTimeoutTimer?.cancel(); // Update status to connecting callStatus = CallStatus.connecting; notifyListeners(); log('๐Ÿ”ต [CALL] Status changed to: connecting', name: 'ChatProvider'); // Invoke AnswerCallAsync on SignalR FIRST if (chatHubConnection?.state != HubConnectionState.Connected) { log('โŒ [CALL] SignalR not connected', name: 'ChatProvider'); _teardownCall(); if (context != null && context.mounted) { CallErrorHandler.showSignalRNotConnected(context); } return; } final myEmployeeNumber = sender?.employeeNumber; if (myEmployeeNumber == null || myEmployeeNumber.isEmpty) { log('โŒ [CALL] No employee number found', name: 'ChatProvider'); _teardownCall(); if (context != null && context.mounted) { CallErrorHandler.showGenericError(context, 'Unable to accept call. Please try again.'); } return; } log('๐Ÿ”ต [CALL] Invoking AnswerCallAsync with args:', name: 'ChatProvider'); log(' - source: $myEmployeeNumber', name: 'ChatProvider'); log(' - target: ${currentCall!.peerId}', name: 'ChatProvider'); try { await chatHubConnection!.invoke( 'AnswerCallAsync', args: [ myEmployeeNumber, currentCall!.peerId, moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], ); log('โœ… [CALL] AnswerCallAsync invoked successfully', name: 'ChatProvider'); } catch (e) { log('โŒ [CALL] Failed to invoke AnswerCallAsync: $e', name: 'ChatProvider'); _teardownCall(); if (context != null && context.mounted) { CallErrorHandler.showGenericError(context, 'Failed to accept call. Please try again.'); } return; } // Initialize WebRTC and setup callbacks log('๐Ÿ”ง [CALL] Initializing WebRTC for incoming call...', name: 'ChatProvider'); try { _webrtcService = WebRTCService(); _setupWebRTCCallbacks(); if (currentCall!.type == CallType.audio) { await _webrtcService!.initializeForAudioCall(); } else { await _webrtcService!.initializeForVideoCall(); } log('โœ… [CALL] WebRTC initialized', name: 'ChatProvider'); } catch (e, stackTrace) { log('โŒ [CALL] WebRTC initialization failed: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); // Cleanup and notify peer await chatHubConnection?.invoke('HangUpAsync', args: [ myEmployeeNumber, currentCall!.peerId, moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ]).catchError((err) { log('โŒ [CALL] Failed to send hangup after WebRTC init failure: $err', name: 'ChatProvider'); }); _teardownCall(); // Retry getting context context = navigatorKey.currentContext; if (context != null && context.mounted) { CallErrorHandler.showWebRTCInitFailed(context); } return; } // Wait a moment for the app to come to foreground if needed await Future.delayed(const Duration(milliseconds: 500)); // Retry getting context after delay context = navigatorKey.currentContext; // Navigate to call screen if (context != null && context.mounted) { log('๐Ÿ”ต [CALL] Navigating to call screen...', name: 'ChatProvider'); // Double check currentCall is still valid before navigation if (currentCall == null) { log('โŒ [CALL] Cannot navigate - currentCall is null', name: 'ChatProvider'); return; } Navigator.of(context).push( MaterialPageRoute( builder: (context) => currentCall!.type == CallType.audio ? const AudioCallPage() : const VideoCallPage(), ), ).then((_) { log('โœ… [CALL] Navigation completed', name: 'ChatProvider'); }).catchError((e) { log('โŒ [CALL] Navigation error: $e', name: 'ChatProvider'); }); log('โœ… [CALL] Navigated to call screen', name: 'ChatProvider'); } else { log('โš ๏ธ [CALL] No context for navigation - will try alternative approach', name: 'ChatProvider'); // Alternative: Use WidgetsBinding to schedule navigation after frame WidgetsBinding.instance.addPostFrameCallback((_) { // Wait for app to fully come to foreground Future.delayed(const Duration(milliseconds: 500), () { final ctx = navigatorKey.currentContext; if (ctx != null && ctx.mounted) { log('๐Ÿ”ต [CALL] Navigating via post-frame callback...', name: 'ChatProvider'); Navigator.of(ctx).push( MaterialPageRoute( builder: (context) => currentCall!.type == CallType.audio ? const AudioCallPage() : const VideoCallPage(), ), ).then((_) { log('โœ… [CALL] Post-frame navigation completed', name: 'ChatProvider'); }).catchError((e) { log('โŒ [CALL] Post-frame navigation error: $e', name: 'ChatProvider'); }); } else { log('โŒ [CALL] Still no context available - navigation failed', name: 'ChatProvider'); // Last resort: dismiss CallKit and clean up _callKitService.endCall(currentCall!.callId); _teardownCall(); } }); }); } // Wait for caller to send SDP offer via OnOfferAsync event log('โณ [CALL] Waiting for SDP offer from caller...', name: 'ChatProvider'); } catch (e, stackTrace) { log('โŒ [CALL] Error accepting call: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); if (kDebugMode) { print('โš ๏ธ Error accepting call: $e'); } callStatus = CallStatus.idle; currentCall = null; notifyListeners(); context = navigatorKey.currentContext; if (context != null && context.mounted) { CallErrorHandler.showGenericError(context, 'Failed to accept call. Please try again.'); } } } /// Decline an incoming call Future declineCall(String reason) async { log('๐Ÿ”ด [CALL] declineCall() called - reason: $reason', name: 'ChatProvider'); log('๐Ÿ”ด [CALL] Current call: ${currentCall?.callId}', name: 'ChatProvider'); log('๐Ÿ”ด [CALL] Current status: $callStatus', name: 'ChatProvider'); if (currentCall == null || callStatus != CallStatus.incomingRinging) { log('โš ๏ธ [CALL] Cannot decline - no incoming call or wrong status', name: 'ChatProvider'); return; } try { // Invoke SignalR CallDeclinedAsync log('๐Ÿ”ด [CALL] Invoking CallDeclinedAsync with args:', name: 'ChatProvider'); log(' - source: ${sender?.employeeNumber}', name: 'ChatProvider'); log(' - target: ${currentCall!.peerId}', name: 'ChatProvider'); log(' - moduleCode: $moduleID', name: 'ChatProvider'); log(' - referenceId: $referenceID', name: 'ChatProvider'); log(' - conversationId: ${chatParticipantModel?.id}', name: 'ChatProvider'); await chatHubConnection?.invoke( 'CallDeclinedAsync', args: [ sender?.employeeNumber ?? '', currentCall!.peerId, moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], ); log('โœ… [CALL] CallDeclinedAsync invoked successfully', name: 'ChatProvider'); await _stopOutgoingRingtone(); } catch (e, stackTrace) { log('โŒ [CALL] Error invoking CallDeclinedAsync: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); if (kDebugMode) { print('โš ๏ธ Error declining call: $e'); } } _teardownCall(); } /// Register all call event handlers from SignalR void _registerCallHandlers() { if (chatHubConnection == null) { log('โš ๏ธ [CALL] Cannot register call handlers - connection is null', name: 'ChatProvider'); return; } log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•', name: 'ChatProvider'); log('๐Ÿ“ž [CALL] Starting call handler registration...', name: 'ChatProvider'); chatHubConnection!.on('OnIncomingCallAsync', _handleIncomingCall); chatHubConnection!.on('OnCallAcceptedAsync', _handleCallAccepted); chatHubConnection!.on('OnCallDeclinedAsync', _handleCallDeclined); chatHubConnection!.on('OnHangUpAsync', _handleHangUp); chatHubConnection!.on('OnOfferAsync', _handleOffer); chatHubConnection!.on('OnAnswerOfferAsync', _handleAnswer); chatHubConnection!.on('OnIceCandidateAsync', _handleIceCandidate); chatHubConnection!.on('OnAudioToggle', _handleAudioToggle); chatHubConnection!.on('OnCameraToggle', _handleCameraToggle); _callHandlersRegistered = true; notifyListeners(); log('โœ… [CALL] All call handlers registered successfully', name: 'ChatProvider'); log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•', name: 'ChatProvider'); if (kDebugMode) { print('โœ… Call handlers registered'); } } /// Setup WebRTC callbacks void _setupWebRTCCallbacks() { if (_webrtcService == null) { log('โš ๏ธ [CALL] Cannot setup callbacks - WebRTC service is null', name: 'ChatProvider'); return; } log('๐Ÿ”ง [CALL] Setting up WebRTC callbacks...', name: 'ChatProvider'); _webrtcService!.onIceCandidate = (RTCIceCandidate candidate) { log('๐ŸงŠ [CALL] Local ICE candidate generated', name: 'ChatProvider'); if (chatHubConnection?.state == HubConnectionState.Connected && sender != null && currentCall != null) { final candidateJson = jsonEncode({ 'candidate': candidate.candidate, 'sdpMid': candidate.sdpMid, 'sdpMLineIndex': candidate.sdpMLineIndex, }); chatHubConnection!.invoke( 'IceCandidateAsync', args: [currentCall!.peerId, candidateJson, currentCall!.sessionId ?? ''], ); } }; _webrtcService!.onRemoteStream = (MediaStream stream) { log('๐Ÿ“ก [CALL] Remote stream received in ChatProvider callback', name: 'ChatProvider'); log('๐Ÿ“ก [CALL] Remote stream has ${stream.getVideoTracks().length} video tracks', name: 'ChatProvider'); log('๐Ÿ“ก [CALL] Remote stream has ${stream.getAudioTracks().length} audio tracks', name: 'ChatProvider'); _remoteMediaStream = stream; // Notify listeners to update UI when remote stream is received notifyListeners(); log('โœ… [CALL] UI notified about remote stream', name: 'ChatProvider'); }; _webrtcService!.onIceConnectionStateChange = (RTCIceConnectionState state) { log('๐Ÿ”— [CALL] ICE state: ${state.toString()}', name: 'ChatProvider'); if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) { _stopOutgoingRingtone(); // Cancel connection timeout since we're now connected _callTimeoutTimer?.cancel(); if (callStatus != CallStatus.connected) { callStatus = CallStatus.connected; notifyListeners(); _startCallDurationTimer(); } } else if (state == RTCIceConnectionState.RTCIceConnectionStateChecking) { log('๐Ÿ” [CALL] ICE checking - establishing connection...', name: 'ChatProvider'); // Start a longer timeout for connection establishment (30 seconds) _startConnectionTimeout(); } else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) { log('โŒ [CALL] ICE connection failed - ending call', name: 'ChatProvider'); // Connection failed, terminate the call _handleConnectionFailure(); } else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) { log('โš ๏ธ [CALL] ICE connection disconnected - waiting for reconnection', name: 'ChatProvider'); // Start a timeout to end call if it doesn't reconnect within 10 seconds _startReconnectionTimeout(); } }; log('โœ… [CALL] WebRTC callbacks setup complete', name: 'ChatProvider'); } /// Create and send SDP offer Future _createAndSendOffer() async { try { log('๐Ÿ”ง [CALL] Creating SDP offer...', name: 'ChatProvider'); if (_webrtcService == null || currentCall == null) { throw Exception('WebRTC service or call session is null'); } final offer = await _webrtcService!.createOffer(); log('โœ… [CALL] SDP offer created', name: 'ChatProvider'); await chatHubConnection!.invoke( 'OfferAsync', args: [currentCall!.peerId, offer.sdp ?? '', currentCall!.callId], ); log('โœ… [CALL] SDP offer sent', name: 'ChatProvider'); } catch (e, stackTrace) { log('โŒ [CALL] Error creating/sending offer: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); _teardownCall(); } } // ==================== CALL EVENT HANDLERS ==================== void _handleIncomingCall(List? args) async { log('๐Ÿ“ž [CALL EVENT] OnIncomingCallAsync received', name: 'ChatProvider'); if (args == null || args.isEmpty) return; try { final callData = args[0] as Map; final callerId = callData['sourceUserId'] as String?; final callerName = callData['userName'] as String? ?? 'Unknown'; final isVideoCall = callData['isVideoCall'] as bool? ?? false; final sdpOffer = callData['sdpOffer'] as String?; // Check if already in a call - decline if busy if (callStatus != CallStatus.idle) { log('โš ๏ธ [CALL] Already in a call, declining incoming call', name: 'ChatProvider'); chatHubConnection?.invoke('CallDeclinedAsync', args: [ sender?.employeeNumber ?? '', callerId ?? '', moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ]); return; } // Generate unique call ID final callId = const Uuid().v4(); // Create call session currentCall = CallSession( callId: callId, type: isVideoCall ? CallType.video : CallType.audio, direction: CallDirection.incoming, peerId: callerId ?? '', peerName: callerName, peerAvatar: null, startTime: DateTime.now(), sdpOffer: sdpOffer, ); callStatus = CallStatus.incomingRinging; notifyListeners(); log('๐Ÿ“ž [CallKit] Showing native incoming call UI...', name: 'ChatProvider'); log(' Caller: $callerName', name: 'ChatProvider'); log(' Caller ID: $callerId', name: 'ChatProvider'); log(' Video: $isVideoCall', name: 'ChatProvider'); log(' Call ID: $callId', name: 'ChatProvider'); // Initialize CallKit if not already initialized if (!_callKitInitialized) { await _initializeCallKit(); } // Show native incoming call UI using CallKit try { await _callKitService.showIncomingCall( callId: callId, callerName: callerName, callerNumber: callerId ?? '', callerAvatar: null, // TODO: Get avatar from participant data if available isVideo: isVideoCall, extra: { 'peerId': callerId ?? '', 'moduleId': moduleID.toString(), 'referenceId': referenceID?.toString() ?? '', 'conversationId': chatParticipantModel?.id?.toString() ?? '', }, ); log('โœ… [CallKit] Native incoming call UI displayed', name: 'ChatProvider'); } catch (e, stackTrace) { log('โŒ [CallKit] Error showing native UI, falling back to dialog: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); // Fallback to custom dialog if CallKit fails final context = navigatorKey.currentContext; if (context != null) { showDialog( context: context, barrierDismissible: false, builder: (context) => IncomingCallDialog(call: currentCall!), ); } } // Start call timeout timer (30 seconds for incoming calls) _callTimeoutTimer?.cancel(); _callTimeoutTimer = Timer(const Duration(seconds: 30), () { if (callStatus == CallStatus.incomingRinging) { log('โฑ๏ธ [CALL] Incoming call timeout - no answer after 30s', name: 'ChatProvider'); // End CallKit UI _callKitService.endCall(callId); // Cleanup _teardownCall(); } }); log('โฑ๏ธ [CALL] Incoming call timeout started (30s)', name: 'ChatProvider'); } catch (e, stackTrace) { log('โŒ [CALL EVENT] Error handling incoming call: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); // Cleanup on error callStatus = CallStatus.idle; currentCall = null; notifyListeners(); } } void _handleCallAccepted(List? args) { log('๐Ÿ“ž [CALL EVENT] OnCallAcceptedAsync received', name: 'ChatProvider'); if (callStatus != CallStatus.outgoingRinging) return; _callTimeoutTimer?.cancel(); callStatus = CallStatus.connecting; notifyListeners(); _createAndSendOffer(); } void _handleCallDeclined(List? args) { log('๐Ÿ“ž [CALL EVENT] OnCallDeclinedAsync received', name: 'ChatProvider'); _teardownCall(); } void _handleHangUp(List? args) { log('๐Ÿ“ž [CALL EVENT] OnHangUpAsync received', name: 'ChatProvider'); _teardownCall(); } void _handleOffer(List? args) async { log('๐Ÿ“ž [CALL EVENT] OnOfferAsync received', name: 'ChatProvider'); if (args == null || args.isEmpty) return; try { final offerSdp = args[0] as String?; if (offerSdp == null || _webrtcService == null) return; final answer = await _webrtcService!.createAnswer(offerSdp); await chatHubConnection!.invoke( 'AnswerOfferAsync', args: [currentCall!.peerId, answer.sdp ?? '', currentCall!.callId], ); log('โœ… [CALL EVENT] SDP answer sent', name: 'ChatProvider'); } catch (e, stackTrace) { log('โŒ [CALL EVENT] Error handling offer: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); } } void _handleAnswer(List? args) async { log('๐Ÿ“ž [CALL EVENT] OnAnswerOfferAsync received', name: 'ChatProvider'); if (args == null || args.isEmpty) return; try { final answerSdp = args[0] as String?; if (answerSdp == null || _webrtcService == null) return; await _webrtcService!.setRemoteAnswer(answerSdp); log('โœ… [CALL EVENT] Remote answer set', name: 'ChatProvider'); } catch (e, stackTrace) { log('โŒ [CALL EVENT] Error handling answer: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); } } void _handleIceCandidate(List? args) async { log('๐Ÿ“ž [CALL EVENT] OnIceCandidateAsync received', name: 'ChatProvider'); if (args == null || args.isEmpty) { log('โš ๏ธ [CALL EVENT] ICE candidate args are null or empty', name: 'ChatProvider'); return; } try { final candidateJson = args[0] as String?; if (candidateJson == null) { log('โš ๏ธ [CALL EVENT] ICE candidate JSON is null', name: 'ChatProvider'); return; } if (_webrtcService == null) { log('โš ๏ธ [CALL EVENT] WebRTC service not initialized yet, ignoring ICE candidate', name: 'ChatProvider'); return; } final candidateData = jsonDecode(candidateJson) as Map; final candidate = RTCIceCandidate( candidateData['candidate'] as String?, candidateData['sdpMid'] as String?, candidateData['sdpMLineIndex'] as int?, ); log('๐ŸงŠ [CALL EVENT] Parsed ICE candidate: ${candidateData['candidate']}', name: 'ChatProvider'); await _webrtcService!.addIceCandidate(candidate); log('โœ… [CALL EVENT] ICE candidate added successfully', name: 'ChatProvider'); } catch (e, stackTrace) { log('โŒ [CALL EVENT] Error handling ICE candidate: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); // Don't rethrow - ICE candidate errors shouldn't crash the app } } void _handleAudioToggle(List? args) { log('๐Ÿ“ž [CALL EVENT] OnAudioToggle received', name: 'ChatProvider'); isPeerMuted = !isPeerMuted; notifyListeners(); } void _handleCameraToggle(List? args) { log('๐Ÿ“ž [CALL EVENT] OnCameraToggle received', name: 'ChatProvider'); isPeerCameraOn = !isPeerCameraOn; notifyListeners(); } void _onCallHistoryUpdated(List? args) async { log('๐Ÿ“ž [CALL HISTORY] OnCallHistoryUpdated received', name: 'ChatProvider'); try { if (sender != null && recipient != null) { userChatHistory = await ChatApiClient().loadChatHistory( moduleID, referenceID ?? 0, sender!.employeeNumber ?? '', recipient!.employeeNumber ?? '', ); chatResponseList = userChatHistory ?? []; chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!)); notifyListeners(); } } catch (e, stackTrace) { log('โŒ [CALL HISTORY] Error reloading chat history: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); } } /// Handle connection failure (ICE connection failed) void _handleConnectionFailure() { log('โŒ [CALL] Connection failure detected', name: 'ChatProvider'); final context = navigatorKey.currentContext; // Notify user about connection failure if (context != null && context.mounted) { CallErrorHandler.showConnectionFailed(context); } // Send hangup signal to peer if (chatHubConnection?.state == HubConnectionState.Connected && sender != null && currentCall != null) { chatHubConnection!.invoke( 'HangUpAsync', args: [ sender!.employeeNumber ?? '', currentCall!.peerId, moduleID.toString(), referenceID?.toString() ?? '', chatParticipantModel?.id?.toString() ?? '', ], ).catchError((e) { log('โŒ [CALL] Failed to send hangup after connection failure: $e', name: 'ChatProvider'); }); } // Clean up call resources _teardownCall(); if (kDebugMode) { print('โŒ Call ended due to connection failure'); } } /// Start timeout for ICE connection establishment (60 seconds - increased for TURN relay) void _startConnectionTimeout() { _callTimeoutTimer?.cancel(); _callTimeoutTimer = Timer(const Duration(seconds: 60), () { log('โฑ๏ธ [CALL] Connection timeout - ICE failed to establish within 60s', name: 'ChatProvider'); if (callStatus == CallStatus.connecting) { log('โŒ [CALL] Ending call due to connection timeout', name: 'ChatProvider'); _handleConnectionFailure(); } }); log('โฑ๏ธ [CALL] Connection timeout started (60s - extended for TURN relay)', name: 'ChatProvider'); } /// Start timeout for reconnection (10 seconds) void _startReconnectionTimeout() { _callTimeoutTimer?.cancel(); _callTimeoutTimer = Timer(const Duration(seconds: 10), () { log('โฑ๏ธ [CALL] Reconnection timeout - connection did not recover', name: 'ChatProvider'); if (callStatus != CallStatus.connected && callStatus != CallStatus.idle) { log('โŒ [CALL] Ending call due to reconnection timeout', name: 'ChatProvider'); _handleConnectionFailure(); } }); log('โฑ๏ธ [CALL] Reconnection timeout started (10s)', name: 'ChatProvider'); } /// Initialize CallKit service Future _initializeCallKit() async { if (_callKitInitialized) return; try { log('๐Ÿ“ž [CallKit] Initializing CallKit service...', name: 'ChatProvider'); await _callKitService.initialize(); // Setup CallKit callbacks _callKitService.onCallAccepted = _handleCallKitAccepted; _callKitService.onCallDeclined = _handleCallKitDeclined; _callKitService.onCallEnded = _handleCallKitEnded; _callKitService.onCallTimeout = _handleCallKitTimeout; _callKitInitialized = true; log('โœ… [CallKit] Service initialized successfully', name: 'ChatProvider'); } catch (e, stackTrace) { log('โŒ [CallKit] Error initializing: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); } } /// Handle CallKit accept event void _handleCallKitAccepted(String callId) { log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•', name: 'ChatProvider'); log('โœ… [CallKit] Call accepted via native UI: $callId', name: 'ChatProvider'); log('๐Ÿ”ต [CallKit] Current call ID: ${currentCall?.callId}', name: 'ChatProvider'); log('๐Ÿ”ต [CallKit] Current call status: $callStatus', name: 'ChatProvider'); log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•', name: 'ChatProvider'); // Find the call by ID and accept it if (currentCall != null && currentCall!.callId == callId) { log('โœ… [CallKit] Call IDs match - calling acceptCall()', name: 'ChatProvider'); // Schedule on next frame to ensure we're on main thread WidgetsBinding.instance.addPostFrameCallback((_) { log('๐Ÿ”ต [CallKit] Post-frame callback executing acceptCall()', name: 'ChatProvider'); acceptCall(); }); // Also call immediately in case we're already on main thread acceptCall(); } else { log('โš ๏ธ [CallKit] Call IDs do NOT match or currentCall is null', name: 'ChatProvider'); log(' Expected: $callId', name: 'ChatProvider'); log(' Current: ${currentCall?.callId}', name: 'ChatProvider'); } } /// Handle CallKit decline event void _handleCallKitDeclined(String callId) { log('โŒ [CallKit] Call declined via native UI: $callId', name: 'ChatProvider'); // Find the call by ID and decline it if (currentCall != null && currentCall!.callId == callId) { declineCall('user_declined_native_ui'); } } /// Handle CallKit ended event void _handleCallKitEnded(String callId) { log('๐Ÿ”ด [CallKit] Call ended via native UI: $callId', name: 'ChatProvider'); // End the call if (currentCall != null && currentCall!.callId == callId) { hangUp(); } } /// Handle CallKit timeout event void _handleCallKitTimeout(String callId) { log('โฑ๏ธ [CallKit] Call timeout: $callId', name: 'ChatProvider'); // Handle timeout if (currentCall != null && currentCall!.callId == callId) { _teardownCall(); } } }