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

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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