# Firebase Integration

## Introduction

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

Make sure you created Firebase provider and configured push notification manager as described in [this section](https://getstream.io/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.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.

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

<Admonition type="note">

Ringing events are exposed by the [stream_video_push_notification](https://pub.dev/packages/stream_video_push_notification) package 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 Ringing events:

```dart
import 'package:rxdart/rxdart.dart';

final _compositeSubscription = CompositeSubscription();

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

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

  // You can use our helper method to observe core Ringing events
  // It will handled call accepted, declined and ended events
  _compositeSubscription.add(
      streamVideo.observeCoreRingingEvents(
        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 (iOS)
  // _compositeSubscription.add(streamVideo.onRingingEvent<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 Ringing events during the ringing
    final subscription = streamVideo.observeCoreRingingEventsForBackground();

    // 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](https://getstream.io/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](https://getstream.io/video/docs/flutter/advanced/troubleshooting/) section for solutions to common mistakes.


---

This page was last updated at 2026-08-19T08:17:20.786Z.

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/).