|
|
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<CallEvent?>? _eventSubscription;
|
|
|
|
|
|
// Current active call UUID
|
|
|
String? _currentCallId;
|
|
|
|
|
|
/// Initialize CallKit service and listen for events
|
|
|
Future<void> 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<void> showIncomingCall({
|
|
|
required String callId,
|
|
|
required String callerName,
|
|
|
required String callerNumber,
|
|
|
String? callerAvatar,
|
|
|
required bool isVideo,
|
|
|
Map<String, dynamic>? extra,
|
|
|
}) async {
|
|
|
try {
|
|
|
if (kDebugMode) {
|
|
|
log('📞 [CallKit] Showing incoming call UI', name: 'CallKitService');
|
|
|
log(' Caller: $callerName ($callerNumber)', name: 'CallKitService');
|
|
|
log(' Video: $isVideo', name: 'CallKitService');
|
|
|
log(' Call ID: $callId', name: 'CallKitService');
|
|
|
}
|
|
|
|
|
|
_currentCallId = callId;
|
|
|
|
|
|
// Configure call parameters
|
|
|
final params = CallKitParams(
|
|
|
id: callId,
|
|
|
nameCaller: callerName,
|
|
|
appName: 'Atoms SA',
|
|
|
avatar: callerAvatar,
|
|
|
handle: callerNumber,
|
|
|
type: isVideo ? 1 : 0, // 0 = audio, 1 = video
|
|
|
duration: 30000, // 30 seconds timeout
|
|
|
extra: extra ?? {},
|
|
|
headers: <String, dynamic>{'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<void> 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: <String, dynamic>{'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<void> setCallConnected(String callId) async {
|
|
|
try {
|
|
|
if (kDebugMode) {
|
|
|
log('✅ [CallKit] Marking call as connected: $callId', name: 'CallKitService');
|
|
|
}
|
|
|
|
|
|
// Update call to connected state - this will update the native UI
|
|
|
await FlutterCallkitIncoming.setCallConnected(callId);
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
log('✅ [CallKit] Call marked as connected', name: 'CallKitService');
|
|
|
}
|
|
|
} catch (e) {
|
|
|
log('❌ [CallKit] Error setting call connected: $e', name: 'CallKitService');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// End the current call
|
|
|
Future<void> endCall(String callId) async {
|
|
|
try {
|
|
|
if (kDebugMode) {
|
|
|
log('🔴 [CallKit] Ending call: $callId', name: 'CallKitService');
|
|
|
}
|
|
|
|
|
|
await FlutterCallkitIncoming.endCall(callId);
|
|
|
|
|
|
if (_currentCallId == callId) {
|
|
|
_currentCallId = null;
|
|
|
}
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
log('✅ [CallKit] Call ended', name: 'CallKitService');
|
|
|
}
|
|
|
} catch (e) {
|
|
|
log('❌ [CallKit] Error ending call: $e', name: 'CallKitService');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// End all active calls
|
|
|
Future<void> endAllCalls() async {
|
|
|
try {
|
|
|
if (kDebugMode) {
|
|
|
log('🔴 [CallKit] Ending all calls', name: 'CallKitService');
|
|
|
}
|
|
|
|
|
|
await FlutterCallkitIncoming.endAllCalls();
|
|
|
_currentCallId = null;
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
log('✅ [CallKit] All calls ended', name: 'CallKitService');
|
|
|
}
|
|
|
} catch (e) {
|
|
|
log('❌ [CallKit] Error ending all calls: $e', name: 'CallKitService');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// Get all active calls
|
|
|
Future<List<dynamic>> getActiveCalls() async {
|
|
|
try {
|
|
|
final calls = await FlutterCallkitIncoming.activeCalls();
|
|
|
if (kDebugMode) {
|
|
|
log('📋 [CallKit] Active calls: ${calls.length}', name: 'CallKitService');
|
|
|
}
|
|
|
return calls;
|
|
|
} catch (e) {
|
|
|
log('❌ [CallKit] Error getting active calls: $e', name: 'CallKitService');
|
|
|
return [];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// Dispose and cleanup
|
|
|
Future<void> dispose() async {
|
|
|
try {
|
|
|
if (kDebugMode) {
|
|
|
log('🧹 [CallKit] Disposing service', name: 'CallKitService');
|
|
|
}
|
|
|
|
|
|
// Cancel event subscription
|
|
|
await _eventSubscription?.cancel();
|
|
|
_eventSubscription = null;
|
|
|
|
|
|
// End all active calls
|
|
|
await endAllCalls();
|
|
|
|
|
|
// Clear callbacks
|
|
|
onCallAccepted = null;
|
|
|
onCallDeclined = null;
|
|
|
onCallEnded = null;
|
|
|
onCallTimeout = null;
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
log('✅ [CallKit] Service disposed', name: 'CallKitService');
|
|
|
}
|
|
|
} catch (e) {
|
|
|
log('❌ [CallKit] Error disposing service: $e', name: 'CallKitService');
|
|
|
}
|
|
|
}
|
|
|
}
|