Mahammad Azimzada
Mahammad Azimzada

Reputation: 121

Why isn’t CallKit working when the app is closed? How can I trigger it correctly with VoIP Push Notifications in Flutter?

I’m implementing CallKit in my Flutter app, but I’m encountering an issue with handling incoming calls when the app is completely closed (not running in the background). The app works fine when it is in the foreground or background—the CallKit triggers and the call screen appears when accepting an incoming call. However, when the app is closed (completely not running in the background), even if I receive an incoming call via a VoIP Push Notification, the CallKit doesn’t trigger, and the app doesn’t navigate to the call screen. I’ve set up VoIP Push Notifications to wake the app up, but CallKit does not work as expected when the app is completely closed. What have I tried? 1. Enabling Background Modes: I enabled Background Fetch and Push Notifications in Xcode to ensure the app is able to handle VoIP Push Notifications even when the app is closed. 2. Handling VoIP Push Notifications: I ensured that the app can receive VoIP Push Notifications and that the notification handler is properly configured to wake up the app. 3. Using CallKit with Push Notifications: I’ve implemented CallKit to handle incoming calls, but it works only when the app is in the background or foreground.
Expected Results:

I expected that when a VoIP Push Notification is received while the app is closed, the app would wake up, show the CallKit screen, and transition to the call page where I can accept the call.

Actual Results:

When the app is closed, CallKit does not trigger, and the app doesn’t navigate to the call screen, even after receiving the VoIP Push Notification.

Additional Information:

I’ve tried to follow the documentation for CallKit and VoIP Push Notifications on iOS, but I still can’t get the app to work properly when it is fully closed. If anyone has experience with handling VoIP calls and CallKit in Flutter (or even with native iOS code), I would appreciate any guidance or suggestions on how to resolve this issue.

import 'dart:async';
import 'dart:developer' as dev;
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:awesome_notifications/awesome_notifications.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_callkit_incoming/entities/entities.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:legalis/AppUtil.dart';
import 'package:legalis/l10n/support_locale.dart';
import 'package:legalis/notify_controller.dart';
import 'package:legalis/provider/auth/auth_provider.dart';
import 'package:legalis/provider/call/call_provider.dart';
import 'package:legalis/provider/category/category_provider.dart';
import 'package:legalis/provider/chat/chat_detail_provider.dart';
import 'package:legalis/provider/communication/communication_provider.dart';
import 'package:legalis/provider/home/lawyer_report_provider.dart';
import 'package:legalis/provider/language_provider.dart';
import 'package:legalis/provider/localization/localization_provider.dart';
import 'package:legalis/provider/message/message_list_provider.dart';
import 'package:legalis/provider/navigation/navigation_provider.dart';
import 'package:legalis/provider/service/user_service_provider.dart';
import 'package:legalis/provider/user/user_provider.dart';
import 'package:legalis/screens/call/call_page.dart';
import 'package:legalis/screens/pages/home/home_page.dart';
import 'package:legalis/static/call_accept_observer.dart';
import 'package:logger/logger.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import 'firebase_options.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Bu dosya tüm providerları içeriyor.
import 'package:legalis/screens/splash_screen.dart';
import 'package:legalis/screens/auth/login_screen.dart';
import 'package:legalis/screens/pages/message/message_page.dart';
import 'package:legalis/screens/pages/profile/profile_page.dart';
import 'package:legalis/screens/pages/call/call_history.dart';
import 'package:legalis/wrapper/main_wrapper.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

int getUniqueNotificationId() {
  var num = Random().nextInt(2000);
  AppUtil.setNotification(num);
  return num;
}

GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // Initialize navigatorKey at the start
  navigatorKey = GlobalKey<NavigatorState>();

