onecxi_flutter_sdk 1.0.0+1 copy "onecxi_flutter_sdk: ^1.0.0+1" to clipboard
onecxi_flutter_sdk: ^1.0.0+1 copied to clipboard

A Flutter SDK for OneCxi calling functionality with WebSocket and VoIP support

example/lib/main.dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:onecxi_flutter_sdk/onecxi_flutter_sdk.dart';
import 'screens/splash_screen.dart';
import 'config/app_config.dart';
import 'services/registration_state_manager.dart';
import 'services/fcm_background_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() async {
  // Initialize Flutter binding first
  WidgetsFlutterBinding.ensureInitialized();

  // Register background message handler after binding is initialized
  FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);

  // Initialize background service BEFORE runApp (required for flutter_background_service)
  // This is Android-only and will be skipped on iOS
  print('๐Ÿ”ง [Main] Initializing background service before app starts...');
  try {
    await CallBackgroundService().initialize();
    print('โœ… [Main] Background service pre-initialized successfully');
  } catch (e) {
    print(
        'โš ๏ธ [Main] Background service pre-initialization failed (expected on iOS): $e');
  }

  runApp(const MyApp());
}

// Global navigation key for cross-screen navigation
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: AppConfig.appName,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      navigatorKey: navigatorKey, // Use global navigation key
      home: const InitialScreen(),
    );
  }
}

class InitialScreen extends StatefulWidget {
  const InitialScreen({super.key});

  @override
  State<InitialScreen> createState() => _InitialScreenState();
}

