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
Foreground | When the app is running in the foreground, you would use the onMessageReceived callback to process the data. |
Background | The 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 RemoteMessage | message: RemoteMessage.Notification then read the body from the message with message.body.toString() |
new way just using Intent | intent.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.