// Initialize CallKit event handler
  Widget? initialRoute;

  // Add CallKit listener initialization here
  await _initCallKitListener();

  FirebaseMessaging messaging = FirebaseMessaging.instance;

  // Set background message handler only once
  FirebaseMessaging.onBackgroundMessage(handleNotification);

  // Configure foreground message handling
  FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
    dev.log('Foreground message received');
    await handleNotification(message);
  });

  // Configure message handling when app is opened from terminated state
  FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
    dev.log('App opened from terminated state with message');
    await handleNotification(message);
  });

  await Future.delayed(const Duration(seconds: 1));
  if (Platform.isIOS) {
    NotificationSettings settings = await messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );

    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      String? apnsToken = await messaging.getAPNSToken();
      Logger().t('APNS Token: $apnsToken');
      if (apnsToken != null) {
        print('APNS Token: $apnsToken');
      }
    }
  }

  // Get FCM token only once
  String? fcmToken = await messaging.getToken();
  Logger().t('FCM Token: $fcmToken');

  // Remove duplicate call
  // FirebaseMessaging.onBackgroundMessage(handleNotification);

  AwesomeNotifications().initialize(
    null,
    [
      NotificationChannel(
        channelKey: 'basic_channel',
        channelName: 'Basic notifications',
        channelDescription: 'Notification channel for basic tests',
        defaultColor: const Color(0xFF9D50DD),
        ledColor: Colors.white,
      ),
      NotificationChannel(
        channelKey: 'call_channel',
        channelName: 'Call notifications',
        channelDescription: 'Notification channel for call notifications',
        defaultColor: Colors.orange,
        ledColor: Colors.white,
        importance: NotificationImportance.Max,
        channelShowBadge: true,
        locked: false,
        playSound: true,
        defaultRingtoneType: DefaultRingtoneType.Ringtone,
      ),
      NotificationChannel(
        channelKey: 'message_channel',
        channelName: 'Message notifications',
        channelDescription: 'Notification channel for message notifications',
        defaultColor: Colors.blue,
        ledColor: Colors.white,
        importance: NotificationImportance.High,
        channelShowBadge: true,
      ),
    ],
  );
  await FirebaseMessaging.instance.setAutoInitEnabled(true);
  // Firebase Dynamic Links

  runApp(MyApp(page: initialRoute));
}