class _InitialScreenState extends State<InitialScreen>
    implements CallProgressCallback {
  final OneCxiSdk _sdk = OneCxiSdk();

  @override
  void initState() {
    super.initState();
    _checkForSkipSplash();
  }

  Future<void> _checkForSkipSplash() async {
    try {
      print('๐Ÿ” [InitialScreen] Checking for skip_splash flag...');

      // Check if there's a skip_splash flag (from Accept button)
      final prefs = await SharedPreferences.getInstance();
      final skipSplash = prefs.getBool('flutter.skip_splash') ?? false;

      print(
          '๐Ÿ” [InitialScreen] SharedPreferences flutter.skip_splash value: $skipSplash');

      // Also check Android SharedPreferences as backup
      final androidPrefs = await SharedPreferences.getInstance();
      final androidSkipSplash = androidPrefs.getBool('skip_splash') ?? false;

      print(
          '๐Ÿ” [InitialScreen] Android SharedPreferences skip_splash value: $androidSkipSplash');

      // Use either flag
      final shouldSkipSplash = skipSplash || androidSkipSplash;
      print(
          '๐Ÿ” [InitialScreen] Final decision - should skip splash: $shouldSkipSplash');

      if (shouldSkipSplash) {
        print(
            '๐Ÿ“ž [InitialScreen] Skip splash flag found - going directly to main screen');

        // Clear both flags
        await prefs.remove('flutter.skip_splash');
        await androidPrefs.remove('skip_splash');

        // Initialize SDK quickly and go to main screen
        await _initializeSDKForCall();

        // Navigate directly to main screen
        if (mounted) {
          Navigator.of(context).pushReplacement(
            MaterialPageRoute(builder: (context) => const MyHomePage()),
          );
        }
      } else {
        print(
            '๐Ÿ” [InitialScreen] No skip_splash flag - going to splash screen');

        // Navigate to splash screen
        if (mounted) {
          Navigator.of(context).pushReplacement(
            MaterialPageRoute(builder: (context) => const SplashScreen()),
          );
        }
      }
    } catch (e) {
      print('โŒ [InitialScreen] Error checking skip_splash: $e');

      // Fallback to splash screen
      if (mounted) {
        Navigator.of(context).pushReplacement(
          MaterialPageRoute(builder: (context) => const SplashScreen()),
        );
      }
    }
  }

  Future<void> _initializeSDKForCall() async {
    try {
      print('๐Ÿ”ง [InitialScreen] Initializing SDK for call...');

      // Initialize the SDK
      await _sdk.initialize(
        accountName: AppConfig.demoAccountName,
        apiKey: AppConfig.demoApiKey,
        serverName: AppConfig.demoServerName,
      );

      print('โœ… [InitialScreen] SDK initialized for call');

      // Request microphone permissions for call
      print('๐Ÿ”ง [InitialScreen] Requesting microphone permissions...');
      final hasPermissions = await _sdk.requestPermissions();
      print(
          '๐Ÿ”ง [InitialScreen] Microphone permissions granted: $hasPermissions');

      // Set up callback for call status updates
      print('๐Ÿ”ง [InitialScreen] Setting up call progress callback...');
      _sdk.setCallProgressCallback(this);

      // Check for call accept action and handle it immediately
      await _handleCallAcceptAction();
    } catch (e) {
      print('โŒ [InitialScreen] SDK initialization failed: $e');
    }
  }

  Future<void> _handleCallAcceptAction() async {
    try {
      print('๐Ÿ” [InitialScreen] Checking for call accept action...');

      // Use the SDK method to check for accept action
      final acceptAction = await _sdk.checkCallAcceptAction();

      print('๐Ÿ” [InitialScreen] SDK accept action result: $acceptAction');

      if (acceptAction != null) {
        if (acceptAction['action'] == 'accept') {
          print('๐Ÿ“ž [InitialScreen] Call ACCEPTED - auto-starting call');

          // Get call data from the result
          final callFrom = acceptAction['callFrom'] ?? '';
          final callTo = acceptAction['callTo'] ?? '';
          final callType = acceptAction['callType'] ?? 'OUTBOUND';
          final registeredNumber = acceptAction['registeredNumber'] ?? '';

          print(
              '๐Ÿ” [InitialScreen] Call data - From: $callFrom, To: $callTo, Type: $callType');

          // Clear the pending call data first to prevent showing UI again
          await _sdk.clearPendingIncomingCall();

          // Clear the accept action
          await _sdk.clearCallAcceptAction();

          // Start the call directly without showing UI
          await _startCallDirectly({
            'callFrom': callFrom,
            'callTo': callTo,
            'callType': callType,
            'registeredNumber': registeredNumber,
          });

          print('โœ… [InitialScreen] Call auto-started successfully');
        } else if (acceptAction['action'] == 'notification_tap') {
          print(
              '๐Ÿ“ฑ [InitialScreen] Notification tapped - opening app with call state');

          // Clear the action
          await _sdk.clearCallAcceptAction();

          // The app will open normally and refreshCallStatus() will sync the call state
          print('โœ… [InitialScreen] App will open with proper call state');
        }
      } else {
        print('๐Ÿ” [InitialScreen] No call accept action found');
      }
    } catch (e) {
      print('โŒ [InitialScreen] Error handling call accept action: $e');
      // On iOS, this is expected to fail since it's Android-specific
    }
  }

  Future<void> _startCallDirectly(Map<String, dynamic> callData) async {
    try {
      print('๐Ÿ“ž [InitialScreen] Starting call directly without UI...');

      // Extract call information
      final callFrom = callData['callFrom'] ?? '';
      final callTo = callData['callTo'] ?? '';
      final callType = callData['callType'] ?? 'OUTBOUND';
      final registeredNumber = callData['registeredNumber'] ?? '';

      print(
          '๐Ÿ“ž [InitialScreen] Call details - From: $callFrom, To: $callTo, Type: $callType');

      // Start the call directly using handleCallAnswered - SAME AS FOREGROUND CALL
      print(
          '๐Ÿ“ž [InitialScreen] Starting call using handleCallAnswered (same as foreground)...');
      print(
          '๐Ÿ“ž [InitialScreen] Call details - From: $callFrom, To: $callTo, Type: $callType, Registered: $registeredNumber');

      // Use handleCallAnswered directly - this is what foreground calls use
      await _sdk.handleCallAnswered({
        'callId': 'kill_state_call_${DateTime.now().millisecondsSinceEpoch}',
        'callFrom': callFrom,
        'callTo': callTo,
        'callType': callType,
        'registeredNumber': registeredNumber,
      });

      print(
          'โœ… [InitialScreen] Call started directly and will continue in background');
    } catch (e) {
      print('โŒ [InitialScreen] Error starting call directly: $e');
    }
  }

  // CallProgressCallback implementation
  @override
  void onCallStatusChanged(CallStatus status) {
    print(
        '๐Ÿ“ฑ [InitialScreen] onCallStatusChanged called - isCallOngoing: ${status.isCallOngoing}');
  }

  @override
  void onCallStarted(String callType, String did, String registeredNumber) {
    print(
        '๐Ÿ“ฑ [InitialScreen] onCallStarted called - callType: $callType, did: $did');
  }

  @override
  void onCallEnded(String callType, String did) {
    print(
        '๐Ÿ“ฑ [InitialScreen] onCallEnded called - callType: $callType, did: $did');
    // Call ended - no specific action needed for initial screen
  }

  @override
  void onCallError(String error, String? callType) {
    print(
        '๐Ÿ“ฑ [InitialScreen] onCallError called - error: $error, callType: $callType');
  }

  @override
  void onWebSocketStatusChanged(bool isConnected) {
    print(
        '๐Ÿ“ฑ [InitialScreen] onWebSocketStatusChanged called - isConnected: $isConnected');
  }

  @override
  void onCallDeclined(String callType, String callFrom) {
    print(
        '๐Ÿ“ฑ [InitialScreen] onCallDeclined called - callType: $callType, callFrom: $callFrom');
  }

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with WidgetsBindingObserver
    implements CallProgressCallback {
  final OneCxiSdk _sdk = OneCxiSdk();
  bool _isInitialized = false;
  bool _isCallActive = false;
  bool _isMuted = false;
  bool _isOnHold = false;
  bool _isSpeakerOn = false;

  // Call timer variables
  Timer? _callTimer;
  int _callDurationSeconds = 0;
  String _formattedCallDuration = '00:00';

  // Controllers for app-to-app calls only
  final TextEditingController _appToAppNumberController =
      TextEditingController();
  final TextEditingController _registeredNumberController =
      TextEditingController();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _setupSDK();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _callTimer?.cancel();
    _appToAppNumberController.dispose();
    _registeredNumberController.dispose();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);

    if (state == AppLifecycleState.resumed) {
      print('๐Ÿ“ฑ [MyHomePage] App resumed - syncing call status...');
      _syncCallStatus();
    }
  }

  Future<void> _syncCallStatus() async {
    try {
      await _sdk.refreshCallStatus();
      final currentStatus = _sdk.getCallStatus();

      print(
          '๐Ÿ“ฑ [MyHomePage] Synced call status: isCallOngoing=${currentStatus.isCallOngoing}');

      setState(() {
        _isCallActive = currentStatus.isCallOngoing;
        _isMuted = currentStatus.isMuted;
        _isOnHold = currentStatus.isOnHold;
        _isSpeakerOn = currentStatus.isSpeakerOn;
      });

      // Start timer if call is active but timer is not running
      if (_isCallActive && _callTimer == null) {
        print(
            '๐Ÿ“ฑ [MyHomePage] Call active but timer not running - starting timer');
        _startCallTimer();
      }

      // Stop timer if call is not active but timer is running
      if (!_isCallActive && _callTimer != null) {
        print(
            '๐Ÿ“ฑ [MyHomePage] Call not active but timer running - stopping timer');
        _stopCallTimer();
      }
    } catch (e) {
      print('โš ๏ธ [MyHomePage] Error syncing call status: $e');
    }
  }

  Future<void> _setupSDK() async {
    try {
      print('๐Ÿ“ฑ [MyHomePage] Setting up SDK and callback...');

      // SDK is already initialized from splash screen, just set callback and permissions
      _sdk.setCallProgressCallback(this);

      final hasPermissions = await _sdk.requestPermissions();

      // Check if there's already an active call (from kill state flow)
      print(
          '๐Ÿ“ฑ [MyHomePage] Refreshing call status to sync with kill state call...');
      await _sdk.refreshCallStatus();

      // Get the current call status to update UI
      final currentStatus = _sdk.getCallStatus();
      print(
          '๐Ÿ“ฑ [MyHomePage] Current call status: isCallOngoing=${currentStatus.isCallOngoing}');

      setState(() {
        _isInitialized = true;
        _isCallActive = currentStatus.isCallOngoing;
        _isMuted = currentStatus.isMuted;
        _isOnHold = currentStatus.isOnHold;
        _isSpeakerOn = currentStatus.isSpeakerOn;
      });

      // If there's an active call, start the timer
      if (_isCallActive && _callTimer == null) {
        print('๐Ÿ“ฑ [MyHomePage] Active call detected, starting timer...');
        _startCallTimer();
      }

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
              content: Text(hasPermissions
                  ? AppConfig.successSdkReadyWithPermissions
                  : AppConfig.successSdkReadyWithoutPermissions)),
        );
      }
    } catch (e) {
      print('โŒ [MyHomePage] SDK setup error: $e');
      // Don't show error to user if it's just Android-specific methods failing on iOS
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(
            SnackBar(content: Text('${AppConfig.errorSdkSetupFailure}: $e')));
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(AppConfig.appTitle),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(AppConfig.defaultPadding),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _buildStatusCard(),
            const SizedBox(height: AppConfig.defaultPadding),
            _buildAppToAppInput(),
            const SizedBox(height: AppConfig.defaultPadding),
            _buildCallButtons(),
            if (_isCallActive) ...[
              const SizedBox(height: AppConfig.defaultPadding),
              _buildCallControls(),
            ],
            const SizedBox(height: AppConfig.defaultPadding),
            _buildIncomingCallButton(),
          ],
        ),
      ),
    );
  }

  Widget _buildStatusCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(AppConfig.defaultPadding),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Text('Ozonetel',
                    style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                          fontWeight: FontWeight.bold,
                          color: Colors.blue,
                        )),
                const SizedBox(width: 8),
                const _StatusIndicator(),
              ],
            ),
            const SizedBox(height: 8),
            Text('Ozonetel OneCXI App',
                style: Theme.of(context).textTheme.titleMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                    )),
            const SizedBox(height: AppConfig.defaultPadding),
            _CallStatusText(isCallActive: _isCallActive),
            const SizedBox(height: 8),
            _CallDurationText(duration: _formattedCallDuration),
            const SizedBox(height: 8),
            _StatusInfoList(
              isInitialized: _isInitialized,
              isCallActive: _isCallActive,
              isMuted: _isMuted,
              isOnHold: _isOnHold,
              isSpeakerOn: _isSpeakerOn,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildAppToAppInput() {
    return Column(
      children: [
        TextField(
          controller: _appToAppNumberController,
          decoration: const InputDecoration(
            labelText: AppConfig.uiTextAppToAppNumber,
            border: OutlineInputBorder(),
            hintText: AppConfig.defaultAppToAppNumberPlaceholder,
          ),
        ),
        const SizedBox(height: 8),
        TextField(
          controller: _registeredNumberController,
          decoration: const InputDecoration(
            labelText: AppConfig.uiTextRegisteredNumber,
            border: OutlineInputBorder(),
            hintText: AppConfig.defaultRegisteredNumberPlaceholder,
          ),
        ),
      ],
    );
  }

  Widget _buildCallButtons() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        // Call Button
        Column(
          children: [
            Container(
              width: AppConfig.callButtonSize,
              height: AppConfig.callButtonSize,
              decoration: BoxDecoration(
                color: _isCallActive ? Colors.grey : Colors.green,
                borderRadius:
                    BorderRadius.circular(AppConfig.callButtonSize / 2),
                boxShadow: [
                  BoxShadow(
                    color: (_isCallActive ? Colors.grey : Colors.green)
                        .withValues(alpha: 0.3),
                    blurRadius: 8,
                    offset: const Offset(0, 4),
                  ),
                ],
              ),
              child: IconButton(
                onPressed: _isCallActive ? null : _startCall,
                icon: const Icon(Icons.call,
                    color: Colors.white, size: AppConfig.callButtonIconSize),
                iconSize: AppConfig.callButtonIconSize,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              AppConfig.uiTextCall,
              style: TextStyle(
                color: _isCallActive ? Colors.grey : Colors.green,
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
        // HangUp Button
        Column(
          children: [
            Container(
              width: AppConfig.callButtonSize,
              height: AppConfig.callButtonSize,
              decoration: BoxDecoration(
                color: _isCallActive ? Colors.red : Colors.grey,
                borderRadius:
                    BorderRadius.circular(AppConfig.callButtonSize / 2),
                boxShadow: [
                  BoxShadow(
                    color: (_isCallActive ? Colors.red : Colors.grey)
                        .withValues(alpha: 0.3),
                    blurRadius: 8,
                    offset: const Offset(0, 4),
                  ),
                ],
              ),
              child: IconButton(
                onPressed: _isCallActive ? _endCall : null,
                icon: const Icon(Icons.call_end,
                    color: Colors.white, size: AppConfig.callButtonIconSize),
                iconSize: AppConfig.callButtonIconSize,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              AppConfig.uiTextHangup,
              style: TextStyle(
                color: _isCallActive ? Colors.red : Colors.grey,
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
      ],
    );
  }

  Widget _buildCallControls() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        _buildControlButton(
          icon: Icons.volume_up,
          label: AppConfig.uiTextSpeaker,
          isActive: _isSpeakerOn,
          onPressed: () => _toggleSpeaker(),
        ),
        _buildControlButton(
          icon: Icons.mic_off,
          label: AppConfig.uiTextMute,
          isActive: _isMuted,
          onPressed: () => _toggleMute(),
        ),
        _buildControlButton(
          icon: Icons.pause,
          label: AppConfig.uiTextHold,
          isActive: _isOnHold,
          onPressed: () => _toggleHold(),
        ),
        _buildControlButton(
          icon: Icons.dialpad,
          label: AppConfig.uiTextDialPad,
          isActive: false,
          onPressed: () => _showDialPad(),
        ),
      ],
    );
  }

  Widget _buildControlButton({
    required IconData icon,
    required String label,
    required bool isActive,
    required VoidCallback onPressed,
  }) {
    return Column(
      children: [
        Container(
          width: AppConfig.controlButtonSize,
          height: AppConfig.controlButtonSize,
          decoration: BoxDecoration(
            color: isActive ? Colors.blue : Colors.grey.withValues(alpha: 0.1),
            borderRadius: BorderRadius.circular(AppConfig.defaultBorderRadius),
            border: Border.all(
              color:
                  isActive ? Colors.blue : Colors.grey.withValues(alpha: 0.3),
              width: 1,
            ),
          ),
          child: IconButton(
            onPressed: onPressed,
            icon: Icon(
              icon,
              color: isActive ? Colors.white : Colors.grey[600],
              size: AppConfig.controlButtonIconSize,
            ),
            iconSize: AppConfig.controlButtonIconSize,
          ),
        ),
        const SizedBox(height: 4),
        Text(
          label,
          style: TextStyle(
            color: isActive ? Colors.blue : Colors.grey[600],
            fontSize: 11,
            fontWeight: FontWeight.w500,
          ),
        ),
      ],
    );
  }

  Widget _buildIncomingCallButton() {
    return Container(
      width: double.infinity,
      height: 56,
      margin: const EdgeInsets.symmetric(horizontal: AppConfig.defaultPadding),
      child: ElevatedButton(
        onPressed: _isCallActive ? null : _simulateIncomingCall,
        style: ElevatedButton.styleFrom(
          backgroundColor: _isCallActive ? Colors.grey : Colors.orange,
          foregroundColor: Colors.white,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(AppConfig.defaultBorderRadius),
          ),
          elevation: 2,
        ),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              _isCallActive ? Icons.call_end : Icons.call_received,
              size: 20,
            ),
            const SizedBox(width: 8),
            Text(
              _isCallActive
                  ? AppConfig.uiTextCallActive
                  : AppConfig.uiTextSimulateIncomingCall,
              style: const TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
      ),
    );
  }

  // Call Methods
  Future<void> _startCall() async {
    print('๐Ÿ“ž [Main] _startCall called');
    print('๐Ÿ“ž [Main] Platform: ${Platform.isIOS ? "iOS" : "Android"}');

    try {
      // Check permissions before starting call
      print('๐Ÿ“ž [Main] Checking permissions...');
      final hasPermissions = await _sdk.checkForPermissions();
      print('๐Ÿ“ž [Main] Permissions result: $hasPermissions');

      if (!hasPermissions) {
        _showError(AppConfig.errorMissingMicrophonePermission);
        return;
      }

      final appToAppNumber = _appToAppNumberController.text.trim();

      // Get the actual registered number from registration service
      final registrationStateManager = RegistrationStateManager();
      final actualRegisteredNumber =
          await registrationStateManager.getUserNumber();
      final registeredNumber =
          actualRegisteredNumber ?? AppConfig.defaultRegisteredNumber;

      print('๐Ÿ“ž [Main] Using registered number: $registeredNumber');

      if (appToAppNumber.isEmpty) {
        // Empty field = Inbound call (like iOS reference)
        print('๐Ÿ“ž [Main] Starting inbound call with DID: ${AppConfig.demoDid}');
        await _sdk.startInBoundCall(
          did: AppConfig.demoDid,
          registeredNumber: registeredNumber,
        );
        print('๐Ÿ“ž [Main] Inbound call started successfully');
      } else {
        // Filled field = App-to-App call (like iOS reference)
        // Prevent self-calling
        if (appToAppNumber == registeredNumber) {
          print(
              'โŒ [Main] Cannot call yourself! Please enter a different number.');
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text(
                  'Cannot call yourself! Please enter a different number.'),
              backgroundColor: Colors.red,
            ),
          );
          return;
        }

        print('๐Ÿ“ž [Main] Starting app-to-app call to: $appToAppNumber');
        print('๐Ÿ“ž [Main] From registered number: $registeredNumber');
        print(
            '๐Ÿ“ž [Main] Validation check - appToAppNumber: "$appToAppNumber", registeredNumber: "$registeredNumber"');
        print(
            '๐Ÿ“ž [Main] Are they equal? ${appToAppNumber == registeredNumber}');
        await _sdk.startAppToAppInboundCall(
          inputNumber: appToAppNumber,
          registeredNumber: registeredNumber,
        );
        print('๐Ÿ“ž [Main] App-to-app call started successfully');
      }
    } catch (e) {
      print('๐Ÿ“ž [Main] Call failed: $e');
      _showError('Failed to start call: $e');
    }
  }

  Future<void> _endCall() async {
    try {
      await _sdk.endCall();
    } catch (e) {
      _showError('Failed to end call: $e');
    }
  }

  Future<void> _simulateIncomingCall() async {
    try {
      // Get the actual registered number from registration service (same as _startCall)
      final registrationStateManager = RegistrationStateManager();
      final actualRegisteredNumber =
          await registrationStateManager.getUserNumber();
      final registeredNumber =
          actualRegisteredNumber ?? AppConfig.defaultRegisteredNumber;

      print(
          '๐Ÿงช [Main] Using registered number for simulation: $registeredNumber');
      print('๐Ÿงช [Main] Controller text: "${_registeredNumberController.text}"');
      print(
          '๐Ÿงช [Main] Controller text isEmpty: ${_registeredNumberController.text.isEmpty}');

      // Simulate a VoIP push notification payload
      final pushPayload = {
        'callFrom': AppConfig.demoDid,
        'callTo': registeredNumber, // Always use actual registered number
        'callType': 'Outbound',
        'callerName': 'Test Caller',
        'registeredNumber':
            registeredNumber, // Always use actual registered number
        'callId': 'call_${DateTime.now().millisecondsSinceEpoch}_test',
        'environment': 'DEVELOPMENT',
        'timestamp': DateTime.now().millisecondsSinceEpoch,
        'platform': 'ios',
        'appType': 'Flutter',
        'event': 'initial',
      };

      print('๐Ÿงช [Main] Simulating incoming call with payload: $pushPayload');

      // Use the new flutter_callkit_incoming integration
      await _sdk.handleIncomingCallWithUI(pushPayload);

      print('โœ… [Main] Incoming call simulation completed');
    } catch (e) {
      print('โŒ [Main] Failed to simulate incoming call: $e');
      _showError('Failed to simulate incoming call: $e');
    }
  }

  void _toggleSpeaker() async {
    // Optimistic UI update for better responsiveness
    setState(() {
      _isSpeakerOn = !_isSpeakerOn;
    });

    try {
      await _sdk.setSpeaker(_isSpeakerOn);
    } catch (e) {
      // Revert on error
      setState(() {
        _isSpeakerOn = !_isSpeakerOn;
      });
      _showError('${AppConfig.errorSpeakerToggleFailure}: $e');
    }
  }

  void _toggleMute() async {
    // Optimistic UI update for better responsiveness
    setState(() {
      _isMuted = !_isMuted;
    });

    try {
      await _sdk.muteCall(_isMuted);
    } catch (e) {
      // Revert on error
      setState(() {
        _isMuted = !_isMuted;
      });
      _showError('${AppConfig.errorMuteToggleFailure}: $e');
    }
  }

  void _toggleHold() async {
    // Optimistic UI update for better responsiveness
    setState(() {
      _isOnHold = !_isOnHold;
    });

    try {
      await _sdk.holdCall(_isOnHold);
    } catch (e) {
      // Revert on error
      setState(() {
        _isOnHold = !_isOnHold;
      });
      _showError('${AppConfig.errorHoldToggleFailure}: $e');
    }
  }

  void _showDialPad() {
    // TODO: Implement dial pad
    _showError(AppConfig.errorDialPadNotImplemented);
  }

  void _showError(String message) {
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(message)),
      );
    }
  }

  // CallProgressCallback implementation
  @override
  void onCallStatusChanged(CallStatus status) {
    print(
        '๐Ÿ“ฑ [Main] onCallStatusChanged called - isCallOngoing: ${status.isCallOngoing}');

    // Stop timer if call becomes inactive
    if (!status.isCallOngoing && _isCallActive) {
      print('๐Ÿ“ฑ [Main] Call became inactive - stopping timer and resetting UI');
      _stopCallTimer();

      // Also reset all UI states immediately
      setState(() {
        _isCallActive = false;
        _isMuted = false;
        _isOnHold = false;
        _isSpeakerOn = false;
      });
      print('๐Ÿ“ฑ [Main] UI reset - Call Active: false, Timer: 00:00');
      return; // Exit early to avoid setting state twice
    }

    // Update state first
    setState(() {
      _isCallActive = status.isCallOngoing;
      _isMuted = status.isMuted;
      _isOnHold = status.isOnHold;
      _isSpeakerOn = status.isSpeakerOn;
    });
    print(
        '๐Ÿ“ฑ [Main] Flutter app state updated - _isCallActive: $_isCallActive');

    // Start timer if call becomes active and timer is not running
    if (status.isCallOngoing && _callTimer == null) {
      print('๐Ÿ“ฑ [Main] Call became active - starting timer');
      _startCallTimer();
    }
  }

  @override
  void onCallStarted(String callType, String did, String registeredNumber) {
    _startCallTimer(); // Start the timer when call begins
  }

  @override
  void onCallEnded(String callType, String did) {
    print('๐Ÿ“ž [Main] onCallEnded called - callType: $callType, did: $did');

    // Stop the timer FIRST
    _stopCallTimer();
    print('๐Ÿ“ž [Main] Timer stopped in onCallEnded');

    // Update UI state to reflect call ended
    setState(() {
      _isCallActive = false;
      _isMuted = false;
      _isOnHold = false;
      _isSpeakerOn = false;
    });
    print('๐Ÿ“ฑ [Main] Call ended - UI state reset');
  }

  @override
  void onCallError(String error, String? callType) {
    _stopCallTimer(); // Stop timer on error to reset the display
    _showError('Call error: $error');
  }

  @override
  void onWebSocketStatusChanged(bool isConnected) {
    // WebSocket status changed
  }

  @override
  void onCallDeclined(String callType, String callFrom) {
    print('๐Ÿ“ž [Main] Call declined: $callType from $callFrom');
    // Handle call declined
  }

  // Call timer methods
  void _startCallTimer() {
    // Don't start timer if call is not active
    if (!_isCallActive) {
      print('๐Ÿ“ž [MyHomePage] Not starting timer - call not active');
      return;
    }

    // Don't start timer if one is already running
    if (_callTimer != null) {
      print('๐Ÿ“ž [MyHomePage] Timer already running - not starting new one');
      return;
    }

    print('๐Ÿ“ž [MyHomePage] Starting new call timer');
    _callDurationSeconds = 0;
    _formattedCallDuration = AppConfig.defaultCallDurationFormat;

    _callTimer = Timer.periodic(
        const Duration(seconds: AppConfig.callTimerUpdateInterval),
        (timer) async {
      // Check if call is still active locally
      if (!_isCallActive) {
        print('๐Ÿ“ž [MyHomePage] Call not active - stopping timer');
        _stopCallTimer();
        return;
      }

      // Periodically check native call status to sync UI
      if (Platform.isAndroid) {
        try {
          await _sdk.refreshCallStatus();
          final currentStatus = _sdk.getCallStatus();

          // If native says call is not active but UI shows active, stop timer
          if (!currentStatus.isCallOngoing && _isCallActive) {
            print('๐Ÿ“ž [MyHomePage] Native call ended - stopping timer');
            _stopCallTimer();
            return;
          }
        } catch (e) {
          print('โš ๏ธ [MyHomePage] Error checking native status: $e');
        }
      }

      // Increment timer
      _callDurationSeconds++;
      _formattedCallDuration = _formatDuration(_callDurationSeconds);
      print('โฑ๏ธ [MyHomePage] Timer tick - Duration: $_formattedCallDuration');

      setState(() {
        // Trigger UI update
      });
    });
  }

  void _stopCallTimer() {
    print('๐Ÿ“ž [MyHomePage] _stopCallTimer called - timer: $_callTimer');

    if (_callTimer != null) {
      _callTimer!.cancel();
      _callTimer = null;
      print('๐Ÿ“ž [MyHomePage] Timer cancelled and set to null');
    } else {
      print('๐Ÿ“ž [MyHomePage] Timer was already null');
    }

    setState(() {
      _callDurationSeconds = 0;
      _formattedCallDuration = AppConfig.defaultCallDurationFormat;
    });

    print('๐Ÿ“ž [MyHomePage] Timer stopped - duration reset to 00:00');
  }

  String _formatDuration(int seconds) {
    final minutes = seconds ~/ 60;
    final remainingSeconds = seconds % 60;
    return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
  }
}

