# Firebase Integration

## Introduction

With FCM integration, we enable the ringing flow by handling push messages and displaying custom notifications using the [flutter_callkit_incoming](https://pub.dev/packages/flutter_callkit_incoming) package.

Make sure you created Firebase provider and configured push notification manager as described in [this section](/video/docs/flutter/advanced/incoming-calls/providers-configuration/).

### Add native permissions

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

```xml
<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`:

```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`:

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

### Handling CallKit events (common for iOS and Android)

<admonition type="note">

CallKit events are exposed by the [flutter_callkit_incoming](https://pub.dev/packages/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.

</admonition>

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

```dart
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:

```dart
StreamVideo.instance.pushNotificationManager?.endAllCalls();
```

### Listen to push notifications

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

```dart
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.

<admonition type="note">

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.

</admonition>

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

```dart
// 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:

```dart
_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:

```dart
@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](https://pub.dev/packages/permission_handler) package.

Remember to follow [official best practices](https://developer.android.com/develop/ui/views/notifications/notification-permission#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](https://source.android.com/docs/core/permissions/fsi-limits) 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:

```dart
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](/video/docs/flutter/advanced/incoming-calls/ringing/)).
- 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](/video/docs/flutter/advanced/troubleshooting/) section for solutions to common mistakes.

---

This page was last updated at 2025-08-15T14:16:38.039Z.

For the most recent version of this documentation, visit [https://getstream.io/video/docs/flutter/advanced/incoming-calls/android-firebase-integration/](https://getstream.io/video/docs/flutter/advanced/incoming-calls/android-firebase-integration/).