@pragma('vm:entry-point')
Future<void> handleNotification(RemoteMessage message) async {
  Logger().d('Remote Message  ${message.toString()}');
  Logger().d('Remote Message  ${message.data.toString()}');
  String? title = message.data['sender_name'] ?? 'Yeni Mesaj';
  String? body = message.data['body'];
  String channelKey = message.data['channel_key'] ?? 'default_channel';
  String? id = message.data['id'] ?? '0';

  if (channelKey == 'call_channel') {
    // Clear any existing calls first
    await FlutterCallkitIncoming.endAllCalls();

    final uuid = const Uuid().v4();
    final params = CallKitParams(
      id: uuid,
      nameCaller: title,
      appName: 'Legalis',
      avatar: message.data['caller_avatar'] ?? '',
      handle: body ?? '',
      type: 0,
      duration: 30000,
      textAccept: Platform.isIOS ? 'Accept' : 'Qəbul et',
      textDecline: Platform.isIOS ? 'Decline' : 'Rədd et',
      extra: {
        ...message.data,
        'timestamp': DateTime.now().toIso8601String(),
        'uuid': uuid, // Add unique identifier
      },
      headers: {},
      android: const AndroidParams(
          isCustomNotification: true,
          isShowLogo: false,
          ringtonePath: 'system_ringtone_default',
          backgroundColor: '#0955fa',
          backgroundUrl: '',
          actionColor: '#4CAF50',
          incomingCallNotificationChannelName: "Incoming Call",
          missedCallNotificationChannelName: "Missed Call"),
      ios: const IOSParams(
        iconName: 'CallKitLogo',
        handleType: 'generic',
        supportsVideo: false,
        maximumCallGroups: 1, // Limit to 1 call group
        maximumCallsPerCallGroup: 1,
        audioSessionMode: 'voicechat',
        audioSessionActive: true,
        audioSessionPreferredSampleRate: 44100.0,
        audioSessionPreferredIOBufferDuration: 0.005,
        supportsDTMF: true,
        supportsHolding: true,
        supportsGrouping: false,
        supportsUngrouping: false,
      ),
    );

    try {
      await FlutterCallkitIncoming.showCallkitIncoming(params);
    } catch (e) {
      Logger().e('Error showing CallKit: $e');
    }
  } else if (channelKey == 'message_channel') {
    if (AppUtil.connectionUserId != id) {
      await AwesomeNotifications().createNotification(
        content: NotificationContent(
            id: getUniqueNotificationId(),
            channelKey: channelKey,
            color: Colors.blue,
            title: title,
            body: body,
            category: NotificationCategory.Message,
            backgroundColor: Colors.blue,
            payload: {'message-api-id': id, 'username': title}),
        actionButtons: [
          NotificationActionButton(
            key: 'READ',
            label: 'Read Message',
            color: Colors.green,
          ),
          NotificationActionButton(
            key: 'DISMISS',
            label: 'Dismiss',
            color: Colors.red,
          ),
        ],
        localizations: {
          // Azərbaycanca
          'az': NotificationLocalization(buttonLabels: {
            'READ': 'Mesajı oxu',
            'DISMISS': 'İmtina et',
          }),
          // EN
          'en': NotificationLocalization(
            buttonLabels: {
              'READ': 'Read Message',
              'DISMISS': 'Dismiss',
            },
          ),
          // Rus
          'ru': NotificationLocalization(
            buttonLabels: {
              'READ': 'Прочитать сообщение',
              'DISMISS': 'Отклонить',
            },
          ),
        },
      );
    }
  }

  AwesomeNotifications().setListeners(
    onActionReceivedMethod: (ReceivedAction receivedAction) async {
      AppUtil.init();
      AppUtil.setNotification(receivedAction.id);
      NotificationController.onActionReceivedMethod(receivedAction);
    },
    onNotificationCreatedMethod:
        (ReceivedNotification receivedNotification) async {
      AppUtil.setNotification(receivedNotification.id);
      NotificationController.onNotificationCreatedMethod(receivedNotification);
    },
    onNotificationDisplayedMethod:
        (ReceivedNotification receivedNotification) async {
      NotificationController.onNotificationDisplayedMethod(
          receivedNotification);
    },
    onDismissActionReceivedMethod: (ReceivedAction receivedAction) async {
      NotificationController.onDismissActionReceivedMethod(receivedAction);
    },
  );
}

@pragma('vm:entry-point')
Future<void> _initCallKitListener() async {
  try {
    FlutterCallkitIncoming.onEvent.listen((event) async {
      if (event?.event == null || event?.body == null) return;

      Logger().i('CallKit Event: ${event?.event}, Body: ${event?.body}');
      final data = event?.body;
      if (data == null) return;

      switch (event?.event) {
        case Event.actionCallAccept:
          if (AppUtil.activeCallUser) {
            Logger().d('Call already active, ignoring accept action');
            return;
          }
          AppUtil.activeCallUser = true;

          if (navigatorKey.currentState == null) {
            await Future.delayed(const Duration(seconds: 2));
          }

          // Navigate to CallPage and remove all previous routes
          await navigatorKey.currentState?.pushAndRemoveUntil(
            MaterialPageRoute(
              builder: (context) => CallPage(
                userName: data['nameCaller'],
                callId: data['extra']?['callId'],
                recieverId: data['extra']?['userId'] ?? '',
                accep: true,
              ),
            ),
            (route) => false, // Remove all previous routes
          );
          break;

        case Event.actionCallDecline:
          await AppUtil.endCallAsync(callId: data['extra']?['callId']);
          await FlutterCallkitIncoming.endAllCalls();
          break;

        case Event.actionCallEnded:
          if (AppUtil.activeCallUser) {
            await AppUtil.endCallAsync(callId: data['extra']?['callId']);
            await FlutterCallkitIncoming.endAllCalls();
            AppUtil.activeCallUser = false;
          }
          break;

        case Event.actionCallIncoming:
          AppUtil.activeCallUser = false;
          break;

        default:
          break;
      }
    });
  } catch (e, stackTrace) {
    Logger().e("Error in call listener", error: e, stackTrace: stackTrace);
  }
}

