FCM Multiple Notifications When Android App In Background
A solution to FCM multiple notifications being sent to user device when Android app is in background providing negative user experience.

In this post I will describe an issue I had with FCM multiple notifications when the Android app is in the background and how I resolved it.

Resolve FCM Multiple Notifications In Android

Please view the posts below to help provide general setup steps to implement Firebase Cloud Messaging.

Push Notifications With Firebase Cloud Messaging From PHP

Use FCM In Android Compose to Receive Push Notifications

Use Android Datastore and FCM For Realtime App Updates

FCM States

There are two states the library behaves differently to

ForegroundWhen the app is running in the foreground, you would use the onMessageReceived callback to process the data.
BackgroundThe system tray is sent the notification with the data in the intent bundle.

Multiple FCM Background Notifications

Using FCM in my project worked as expected except when the app was in the background. No matter what PendingIntent flags I changed the parameters to, the same issue kept happening. I was processing all the data from the payload and creating the notification in the onMessageReceived callback. This was to reason for the issue. It was working perfect when the app was in the foreground ( based on the chart above, this is correct ) but not in the background.

I read through a few Stackoverflow posts and reread the FCM documentation on handling messages.

I found a post about overriding the handleIntent in the FirebaseMessagingService from this Stackoverflow post.

FCM Push Notification Android receiving 2 notifications in the background

This helped me correct my issue and resolve my FCM multiple notifications when the app is in the background.

Code

Below I will be posting the relevant code needed to help implement / resolve this issue. I will post the old then the new in each section I changed.

FirebaseMessagingService

onMessageReceived

Old Method - Issue with background notifications

Reading the data payload will be different in this method and the new method. Take note of how to extract the data from the RemoteMessage data type.

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)
        //not all payloads received in project will need to create notification, this checks just for single value then creates notification
        if(message.data["item_id"] == "<value>"){
            message.notification?.let { createDataNotification(message, it) }
        }
    }

handleIntent

New Method - fixes the issue

Using this method, the notifications now appropriately work for both background and foreground notifications.

Extracting the data payload will be a little different. In the old method, you would be supplied a RemoteMessage data type to get the data but the new method will extract it from the intent. We will be following a different key naming convention. I found this post to help me do a quick loop of all keys in the intent to extract the ones I needed.

FirebaseMessagingService 11.6.0 HandleIntent

    if (intent?.getExtras() != null) {
        for (String key : intent?.getExtras().keySet()) {
            Object value = intent?.getExtras().get(key);
            Log.d(TAG, "Key: " + key + " Value: " + value);
        }
    }

For my situation, my PHP server sending the payload is sending it as:

FCM Payload Sent From PHP Server
            $payload = [
                'title' => $title,
                'body' => $body,
            ];
    
            //custom data fields
            $data = [
                'id' => "1"
            ];
    
            $fcm = [
                'message' => [
                'token' => $dev_token,
                    'notification' => $payload,
                    'data' => $data
                ],
            ];

Example to get body parameter from payload:

old way using RemoteMessagemessage: RemoteMessage.Notification
then read the body from the message with message.body.toString()
new way just using Intentintent.getStringExtra("gcm.notification.body")
Note

You will need to comment out the super.handleIntent(intent) call. This is because we are taking full control of the callback.

    override fun handleIntent(intent: Intent?) {
        //super.handleIntent(intent)
          if(intent?.hasExtra("id") == false) {
              createDataNotificationFromIntent(
                  intent.getStringExtra("gcm.notification.body"),
                  intent.getStringExtra("gcm.notification.title"))
          } else {
              if(intent?.getStringExtra("id") == "1") {
                    createDataNotificationFromIntent(
                        intent.getStringExtra("gcm.notification.body"),
                        intent.getStringExtra("gcm.notification.title"))
              }
          }
    }

createDataNotificationFromIntent

This is a simple custom function to send the data needed to create a notification to my notification builder.

    private fun createDataNotificationFromIntent(id: String?, body: String, title : String) {
        MyNotificationBuilder.createWithDataFromIntent(
            this@MyMessagingService,
            applicationContext.resources.getInteger(R.integer.fcm_notification)+id.toInt(),
            getString(R.string.default_notification_channel_id),
            id,
            body,
            title,
            "FCM Title",
            "FCM Desc",
            NotificationManager.IMPORTANCE_DEFAULT
        )
    }

MyNotificationBuilder

I figured I would share this class as well. This is my notification builder for the project.

object MyNotificationBuilder {

    fun createWithDataFromIntent(
        context: Context,
        notifyID : Int,
        channelId: String,
        id: String,
        body: String,
        title: String,
        channelName: String,
        channelDesc: String,
        importance: Int
    ) {

        val notificationManager =
            context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationManager.createNotificationChannel(
                createNotificationChannel(
                    channelId,
                    channelName,
                    channelDesc,
                    importance
                )
            )
        }
        var pendingIntent: PendingIntent

        //if id is set, then send to activity on user click else no activity added to intent
        if (id != null) {
            Log.d("Testing data payload", list_id.toString())
            val intent : Intent
            if(id != "1") {
                intent = Intent(context, OtherActivity::class.java)
                    .apply {
                        putExtra("id", id)
                        setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
                    }
            } else {
                intent = Intent(context, Activity::class.java)
            }
            pendingIntent =
                PendingIntent.getActivity(context, 0, intent,
                    PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
        } else {
            pendingIntent =
                PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)

        }

        val builder = NotificationCompat.Builder(context, channelId)
            //.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher_new))
            .setSmallIcon(R.drawable.ic_notification_foreground)
            .setColor(Color(0xFFAAEDFF).toArgb())
            .setContentTitle(title)
            .setContentText(content)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
        notificationManager.notify(notifyID, builder.build())
    }

    @SuppressLint("NewApi")
    private fun createNotificationChannel(
        channelId: String,
        channelName: String,
        channelDesc: String,
        importance: Int
    ): NotificationChannel {
        val channel = NotificationChannel(channelId, channelName, importance).apply {
            description = channelDesc
        }
        return channel
    }
}

Hope this was helpful.