You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cloudsolutions-atoms/lib/modules/cx_module/chat/call/services/callkit_service.dart

387 lines
12 KiB
Dart

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');
}
}
}