// ignore: must_be_immutable
class MyApp extends StatefulWidget {
  final Widget? page;
  const MyApp({super.key, this.page});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {  
  @override
  void initState() {
    super.initState();
    // Remove _listenerCallKit call since we already initialized it in main()
    // _listenerCallKit();

    AwesomeNotifications().setListeners(
        onActionReceivedMethod: NotificationController.onActionReceivedMethod,
        onNotificationCreatedMethod:
            NotificationController.onNotificationCreatedMethod,
        onNotificationDisplayedMethod:
            NotificationController.onNotificationDisplayedMethod,
        onDismissActionReceivedMethod:
            NotificationController.onDismissActionReceivedMethod);
  }

  @override
  Widget build(BuildContext context) {
    // Fbs
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => MessageListProvider()),
        ChangeNotifierProvider(create: (_) => ChatDetailProvider()),
        ChangeNotifierProvider(create: (_) => LawyerReportProvider()),
        ChangeNotifierProvider(create: (_) => UserServiceProvider()),
        ChangeNotifierProvider(create: (_) => CallProvider()),
        ChangeNotifierProvider(create: (_) => CallAcceptProvider()),
        ChangeNotifierProvider(create: (_) => NavigationProvider()),
        ChangeNotifierProvider(create: (_) => LocalizationProvider()),
        ChangeNotifierProvider(create: (_) => CommunicationProvider()),
        ChangeNotifierProvider(create: (_) => AuthProvider()),
        ChangeNotifierProvider(create: (_) => CategoryProvider()),
        ChangeNotifierProvider(create: (_) => LanguageProvider()),
        ChangeNotifierProvider(create: (_) => UserProvider()),
      ],
      child: Consumer<LocalizationProvider>(
        builder: (context, provider, child) {
          return MaterialApp(
            onGenerateRoute: (settings) {
              Logger().i('Route: ${settings}');
            },
            title: 'Legalis',
            navigatorKey: navigatorKey,
            localizationsDelegates: const [
              AppLocalizations.delegate,
              GlobalMaterialLocalizations.delegate,
              GlobalWidgetsLocalizations.delegate,
              GlobalCupertinoLocalizations.delegate,
            ],
            locale: provider.locale,
            supportedLocales: L10n.support,
            theme: ThemeData(
              bottomSheetTheme: const BottomSheetThemeData(
                  backgroundColor: Color(0xFF000E2B)),
              pageTransitionsTheme: const PageTransitionsTheme(
                builders: {
                  TargetPlatform.android: CupertinoPageTransitionsBuilder(),
                  TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
                },
              ),
              fontFamily: 'SF-Pro-Display',
              primarySwatch: Colors.green,
              appBarTheme: const AppBarTheme(
                backgroundColor: Color(0xFF000E2B),
                elevation: 0,
                iconTheme: IconThemeData(color: Colors.white),
                titleTextStyle: TextStyle(
                  color: Colors.white,
                  fontSize: 24,
                  fontWeight: FontWeight.w700,
                ),
              ),
              scaffoldBackgroundColor: Colors.white,
            ),
            home: widget.page ?? const SplashPage(),
            routes: {
              '/login': (context) => const LoginScreen(),
              '/home': (context) => const HomePageView(),
              '/messagePage': (context) => const MessagePage(),
              '/profilePage': (context) => const ProfilePage(),
              '/callHistory': (context) => const CallHistory(),
              '/mainWrapper': (context) => MainWrapper(),
            },
          );
        },
      ),
    );
  }
}

Upvotes: 0

Views: 76

Answers (0)

Related Questions