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

231 lines
6.8 KiB
Dart

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<CallEvent?>? _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<void> 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<void> showIncomingCall({
required String callId,
required String callerName,
required String callerNumber,
String? callerAvatar,
required bool isVideo,
Map<String, dynamic>? 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: <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',
),
);
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<void> 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: <String, dynamic>{'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<void> 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<void> 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<void> endAllCalls() async {
try {
await FlutterCallkitIncoming.endAllCalls();
_currentCallId = null;
} 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();
return calls;
} catch (e) {
log('❌ [CallKit] Error getting active calls: $e', name: 'CallKitService');
return [];
}
}
/// Dispose and cleanup
Future<void> 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');
}
}
}