// Optimized stateless widgets to reduce rebuilds
class _StatusIndicator extends StatelessWidget {
  const _StatusIndicator();

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 8,
      height: 8,
      decoration: const BoxDecoration(
        color: Colors.green,
        shape: BoxShape.circle,
      ),
    );
  }
}

class _CallStatusText extends StatelessWidget {
  final bool isCallActive;

  const _CallStatusText({required this.isCallActive});

  @override
  Widget build(BuildContext context) {
    return Text(
      isCallActive
          ? AppConfig.uiTextCallInProgress
          : AppConfig.uiTextReadyForCall,
      style: Theme.of(context).textTheme.bodyLarge,
    );
  }
}

class _CallDurationText extends StatelessWidget {
  final String duration;

  const _CallDurationText({required this.duration});

  @override
  Widget build(BuildContext context) {
    return Text(
      duration,
      style: Theme.of(context).textTheme.headlineSmall,
    );
  }
}

class _StatusInfoList extends StatelessWidget {
  final bool isInitialized;
  final bool isCallActive;
  final bool isMuted;
  final bool isOnHold;
  final bool isSpeakerOn;

  const _StatusInfoList({
    required this.isInitialized,
    required this.isCallActive,
    required this.isMuted,
    required this.isOnHold,
    required this.isSpeakerOn,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Initialized: $isInitialized'),
        Text('Call Active: $isCallActive'),
        Text('Muted: $isMuted'),
        Text('On Hold: $isOnHold'),
        Text('Speaker: $isSpeakerOn'),
      ],
    );
  }
}