import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:dialer/domain/services/call_service.dart'; import 'package:dialer/domain/services/obfuscate_service.dart'; import 'package:dialer/presentation/common/widgets/username_color_generator.dart'; import 'package:dialer/presentation/common/widgets/sim_selection_dialog.dart'; import 'package:flutter/services.dart'; import 'package:sim_data_new/sim_data.dart'; class CallPage extends StatefulWidget { final String displayName; final String phoneNumber; final Uint8List? thumbnail; const CallPage({ super.key, required this.displayName, required this.phoneNumber, this.thumbnail, }); @override _CallPageState createState() => _CallPageState(); } class _CallPageState extends State { final ObfuscateService _obfuscateService = ObfuscateService(); final CallService _callService = CallService(); bool isMuted = false; bool isSpeaker = false; bool isKeypadVisible = false; bool icingProtocolOk = true; String _typedDigits = ""; Timer? _callTimer; int _callSeconds = 0; String _callStatus = "Calling..."; StreamSubscription? _callStateSubscription; StreamSubscription>? _audioStateSubscription; StreamSubscription? _simStateSubscription; bool _isCallActive = true; // Track if call is still active String? _simName; // Human-readable SIM card name bool get isNumberUnknown => widget.displayName == widget.phoneNumber; // Fetch and update human-readable SIM name based on slot Future _updateSimName(int? simSlot) async { if (!mounted) return; if (simSlot != null) { try { final simData = await SimDataPlugin.getSimData(); // Find the SIM card matching the slot index, if any dynamic card; for (var c in simData.cards) { if (c.slotIndex == simSlot) { card = c; break; } } String name; if (card != null && card.displayName.isNotEmpty) { name = card.displayName; } else if (card != null && card.carrierName.isNotEmpty) { name = card.carrierName; } else { name = 'SIM ${simSlot + 1}'; } setState(() { _simName = name; }); } catch (e) { setState(() { _simName = 'SIM ${simSlot + 1}'; }); } } else { setState(() { _simName = null; }); } } @override void initState() { super.initState(); _checkInitialCallState(); _listenToCallState(); _listenToAudioState(); _listenToSimState(); _updateSimName(CallService.getCurrentSimSlot); // Initial SIM name _setInitialAudioState(); } @override void dispose() { _callTimer?.cancel(); _callStateSubscription?.cancel(); _audioStateSubscription?.cancel(); _simStateSubscription?.cancel(); super.dispose(); } void _setInitialAudioState() { final initialAudioState = _callService.currentAudioState; if (initialAudioState != null) { setState(() { isMuted = initialAudioState['muted'] ?? false; isSpeaker = initialAudioState['speaker'] ?? false; }); } } void _checkInitialCallState() async { try { final state = await _callService.getCallState(); print('CallPage: Initial call state: $state'); if (mounted) { setState(() { if (state == "active") { _callStatus = "00:00"; _isCallActive = true; _startCallTimer(); } else if (state == "disconnected" || state == "disconnecting") { _callStatus = "Call Ended"; _isCallActive = false; } else { _callStatus = "Calling..."; _isCallActive = true; } }); } } catch (e) { print('CallPage: Error checking initial state: $e'); } } void _listenToCallState() { _callStateSubscription = _callService.callStateStream.listen((state) { print('CallPage: Call state changed to $state'); if (mounted) { setState(() { if (state == "active") { _callStatus = "00:00"; _isCallActive = true; _startCallTimer(); } else if (state == "disconnected" || state == "disconnecting") { _callTimer?.cancel(); _callStatus = "Call Ended"; _isCallActive = false; // Let CallService handle navigation - don't navigate from here } else { _callStatus = "Calling..."; _isCallActive = true; } }); } }); } void _listenToAudioState() { _audioStateSubscription = _callService.audioStateStream.listen((state) { if (mounted) { setState(() { isMuted = state['muted'] ?? isMuted; isSpeaker = state['speaker'] ?? isSpeaker; }); } }); } void _listenToSimState() { _simStateSubscription = _callService.simStateStream.listen((simSlot) { _updateSimName(simSlot); }); } void _startCallTimer() { _callTimer?.cancel(); _callTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (mounted) { setState(() { _callSeconds++; final minutes = (_callSeconds ~/ 60).toString().padLeft(2, '0'); final seconds = (_callSeconds % 60).toString().padLeft(2, '0'); _callStatus = '$minutes:$seconds'; }); } }); } void _addDigit(String digit) async { print('CallPage: Tapped digit: $digit'); setState(() { _typedDigits += digit; }); // Send DTMF tone const channel = MethodChannel('call_service'); try { final success = await channel.invokeMethod('sendDtmfTone', {'digit': digit}); if (success != true) { print('CallPage: Failed to send DTMF tone for $digit'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to send DTMF tone')), ); } } } catch (e) { print('CallPage: Error sending DTMF tone: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error sending DTMF tone: $e')), ); } } } void _toggleMute() async { try { print('CallPage: Toggling mute, current state: $isMuted'); final result = await _callService.muteCall(context, mute: !isMuted); print('CallPage: Mute call result: $result'); if (mounted && result['status'] != 'success') { print('CallPage: Failed to toggle mute: ${result['message']}'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to toggle mute: ${result['message']}')), ); } } catch (e) { print('CallPage: Error toggling mute: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error toggling mute: $e')), ); } } } Future _toggleSpeaker() async { try { print('CallPage: Toggling speaker, current state: $isSpeaker'); final result = await _callService.speakerCall(context, speaker: !isSpeaker); print('CallPage: Speaker call result: $result'); if (mounted && result['status'] != 'success') { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to toggle speaker: ${result['message']}')), ); } } catch (e) { print('CallPage: Error toggling speaker: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error toggling speaker: $e')), ); } } } void _toggleKeypad() { setState(() { isKeypadVisible = !isKeypadVisible; }); } void _showSimSelectionDialog() { showDialog( context: context, builder: (BuildContext context) { return SimSelectionDialog( phoneNumber: widget.phoneNumber, displayName: widget.displayName, onSimSelected: _switchToNewSim, ); }, ); } void _switchToNewSim(int simSlot) async { try { print( 'CallPage: Initiating SIM switch to slot $simSlot for ${widget.phoneNumber}'); // Use the CallService to handle the SIM switch logic await _callService.switchSimAndRedial( phoneNumber: widget.phoneNumber, displayName: widget.displayName, simSlot: simSlot, thumbnail: widget.thumbnail, ); print('CallPage: SIM switch initiated successfully'); } catch (e) { print('CallPage: Error initiating SIM switch: $e'); // Show error feedback if widget is still mounted if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error switching SIM: $e'), backgroundColor: Colors.red, ), ); } } } void _hangUp() async { // Don't try to hang up if call is already ended if (!_isCallActive) { print('CallPage: Ignoring hangup - call already ended'); return; } try { print( 'CallPage: Initiating manual hangUp - canceling any pending SIM switch'); // Immediately mark call as inactive to prevent further interactions setState(() { _isCallActive = false; _callStatus = "Ending Call..."; }); // Cancel any pending SIM switch since user is manually hanging up _callService.cancelPendingSimSwitch(); final result = await _callService.hangUpCall(context); print('CallPage: Hang up result: $result'); // If the page is still visible after hangup, try to close it if (mounted && ModalRoute.of(context)?.isCurrent == true) { print('CallPage: Still visible after hangup, navigating back'); Navigator.of(context).popUntil((route) => route.isFirst); } } catch (e) { print('CallPage: Error hanging up: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error hanging up: $e')), ); } } } void _addContact() async { if (await FlutterContacts.requestPermission()) { final newContact = Contact()..phones = [Phone(widget.phoneNumber)]; final updatedContact = await FlutterContacts.openExternalInsert(newContact); if (mounted && updatedContact != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Contact added successfully!')), ); } } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Permission denied for contacts')), ); } } } @override Widget build(BuildContext context) { final double avatarRadius = isKeypadVisible ? 45.0 : 45.0; final double nameFontSize = isKeypadVisible ? 24.0 : 24.0; final double statusFontSize = isKeypadVisible ? 16.0 : 16.0; print( 'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}'); // If call is disconnected and we're not actively navigating, force navigation if ((_callStatus == "Call Ended" || !_isCallActive) && mounted) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && ModalRoute.of(context)?.isCurrent == true) { print('CallPage: Call ended, forcing navigation back to home'); Navigator.of(context).popUntil((route) => route.isFirst); } }); } return PopScope( canPop: true, // Always allow popping - CallService manages when it's appropriate onPopInvoked: (didPop) { print( 'CallPage: PopScope onPopInvoked - didPop: $didPop, _isCallActive: $_isCallActive, _callStatus: $_callStatus'); // No longer prevent popping during active calls - CallService handles this }, child: Scaffold( body: Container( color: Colors.black, child: SafeArea( child: Column( children: [ Container( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 35), ObfuscatedAvatar( imageBytes: widget.thumbnail, radius: avatarRadius, backgroundColor: generateColorFromName(widget.displayName), fallbackInitial: widget.displayName, ), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( icingProtocolOk ? Icons.lock : Icons.lock_open, color: icingProtocolOk ? Colors.green : Colors.red, size: 16, ), const SizedBox(width: 4), Text( 'Icing protocol: ${icingProtocolOk ? "ok" : "ko"}', style: TextStyle( color: icingProtocolOk ? Colors.green : Colors.red, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 4), Text( _obfuscateService.obfuscateData(widget.displayName), style: TextStyle( fontSize: nameFontSize, color: Colors.white, fontWeight: FontWeight.bold, ), ), Text( widget.phoneNumber, style: TextStyle( fontSize: statusFontSize, color: Colors.white70, ), ), Text( _callStatus, style: TextStyle( fontSize: statusFontSize, color: Colors.white70, ), ), // Show SIM information if a SIM slot has been set if (_simName != null) Container( margin: const EdgeInsets.only(top: 4.0), padding: const EdgeInsets.symmetric( horizontal: 8.0, vertical: 2.0), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.2), borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.blue.withOpacity(0.5), width: 1, ), ), child: Text( // Show human-readable SIM name plus slot number '$_simName (SIM ${CallService.getCurrentSimSlot! + 1})', style: TextStyle( fontSize: statusFontSize - 2, color: Colors.lightBlueAccent, fontWeight: FontWeight.w500, ), ), ), ], ), ), Expanded( child: Column( children: [ if (isKeypadVisible) ...[ const Spacer(flex: 2), Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( _typedDigits, maxLines: 1, textAlign: TextAlign.right, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, ), ), ), IconButton( padding: EdgeInsets.zero, onPressed: _toggleKeypad, icon: const Icon( Icons.close, color: Colors.white, ), ), ], ), ), Container( height: MediaQuery.of(context).size.height * 0.4, margin: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.all(8), child: GridView.count( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisCount: 3, childAspectRatio: 1.5, mainAxisSpacing: 8, crossAxisSpacing: 8, children: List.generate(12, (index) { String label; if (index < 9) { label = '${index + 1}'; } else if (index == 9) { label = '*'; } else if (index == 10) { label = '0'; } else { label = '#'; } return GestureDetector( onTap: () => _addDigit(label), child: Container( decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.transparent, ), child: Center( child: Text( label, style: const TextStyle( fontSize: 32, color: Colors.white, ), ), ), ), ); }), ), ), const Spacer(flex: 1), ] else ...[ const Spacer(), Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Column( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: _toggleMute, icon: Icon( isMuted ? Icons.mic_off : Icons.mic, color: isMuted ? Colors.amber : Colors.white, size: 32, ), ), Text( isMuted ? 'Unmute' : 'Mute', style: const TextStyle( color: Colors.white, fontSize: 14, ), ), ], ), Column( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: _toggleKeypad, icon: const Icon( Icons.dialpad, color: Colors.white, size: 32, ), ), const Text( 'Keypad', style: TextStyle( color: Colors.white, fontSize: 14, ), ), ], ), Column( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: _toggleSpeaker, icon: Icon( isSpeaker ? Icons.volume_up : Icons.volume_off, color: isSpeaker ? Colors.amber : Colors.white, size: 32, ), ), const Text( 'Speaker', style: TextStyle( color: Colors.white, fontSize: 14, ), ), ], ), ], ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (isNumberUnknown) Column( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: _addContact, icon: const Icon( Icons.person_add, color: Colors.white, size: 32, ), ), const Text( 'Add Contact', style: TextStyle( color: Colors.white, fontSize: 14, ), ), ], ), Column( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: _isCallActive ? _showSimSelectionDialog : null, icon: Icon( Icons.sim_card, color: _isCallActive ? Colors.white : Colors.grey, size: 32, ), ), const Text( 'Change SIM', style: TextStyle( color: Colors.white, fontSize: 14, ), ), ], ), ], ), ], ), ), const Spacer(flex: 3), ], ], ), ), Padding( padding: const EdgeInsets.only(bottom: 16.0), child: GestureDetector( onTap: _isCallActive ? _hangUp : null, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: _isCallActive ? Colors.red : Colors.grey, shape: BoxShape.circle, ), child: Icon( _isCallActive ? Icons.call_end : Icons.call_end, color: Colors.white, size: 32, ), ), ), ), ], ), ), ), ), ); } }