import 'dart:async'; import 'dart:developer'; import 'package:flutter_callkit_incoming/entities/entities.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:test_sa/modules/cx_module/chat/services/pending_call_storage.dart'; /// Service to handle CallKit (iOS) and ConnectionService (Android) integration /// Provides native call UI experience on both platforms class CallKitService { static final CallKitService _instance = CallKitService._internal(); factory CallKitService() => _instance; CallKitService._internal(); // Callbacks for call events Function(String callId)? onCallAccepted; Function(String callId)? onCallDeclined; Function(String callId)? onCallEnded; Function(String callId)? onCallTimeout; // Stream subscription for CallKit events StreamSubscription? _eventSubscription; // Current active call UUID String? _currentCallId; // CRITICAL: Track initialization to prevent duplicate listeners bool _isInitialized = false; /// Initialize CallKit service and listen for events Future initialize() async { if (_isInitialized) { return; } _eventSubscription = FlutterCallkitIncoming.onEvent.listen(_handleCallKitEvent); _isInitialized = true; } /// Handle CallKit events from native side void _handleCallKitEvent(CallEvent? event) { if (event == null) return; switch (event) { case CallEventActionCallAccept(:final callKitParams): final callId = callKitParams.id; PendingCallStorage.markCallAcceptedFromBackground(callId); onCallAccepted?.call(callId); break; case CallEventActionCallDecline(:final callKitParams): final callId = callKitParams.id; onCallDeclined?.call(callId); break; case CallEventActionCallEnded(:final callKitParams): final callId = callKitParams.id; onCallEnded?.call(callId); break; case CallEventActionCallTimeout(:final id): onCallTimeout?.call(id); break; case CallEventActionCallToggleMute(): case CallEventActionCallToggleHold(): case CallEventActionCallIncoming(): case CallEventActionCallStart(): case CallEventActionCallCallback(): case CallEventActionCallConnected(): case CallEventActionDidUpdateDevicePushTokenVoip(): case CallEventActionCallToggleDmtf(): case CallEventActionCallToggleGroup(): case CallEventActionCallToggleAudioSession(): case CallEventActionCallCustom(): break; } } /// Show incoming call UI (CallKit on iOS, ConnectionService on Android) Future showIncomingCall({ required String callId, required String callerName, required String callerNumber, String? callerAvatar, required bool isVideo, Map? extra, }) async { try { _currentCallId = callId; final params = CallKitParams( id: callId, nameCaller: callerName, appName: 'Atoms SA', avatar: callerAvatar, handle: callerNumber, type: isVideo ? 1 : 0, duration: 30000, extra: extra ?? {}, headers: {'platform': 'flutter'}, android: AndroidParams( isCustomNotification: true, isShowLogo: false, ringtonePath: 'system_ringtone_default', backgroundColor: '#0955fa', backgroundUrl: callerAvatar ?? '', actionColor: '#4CAF50', textColor: '#ffffff', incomingCallNotificationChannelName: 'Incoming Calls', missedCallNotificationChannelName: 'Missed Calls', ), ios: const IOSParams( iconName: 'CallKitLogo', handleType: 'generic', supportsVideo: true, maximumCallGroups: 2, maximumCallsPerCallGroup: 1, audioSessionMode: 'videoChat', audioSessionActive: true, audioSessionPreferredSampleRate: 44100.0, audioSessionPreferredIOBufferDuration: 0.005, supportsDTMF: true, supportsHolding: true, supportsGrouping: false, supportsUngrouping: false, ringtonePath: 'system_ringtone_default', ), ); await FlutterCallkitIncoming.showCallkitIncoming(params); } catch (e, stackTrace) { log('❌ [CallKit] Error showing incoming call: $e', name: 'CallKitService', error: e, stackTrace: stackTrace); rethrow; } } /// Start an outgoing call (show native UI) Future startOutgoingCall({ required String callId, required String callerName, required String callerNumber, String? callerAvatar, required bool isVideo, }) async { try { _currentCallId = callId; final params = CallKitParams( id: callId, nameCaller: callerName, appName: 'Atoms SA', avatar: callerAvatar, handle: callerNumber, type: isVideo ? 1 : 0, extra: {'outgoing': true}, ios: const IOSParams( handleType: 'generic', supportsVideo: true, audioSessionMode: 'videoChat', audioSessionActive: true, ), ); await FlutterCallkitIncoming.startCall(params); } catch (e) { log('❌ [CallKit] Error starting outgoing call: $e', name: 'CallKitService'); } } /// Mark call as connected (when peer accepts) Future setCallConnected(String callId) async { try { await FlutterCallkitIncoming.setCallConnected(callId); } catch (e) { log('❌ [CallKit] Error setting call connected: $e', name: 'CallKitService'); } } /// End the current call Future endCall(String callId) async { try { await FlutterCallkitIncoming.endCall(callId); if (_currentCallId == callId) { _currentCallId = null; } } catch (e) { log('❌ [CallKit] Error ending call: $e', name: 'CallKitService'); } } /// End all active calls Future endAllCalls() async { try { await FlutterCallkitIncoming.endAllCalls(); _currentCallId = null; } catch (e) { log('❌ [CallKit] Error ending all calls: $e', name: 'CallKitService'); } } /// Get all active calls Future> getActiveCalls() async { try { final calls = await FlutterCallkitIncoming.activeCalls(); return calls; } catch (e) { log('❌ [CallKit] Error getting active calls: $e', name: 'CallKitService'); return []; } } /// Dispose and cleanup Future dispose() async { try { await _eventSubscription?.cancel(); _eventSubscription = null; await endAllCalls(); onCallAccepted = null; onCallDeclined = null; onCallEnded = null; onCallTimeout = null; } catch (e) { log('❌ [CallKit] Error disposing service: $e', name: 'CallKitService'); } } }