import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:twilio_voice/twilio_voice.dart'; import '../models/call_info.dart'; import '../services/voice_service.dart'; class CallProvider extends ChangeNotifier { final VoiceService _voiceService; CallInfo _callInfo = const CallInfo(); Timer? _durationTimer; StreamSubscription? _eventSub; DateTime? _connectedAt; bool _pendingAutoAnswer = false; CallInfo get callInfo => _callInfo; CallProvider(this._voiceService) { _eventSub = _voiceService.callEvents.listen(_handleCallEvent); } void _handleCallEvent(CallEvent event) { switch (event) { case CallEvent.incoming: if (_pendingAutoAnswer) { _pendingAutoAnswer = false; _callInfo = _callInfo.copyWith(state: CallState.connecting); _voiceService.answer(); } else { _callInfo = _callInfo.copyWith(state: CallState.ringing); } break; case CallEvent.ringing: _callInfo = _callInfo.copyWith(state: CallState.connecting); break; case CallEvent.connected: _connectedAt = DateTime.now(); _callInfo = _callInfo.copyWith(state: CallState.connected); _startDurationTimer(); break; case CallEvent.callEnded: _stopDurationTimer(); _callInfo = const CallInfo(); // reset to idle break; case CallEvent.returningCall: _callInfo = _callInfo.copyWith(state: CallState.connecting); break; case CallEvent.reconnecting: break; case CallEvent.reconnected: break; default: break; } // Update caller info from active call (skip if call just ended) if (_callInfo.state != CallState.idle) { final call = TwilioVoice.instance.call; final active = call.activeCall; if (active != null) { if (_callInfo.callerNumber == null) { _callInfo = _callInfo.copyWith( callerNumber: active.from, ); } // Fetch SID asynchronously call.getSid().then((sid) { if (sid != null && sid != _callInfo.callSid && _callInfo.isActive) { _callInfo = _callInfo.copyWith(callSid: sid); notifyListeners(); } }); } } notifyListeners(); } void _startDurationTimer() { _durationTimer?.cancel(); _durationTimer = Timer.periodic(const Duration(seconds: 1), (_) { if (_connectedAt != null) { _callInfo = _callInfo.copyWith( duration: DateTime.now().difference(_connectedAt!), ); notifyListeners(); } }); } void _stopDurationTimer() { _durationTimer?.cancel(); _connectedAt = null; } Future answer() => _voiceService.answer(); Future reject() => _voiceService.reject(); Future hangUp() async { await _voiceService.hangUp(); // If SDK didn't fire callEnded (e.g. no active SDK call), reset manually if (_callInfo.state != CallState.idle) { _stopDurationTimer(); _callInfo = const CallInfo(); _pendingAutoAnswer = false; notifyListeners(); } } Future toggleMute() async { final newMuted = !_callInfo.isMuted; await _voiceService.toggleMute(newMuted); _callInfo = _callInfo.copyWith(isMuted: newMuted); notifyListeners(); } Future toggleSpeaker() async { final newSpeaker = !_callInfo.isSpeakerOn; await _voiceService.toggleSpeaker(newSpeaker); _callInfo = _callInfo.copyWith(isSpeakerOn: newSpeaker); notifyListeners(); } Future sendDigits(String digits) => _voiceService.sendDigits(digits); Future makeCall(String number, {String? callerId}) async { _callInfo = _callInfo.copyWith( state: CallState.connecting, callerNumber: number, ); notifyListeners(); final success = await _voiceService.makeCall(number, callerId: callerId); if (!success) { debugPrint('CallProvider.makeCall: call.place() returned false'); _callInfo = const CallInfo(); // reset to idle notifyListeners(); } } Future holdCall() async { final sid = _callInfo.callSid; if (sid == null) return; await _voiceService.holdCall(sid); _callInfo = _callInfo.copyWith(isOnHold: true); notifyListeners(); } Future unholdCall() async { final sid = _callInfo.callSid; if (sid == null) return; await _voiceService.unholdCall(sid); _callInfo = _callInfo.copyWith(isOnHold: false); notifyListeners(); } Future transferCall(String target) async { final sid = _callInfo.callSid; if (sid == null) return; await _voiceService.transferCall(sid, target); } Future acceptQueueCall(String callSid) async { _pendingAutoAnswer = true; _callInfo = _callInfo.copyWith(state: CallState.connecting); notifyListeners(); try { await _voiceService.acceptQueueCall(callSid); } catch (e) { debugPrint('CallProvider.acceptQueueCall error: $e'); _pendingAutoAnswer = false; _callInfo = const CallInfo(); notifyListeners(); } } @override void dispose() { _stopDurationTimer(); _eventSub?.cancel(); super.dispose(); } }