import 'dart:async'; import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter_callkit_incoming/entities/entities.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:uuid/uuid.dart'; /// 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; /// Initialize CallKit service and listen for events Future initialize() async { if (kDebugMode) { log('πŸ“ž [CallKit] Initializing CallKit service', name: 'CallKitService'); } // Listen for CallKit events _eventSubscription = FlutterCallkitIncoming.onEvent.listen(_handleCallKitEvent); if (kDebugMode) { log('βœ… [CallKit] Service initialized', name: 'CallKitService'); } } /// Handle CallKit events from native side void _handleCallKitEvent(CallEvent? event) { if (event == null) return; if (kDebugMode) { log('πŸ“ž [CallKit] Event received: ${event.toString()}', name: 'CallKitService'); log('πŸ“ž [CallKit] Event type: ${event.eventName}', name: 'CallKitService'); } // Handle different event types using pattern matching switch (event) { case CallEventActionCallAccept(:final callKitParams): // User accepted the call via CallKit UI final callId = callKitParams.id; if (kDebugMode) { log('βœ… [CallKit] Call ACCEPTED via native UI - calling callback', name: 'CallKitService'); log(' Call ID: $callId', name: 'CallKitService'); } onCallAccepted?.call(callId); break; case CallEventActionCallDecline(:final callKitParams): // User declined the call via CallKit UI final callId = callKitParams.id; if (kDebugMode) { log('❌ [CallKit] Call DECLINED via native UI - calling callback', name: 'CallKitService'); log(' Call ID: $callId', name: 'CallKitService'); } onCallDeclined?.call(callId); break; case CallEventActionCallEnded(:final callKitParams): // User ended the call via CallKit UI final callId = callKitParams.id; if (kDebugMode) { log('πŸ”΄ [CallKit] Call ENDED via native UI - calling callback', name: 'CallKitService'); log(' Call ID: $callId', name: 'CallKitService'); } onCallEnded?.call(callId); break; case CallEventActionCallTimeout(:final id): // Call timed out (no answer) if (kDebugMode) { log('⏱️ [CallKit] Call TIMEOUT - calling callback', name: 'CallKitService'); log(' Call ID: $id', name: 'CallKitService'); } onCallTimeout?.call(id); break; case CallEventActionCallToggleMute(:final id, :final isMuted): // User toggled mute via CallKit UI if (kDebugMode) { log('πŸ”‡ [CallKit] Mute toggled: $isMuted for call $id', name: 'CallKitService'); } break; case CallEventActionCallToggleHold(:final id, :final isOnHold): // User toggled hold via CallKit UI if (kDebugMode) { log('⏸️ [CallKit] Hold toggled: $isOnHold for call $id', name: 'CallKitService'); } break; case CallEventActionCallIncoming(:final callKitParams): // Incoming call notification shown if (kDebugMode) { log('πŸ“² [CallKit] Incoming call notification for: ${callKitParams.id}', name: 'CallKitService'); } break; case CallEventActionCallStart(:final callKitParams): // Call started if (kDebugMode) { log('πŸ“ž [CallKit] Call started: ${callKitParams.id}', name: 'CallKitService'); } break; case CallEventActionCallCallback(:final id): // Callback event if (kDebugMode) { log('πŸ“ž [CallKit] Callback event for call: $id', name: 'CallKitService'); } break; case CallEventActionCallConnected(:final id): // Call connected if (kDebugMode) { log('βœ… [CallKit] Call connected: $id', name: 'CallKitService'); } break; case CallEventActionDidUpdateDevicePushTokenVoip(): // Push token updated if (kDebugMode) { log('πŸ”” [CallKit] Device push token updated', name: 'CallKitService'); } break; case CallEventActionCallToggleDmtf(:final id, :final digits, :final type): // DTMF toggled if (kDebugMode) { log('πŸ”’ [CallKit] DTMF toggled: $digits for call $id', name: 'CallKitService'); } break; case CallEventActionCallToggleGroup(:final id, :final callUUIDToGroupWith): // Call group toggled if (kDebugMode) { log('πŸ‘₯ [CallKit] Group toggled for call $id with $callUUIDToGroupWith', name: 'CallKitService'); } break; case CallEventActionCallToggleAudioSession(:final isActive): // Audio session toggled if (kDebugMode) { log('πŸ”Š [CallKit] Audio session toggled: $isActive', name: 'CallKitService'); } break; case CallEventActionCallCustom(:final body): // Custom event if (kDebugMode) { log('πŸ”§ [CallKit] Custom event: $body', name: 'CallKitService'); } break; default: if (kDebugMode) { log('ℹ️ [CallKit] Unhandled event type: ${event.eventName}', name: 'CallKitService'); } } } /// 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 { if (kDebugMode) { log('πŸ“ž [CallKit] Showing incoming call UI', name: 'CallKitService'); log(' Caller: $callerName ($callerNumber)', name: 'CallKitService'); log(' Video: $isVideo', name: 'CallKitService'); log(' Call ID: $callId', name: 'CallKitService'); } _currentCallId = callId; // Configure call parameters final params = CallKitParams( id: callId, nameCaller: callerName, appName: 'Atoms SA', avatar: callerAvatar, handle: callerNumber, type: isVideo ? 1 : 0, // 0 = audio, 1 = video duration: 30000, // 30 seconds timeout 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', ), ); // Show the incoming call UI await FlutterCallkitIncoming.showCallkitIncoming(params); if (kDebugMode) { log('βœ… [CallKit] Incoming call UI displayed', name: 'CallKitService'); } } catch (e, stackTrace) { log('❌ [CallKit] Error showing incoming call: $e', name: 'CallKitService', error: e, stackTrace: stackTrace); 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 { if (kDebugMode) { log('πŸ“ž [CallKit] Starting outgoing call', name: 'CallKitService'); } _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); if (kDebugMode) { log('βœ… [CallKit] Outgoing call started', name: 'CallKitService'); } } catch (e) { log('❌ [CallKit] Error starting outgoing call: $e', name: 'CallKitService'); } } /// Mark call as connected (when peer accepts) Future setCallConnected(String callId) async { try { if (kDebugMode) { log('βœ… [CallKit] Marking call as connected: $callId', name: 'CallKitService'); } // Update call to connected state - this will update the native UI await FlutterCallkitIncoming.setCallConnected(callId); if (kDebugMode) { log('βœ… [CallKit] Call marked as connected', name: 'CallKitService'); } } catch (e) { log('❌ [CallKit] Error setting call connected: $e', name: 'CallKitService'); } } /// End the current call Future endCall(String callId) async { try { if (kDebugMode) { log('πŸ”΄ [CallKit] Ending call: $callId', name: 'CallKitService'); } await FlutterCallkitIncoming.endCall(callId); if (_currentCallId == callId) { _currentCallId = null; } if (kDebugMode) { log('βœ… [CallKit] Call ended', name: 'CallKitService'); } } catch (e) { log('❌ [CallKit] Error ending call: $e', name: 'CallKitService'); } } /// End all active calls Future endAllCalls() async { try { if (kDebugMode) { log('πŸ”΄ [CallKit] Ending all calls', name: 'CallKitService'); } await FlutterCallkitIncoming.endAllCalls(); _currentCallId = null; if (kDebugMode) { log('βœ… [CallKit] All calls ended', name: 'CallKitService'); } } catch (e) { log('❌ [CallKit] Error ending all calls: $e', name: 'CallKitService'); } } /// Get all active calls Future> getActiveCalls() async { try { final calls = await FlutterCallkitIncoming.activeCalls(); if (kDebugMode) { log('πŸ“‹ [CallKit] Active calls: ${calls.length}', name: 'CallKitService'); } return calls; } catch (e) { log('❌ [CallKit] Error getting active calls: $e', name: 'CallKitService'); return []; } } /// Dispose and cleanup Future dispose() async { try { if (kDebugMode) { log('🧹 [CallKit] Disposing service', name: 'CallKitService'); } // Cancel event subscription await _eventSubscription?.cancel(); _eventSubscription = null; // End all active calls await endAllCalls(); // Clear callbacks onCallAccepted = null; onCallDeclined = null; onCallEnded = null; onCallTimeout = null; if (kDebugMode) { log('βœ… [CallKit] Service disposed', name: 'CallKitService'); } } catch (e) { log('❌ [CallKit] Error disposing service: $e', name: 'CallKitService'); } } }