import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart'; import 'package:test_sa/modules/cx_module/chat/model/call_session.dart'; import 'package:test_sa/modules/cx_module/chat/call/utils/permission_helper.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; class AudioCallPage extends StatefulWidget { const AudioCallPage({Key? key}) : super(key: key); @override State createState() => _AudioCallPageState(); } class _AudioCallPageState extends State { late CallManager _callManager; @override void initState() { super.initState(); // HYBRID: Get CallManager from Provider (UI layer) _callManager = Provider.of(context, listen: false); // Listen for call end and navigate back _callManager.addListener(_checkCallStatus); // CRITICAL: Check permissions after first frame // Permissions must be requested in UI layer, not service layer WidgetsBinding.instance.addPostFrameCallback((_) { _checkPermissions(); }); } /// Request audio call permissions using production-ready helper Future _checkPermissions() async { if (!mounted) return; // Request all audio call permissions (microphone) final granted = await CallPermissionHelper.requestAudioCallPermissions(context); if (!granted && mounted) { // Permission denied - end the call gracefully await CallPermissionHelper.handlePermissionDeniedDuringCall( context, 'Microphone', onEndCall: () => _callManager.declineCall('permission_denied'), delaySeconds: 2, ); } } void _checkCallStatus() { // If call status is idle (call ended), navigate back if (_callManager.callStatus == CallStatus.idle && mounted) { Navigator.of(context).pop(); } } @override void dispose() { _callManager.removeListener(_checkCallStatus); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.backgroundTabBarDark, appBar: const DefaultAppBar( backgroundColor: Colors.transparent, arrowBackColor: AppColor.white10, ), body: SafeArea( child: ListenableBuilder( listenable: _callManager, builder: (context, _) { final session = _callManager.currentCall; if (session == null) { return const Center(child: Text('No active call')); } return Column( children: [ const Spacer(flex: 2), _buildContactInfo(context, session), 8.height, _buildCallStatusOrDuration(context), const Spacer(flex: 4), _buildControls(context, session), 32.height, ], ); }, ), ), ); } Widget _buildTopBar(BuildContext context) { return Padding( padding: const EdgeInsets.all(16.0), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), const Spacer(), Text( 'Audio Call', style: AppTextStyles.heading6.copyWith( color: Colors.white, fontWeight: FontWeight.w600, ), ), const Spacer(), const SizedBox(width: 48), // Balance the back button ], ), ); } Widget _buildContactInfo(BuildContext context, CallSession session) { final isPeerMuted = _callManager.isPeerMuted; return Column( children: [ Stack( alignment: Alignment.center, children: [ Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( shape: BoxShape.circle, color: AppColor.whiteF8d, border: Border.all(color: AppColor.white10.withOpacity(0.2), width: 1), ), child: ClipOval( child: session.peerAvatar != null ? CachedNetworkImage( imageUrl: session.peerAvatar!, fit: BoxFit.cover, placeholder: (context, url) => Center( child: CircularProgressIndicator( color: Colors.white.withOpacity(0.5), ), ), errorWidget: (context, url, error) => 'call_user_avatar'.toSvgAsset(height: 48, width: 48), ) : 'call_user_avatar'.toSvgAsset(height: 48, width: 48), ), ), // Show mute indicator when peer is muted if (isPeerMuted) Positioned( bottom: 0, right: 0, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColor.red30, shape: BoxShape.circle, border: Border.all(color: AppColor.white10, width: 2), ), child: 'mic_disable'.toSvgAsset( width: 16, height: 16, color: AppColor.white10, ), ), ), ], ), 24.height, Text( session.peerName, style: AppTextStyles.heading2.copyWith( color: Colors.white, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, ), // Show "Microphone is off" text when peer is muted if (isPeerMuted) Column( children: [ 8.height, Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: AppColor.red30.withOpacity(0.3), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ 'mic_disable'.toSvgAsset( width: 14, height: 14, color: AppColor.white10, ), 6.width, Text( 'Microphone is off', style: AppTextStyles.bodyText2.copyWith( color: AppColor.white10, fontSize: 12, ), ), ], ), ), ], ), ], ); } Widget _buildCallStatusOrDuration(BuildContext context) { final status = _callManager.callStatus; final duration = _callManager.callDuration; String statusText; if (status == CallStatus.connected) { // Show call duration final minutes = duration.inMinutes; final seconds = duration.inSeconds % 60; statusText = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } else { // Show call status statusText = _getStatusText(status); } return Text( statusText, style: AppTextStyles.bodyText.copyWith(color: Colors.white70), ); } String _getStatusText(CallStatus status) { switch (status) { case CallStatus.connecting: return 'Connecting...'; case CallStatus.incomingRinging: return 'Incoming call...'; case CallStatus.outgoingRinging: return 'Ringing...'; case CallStatus.connected: return 'Connected'; default: return ''; } } Widget _buildControls(BuildContext context, CallSession session) { final isMuted = _callManager.isMuted; final isSpeakerOn = _callManager.isSpeakerOn; return Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildControlButton( icon: isMuted ? Icons.mic_off : Icons.mic, label: isMuted ? 'Unmute' : 'Mute', onPressed: () => _callManager.toggleMute(), backgroundColor: isMuted ? Colors.red : Colors.white24, ), _buildControlButton( icon: isSpeakerOn ? Icons.volume_up : Icons.volume_down, label: isSpeakerOn ? 'Speaker' : 'Earpiece', onPressed: () => _callManager.toggleSpeaker(), backgroundColor: isSpeakerOn ? Colors.blue : Colors.white24, ), _buildControlButton( icon: Icons.call_end, label: 'End', onPressed: () => _callManager.hangUp(), backgroundColor: Colors.red, iconColor: Colors.white, ), ], ), ); } Widget _buildControlButton({ required IconData icon, required String label, required VoidCallback onPressed, Color? backgroundColor, Color? iconColor, }) { return Column( children: [ Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: backgroundColor ?? AppColor.black2E, shape: BoxShape.circle, border: Border.all(color: AppColor.white10.withOpacity(0.2)), ), child: Icon( icon, size: 24, color: iconColor ?? AppColor.white10, ), ).onPress(() { onPressed(); }), 8.height, Text( label, style: AppTextStyles.heading5.copyWith(color: AppColor.neutral100, fontWeight: FontWeight.w400), ), ], ); } }