Firebase Integration

Introduction

With FCM integration, we enable the ringing flow by handling push messages and displaying custom notifications using the flutter_callkit_incoming package.

Make sure you created Firebase provider and configured push notification manager as described in this section.

Add native permissions

Add the following permissions to allow camera, audio, and network access:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-feature android:name="android.hardware.camera"/>
    <uses-feature android:name="android.hardware.camera.autofocus"/>

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>

    <!-- Bluetooth permissions for audio routing -->
    <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

    <!-- Required for displaying call notifications -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

</manifest>

Set android:launchMode to singleInstance

Update your MainActivity declaration in AndroidManifest.xml:

<manifest...>
     ...
   <application ...>
       <activity ...
          android:name=".MainActivity"
          android:launchMode="singleInstance">
        ...
   ...
 </manifest>

This ensures that tapping the push notification does not create a new instance of your app. Instead, it brings the existing instance to the foreground, preventing multiple screens from stacking up when accepting calls.

Prevent Obfuscation in proguard-rules.pro

To avoid issues with call-related keys being obfuscated, add this rule to android/app/proguard-rules.pro:

 -keep class com.hiennv.flutter_callkit_incoming.** { *; }

Handling CallKit events (common for iOS and Android)

CallKit events are exposed by the flutter_callkit_incoming package that we utilize to handle incoming calls on both iOS and Android. It is important to handle these events to ensure a seamless calling experience regardless of which provider is used for push.

In a high-level widget in your app, add this code to listen to CallKit events:

import 'package:rxdart/rxdart.dart';

final _compositeSubscription = CompositeSubscription();

@override
void initState() {
  ...
  _observeCallKitEvents()
}

void _observeCallKitEvents() {
  final streamVideo = StreamVideo.instance;

  // You can use our helper method to observe core CallKit events
  // It will handled call accepted, declined and ended events
  _compositeSubscription.add(
      streamVideo.observeCoreCallKitEvents(
        onCallAccepted: (callToJoin) {
            // Replace with navigation flow of your choice
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => CallScreen()),
            );
        },
      ),
    );

  // Or you can handle them by yourself, and/or add additional events such as handling mute events from CallKit
  // _compositeSubscription.add(streamVideo.onCallKitEvent<ActionCallToggleMute>(_onCallToggleMute));
}

@override
void dispose() {
  // ...
  _compositeSubscription.cancelAll();
}

If you need to manage the ringing flow call, you can use the StreamVideo.pushNotificationManager. As an example, let’s say you want to end all calls, you can end them this way:

StreamVideo.instance.pushNotificationManager?.endAllCalls();

Listen to push notifications

In a high-level widget in your app, add this code to listen to FCM messages:

import 'package:rxdart/rxdart.dart';

final _compositeSubscription = CompositeSubscription();

@override
void initState() {
  ...
  _observeFcmMessages()
}

_observeFcmMessages() {
  _compositeSubscription.add(
      FirebaseMessaging.onMessage.listen(_handleRemoteMessage),
  );
}

Future<void> _handleRemoteMessage(RemoteMessage message) async {
  await StreamVideo.instance.handleRingingFlowNotifications(message.data);
}

@override
void dispose() {
  // ...
  _compositeSubscription.cancelAll();
}

The handleRingingFlowNotifications() method will show custom notification indicating ringing call. It will also handle call.missed push by showing dedicated notification if you want to handle it by yourself set handleMissedCall parameter to false.

Handle push in background and terminated state

When you app is in the background special handling is required. We need to register a handler method that will be called by system when push is received even when app is not running.

We recommend storing user credentials locally when the user logs in so you can automatically set up the user when a push notification is received in background.

Add the following code as top lever functions (for example on top of your main.dart file):

// As this runs in a separate isolate, we need to setup the app again.
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // Initialise Firebase
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  try {
    // Get stored user credentials
    final tutorialUser = await AppInitializer.getStoredUser();
    if (tutorialUser == null) return;

    // Use the `create` factory to create an instance separate from the `StreamVideo.instance` singleton
    final streamVideo = StreamVideo.create(
      ...,
      // Make sure you initialise push notification manager
      pushNotificationManagerProvider: StreamVideoPushNotificationManager.create(
        iosPushProvider: const StreamVideoPushProvider.apn(
          name: 'your-ios-provider-name',
        ),
        androidPushProvider: const StreamVideoPushProvider.firebase(
          name: 'your-fcm-provider',
        ),
        pushParams: const StreamVideoPushParams(
          appName: kAppName,
          ios: IOSParams(iconName: 'IconMask'),
        ),
      ),
    )..connect();

    // Ensure proper handling of CallKit events during the ringing
    final subscription = streamVideo.observeCallDeclinedCallKitEvent();

    // Dispose this instance after ringing is resolved
    streamVideo.disposeAfterResolvingRinging(
      disposingCallback: () => subscription?.cancel(),
    );

    // Handle the push notification
    await streamVideo.handleRingingFlowNotifications(message.data);
  } catch (e, stk) {
    debugPrint('Error handling remote message: $e');
    debugPrint(stk.toString());
  }
}

Now register this handler in FirebaseMessaging instance. You can do it for example inside the _observeFcmMessages() method we created in a previous step:

_observeFcmMessages() {
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  _compositeSubscription.add(
      FirebaseMessaging.onMessage.listen(_handleRemoteMessage),
  );
}

In case the call was accepted when the app was terminated we also need to consume it.

In a high-level widget, add this method and call it from the initState() method:

@override
void initState() {
  //...
  _tryConsumingIncomingCallFromTerminatedState();
}

void _tryConsumingIncomingCallFromTerminatedState() {
  // This is only relevant for Android.
  if (CurrentPlatform.isIos) return;

  WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
    StreamVideo.instance.consumeAndAcceptActiveCall(
      onCallAccepted: (callToJoin) {
        // Replace with navigation flow of your choice
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => CallScreen()),
        );
      },
    );
  });
}

Request notification permission from user

For Android 13+ you need to request the POST_NOTIFICATIONS permission. You can do it using the permission_handler package.

Remember to follow official best practices (especially showing prompt before the request).

Make sure permission to send full-screen notifications is granted

For Android 14+ on some devices, the full-screen intent permission might not be granted, preventing the ringing notification from appearing when the screen is locked.

We expose a dedicated method to make sure this permission is granted:

StreamVideoPushNotificationManager.ensureFullScreenIntentPermission();

In case it is not granted, the user will be taken to the app’s settings page to enable full-screen notifications.

You should now be able to receive a ringing call on Android.

To test this:

  • Create a ringing call on another device (as describe in previous section).
  • Add the ID of a user logged into the Android device to the memberIds array in the call.getOrCreate(ringing: true, memberIds: [{ID}]) method.
  • You should see the custom ringing notification show on the Android device.

If you encounter any issues, refer to the Troubleshooting section for solutions to common mistakes.

© Getstream.io, Inc. All Rights Reserved.