5

I used firebase to build My project.
It will also use the FCM (firebase cloud message).
But there is a problem.
I can't handle the FCM (create my custom notificaion) when app is in background.

The official site tutorial said that
case 1: App foreground -> override the "onMessageReceived()" to create your custom notification.
case 2: App background -> System will create the notification directly. We needn't and can't do anything. Because it doesn't trigger the "onMessageReceived()" in this case.

However if I can do nothing when app is background, I can't create my custom notification. (e.g. After Users click the notification and it will pop up a window to show detail information.)

So how do I handle notifications with FCM when app is in background?

Leon Chang
  • 471
  • 1
  • 4
  • 14
  • 1
    Do you have a question, or are you just saying how you chose to handle messages in your own app? – Doug Stevenson Feb 21 '18 at 04:08
  • @Doug Stevenson I have the problem before. then I got the answer after I traced the Firebase Library code. – Leon Chang Feb 21 '18 at 04:10
  • Hi Leon. You could actually post a question like post and your actual answer. The way it is right now, it might confuse some community members. – AL. Feb 21 '18 at 04:30
  • @LeonChang check this, it will solve your problem:https://stackoverflow.com/questions/48301350/android-notifications-when-app-is-in-background/48301893#48301893. If it does tell me so we can mark it as a duplicate, thank you! – Peter Haddad Feb 22 '18 at 06:51
  • @LeonChang you can send a notification in the background and the content of the notification will be written there also (means you do not need to click it to open the app and see the content) , check the link above to know how. – Peter Haddad Feb 22 '18 at 07:04
  • @Peter Haddad I do set a Log in onMessageReceived(). But I found that onMessageReceived does not be trigger. I also have traced the library source code (FirebaseMessagingService.class - handleIntent() -> zzv.class - zzv()). – Leon Chang Feb 22 '18 at 07:16
  • @LeonChang yes that is because you are not using `data` payload in the backend as said in the answer. You need to use `data` payload alone and in FCM `onMessageRecieved()` you need to recieve the data payload as in the answer also. Example: You need to write this in `onMessageRecieved()` so it gets trigger `body = remoteMessage.getData().get("bodys"); //bodys is the attribute name` – Peter Haddad Feb 22 '18 at 07:19
  • @Peter Haddad I tried it. Although I change the library to lastest verion (11.8.0) and add the "data" payload. But it doesn't work. – Leon Chang Feb 22 '18 at 08:02

5 Answers5

17

There is a bad news.
Google change the Firebase source code in version 'com.google.firebase:firebase-messaging:11.6.0'.
handelIntent is "public final void method" now. which means we can't override it .
If you want to use the solution, change the version to be "com.google.firebase:firebase-messaging:11.4.2"



Try my way. It can perfectly work on the project build version is Android 6.0 above(api level 23) and I have tried it already.

There is better way than official site tutorial

The official site said that the notification will be created by system when app is in background. So you can't handle it by overriding the "onMessageReceived()". Because the "onMessageReceived()" is only triggered when app is in foreground.

But the truth is not. Actually the notificaions (when app is in background) are created by Firebase Library.

After I traced the firebase library code. I find a better way.

Step 1. Override the "handleIntent()" instead of "onMessageReceived()" in FirebaseMessagingService
why:
Because the method will be trigger either app is in foreground or the background. So we can handle FCM message and create our custom notifications in both cases.

@Override
public void handleIntent(Intent intent) {
    Log.d( "FCM", "handleIntent ");
}


Step 2. Parse the message from FCM
how:
If you don't know the format of the message you set. Print it and try to parse it.
Here is the basic illustration

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


Step 2. Remove the notifications created by Firebase library when the app is in background
why:
We can create our custom notification. But the notification created by Firebase Library will still be there (Actually it created by ""super.handleIntent(intent)"". There is detail explaination below.). Then we'll have two notifcations. That is rather weird. So we have to remove the notificaion created by Firebase Library

how (project build level is Android 6.0 above):
Recognize the notifications which we want to remove and get the informaion. And use the "notificationManager.cancel()" to remove them.

private void removeFirebaseOrigianlNotificaitons() {

    //check notificationManager is available
    NotificationManager notificationManager = 
        (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    if (notificationManager == null )
        return;

    //check api level for getActiveNotifications()
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
        //if your Build version is less than android 6.0
        //we can remove all notifications instead. 
        //notificationManager.cancelAll();
        return;
    }


    //check there are notifications
    StatusBarNotification[] activeNotifications = 
        notificationManager.getActiveNotifications();
    if (activeNotifications == null)
        return;

    //remove all notification created by library(super.handleIntent(intent))
    for (StatusBarNotification tmp : activeNotifications) {
        Log.d("FCM StatusBarNotification", 
            "StatusBarNotification tag/id: " + tmp.getTag() + " / " + tmp.getId());
        String tag = tmp.getTag();
        int id = tmp.getId();

        //trace the library source code, follow the rule to remove it.
        if (tag != null && tag.contains("FCM-Notification"))
            notificationManager.cancel(tag, id);
    }
}

The my whole sample code:

public class MyFirebaseMessagingService extends FirebaseMessagingService {

private static int notificationCount=0;

@Override
public void handleIntent(Intent intent) {
    //add a log, and you'll see the method will be triggered all the time (both foreground and background).
    Log.d( "FCM", "handleIntent");

    //if you don't know the format of your FCM message,
    //just print it out, and you'll know how to parse it
    Bundle bundle = intent.getExtras();
    if (bundle != null) {
        for (String key : bundle.keySet()) {
            Object value = bundle.get(key);
            Log.d("FCM", "Key: " + key + " Value: " + value);
        }
    }

    //the background notification is created by super method
    //but you can't remove the super method. 
    //the super method do other things, not just creating the notification
    super.handleIntent(intent);

    //remove the Notificaitons
    removeFirebaseOrigianlNotificaitons();

    if (bundle ==null)
        return;

    //pares the message
    CloudMsg cloudMsg = parseCloudMsg(bundle);

    //if you want take the data to Activity, set it
    Bundle myBundle = new Bundle();
    myBundle.putSerializable(TYPE_FCM_PLATFORM, cloudMsg);
    Intent myIntent = new Intent(this, NotificationActivity.class);
    myIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    myIntent.putExtras(myBundle);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, notificationCount, myIntent, PendingIntent.FLAG_UPDATE_CURRENT);

    //set the Notification
    NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
            .setSmallIcon(R.mipmap.icon)
            .setContentTitle(cloudMsg.getTitle())
            .setContentText(cloudMsg.getMessage())
            .setAutoCancel(true)
            .setContentIntent(pendingIntent);

    NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    notificationManager.notify(notificationCount++, notificationBuilder.build());
}



/**
 * parse the message which is from FCM
 * @param bundle
 */
private CloudMsg parseCloudMsg(Bundle bundle) {
    String title = null, msg=null;

    //if the message is sent from Firebase platform, the key will be that
    msg = (String) bundle.get("gcm.notification.body");

    if(bundle.containsKey("gcm.notification.title"))
    title = (String) bundle.get("gcm.notification.title");

    //parse your custom message
    String testValue=null;
    testValue =  (String) bundle.get("testKey");

    //package them into a object(CloudMsg is your own structure), it is easy to send to Activity.
    CloudMsg cloudMsg = new CloudMsg(title, msg, testValue);
    return cloudMsg;
}


/**
 * remove the notification created by "super.handleIntent(intent)"
 */
    private void removeFirebaseOrigianlNotificaitons() {

    //check notificationManager is available
    NotificationManager notificationManager = 
        (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    if (notificationManager == null )
        return;

    //check api level for getActiveNotifications()
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
        //if your Build version is less than android 6.0
        //we can remove all notifications instead. 
        //notificationManager.cancelAll();
        return;
     }

    //check there are notifications
    StatusBarNotification[] activeNotifications = 
        notificationManager.getActiveNotifications();
    if (activeNotifications == null)
        return;

    //remove all notification created by library(super.handleIntent(intent))
    for (StatusBarNotification tmp : activeNotifications) {
        Log.d("FCM StatusBarNotification", 
            "tag/id: " + tmp.getTag() + " / " + tmp.getId());
        String tag = tmp.getTag();
        int id = tmp.getId();

        //trace the library source code, follow the rule to remove it.
        if (tag != null && tag.contains("FCM-Notification"))
            notificationManager.cancel(tag, id);
    }
}
}
Leon Chang
  • 471
  • 1
  • 4
  • 14
  • 4
    This solution is not good. If your app is not running, you will not receive antyhing. This is why Google has removed it from api, it's not needed. Correct way is to use firebase notication (as I described below) or sync adapter/background service. – user1209216 Feb 22 '18 at 08:31
  • 1
    Maybe you can trace the library source ( handleIntent()). The firebase notication you mention is also create by the super.handleIntent(). So if the "handleIntent()" isn't triggered when app is in backgournd. how do firebase library create the "firebase notication" – Leon Chang Feb 23 '18 at 03:16
  • I guess it's handled by Google Play Services, not your app itself – user1209216 Feb 23 '18 at 07:18
  • 1
    How about that? do me a favor. Just do a simple test. you override handleIntent() but do not call super.handleIntent() and try to send your FCM. If you have time, reply it after you finish the test. – Leon Chang Feb 23 '18 at 09:16
  • I guess I can't, my project uses FCM 11.8.0 and I don't want downgrading now – user1209216 Feb 23 '18 at 09:24
  • @LeonChang - This is great. How did this affect Firebase statistics though? – JoshuaTree Mar 29 '18 at 08:45
  • I did not get any additional notification however, but I can confirm this did work. – JoshuaTree Mar 29 '18 at 09:01
  • just delele line super.handleIntent(intent) then u can modify notify by your own. Tested. – kemdo Apr 06 '18 at 09:03
  • @kemdo deleting the super method is not suggested. Because you didn't trace sources code. you don't know what exactly things they will do in the super method. That's why I didn't do it. – Leon Chang Apr 07 '18 at 09:46
  • @JoshuaTree I am not sure what will be infected with Firebase statistic. So I did some examinations. The conclusion is the below. You could see the Firebase notification console. There are two values they statistic. one is Target estimate, it seems works normally. but the other value, open rate, works unusually. And some articles said that both statistical values are be statistic by Firebase Analysis. But I still can't find how do they do that. Yeah, it really amazing that app-running notifications are created by ourselves, but Firebase still could trace that notification is opened or not. – Leon Chang Apr 07 '18 at 10:03
  • Thanks, yeah I could not get opened rate working. I did end up using events to manually track these though. – JoshuaTree Apr 07 '18 at 10:12
  • 1
    This is a clever idea. However it looks like in 17.x.x version of firebase-messaging library, FirebaseMessagingService no longer has handleIntent()? – Kevin Oct 17 '18 at 06:12
  • I am able to once again override handleIntent in the most recent versions of the Firebase SDK (Firebase BOM 29.1.0, so FCM version 23.0?). Is this workaround viable again? – Sapph Feb 26 '22 at 01:37
3

However if I can do nothing when app is background, I can't create my custom notification. (e.g. After Users click the notification and it will pop up a window to show detail information.)

So how do I handle notifications with FCM when app is in background?

First, you need to create correct message payload that you send to fcm server. Example:

{
  "to": "topic_name",
  "priority": "high",
  "data": {
    "field1": "field1 value" 
    "field2": "field2 value" 
  }

  "notification" : {
      "body" : "Lorem ipsum",
      "title" : "sampke title" 
      "click_action": "SHOW_DETAILS" 
    }
}

data payload is actual data you want to show as message details after user clicks on notification, notification payload represents how generated notification should look (there are much more attributes possible to set), you don't need to build notification by yourself, you only need to set it properties here.

To show your activity after user taps on notication, you need to set intent filter corresponding to click_action:

<intent-filter>
     <action android:name="SHOW_DETAILS"/>
     <category android:name="android.intent.category.DEFAULT"/>
 </intent-filter>

so activity that have above intent filter will be launched automatically when user taps to notification. Last step is to retrieve data when activity is launched after notification tap. It's pretty easy. Custom data is passed to activity via bundle. Inside onCreate method for your activity do something like that:

Bundle bundle = getIntent().getExtras();
if(bundle.getString("action").equals("SHOW_DETAILS")) /*This indicates activity is launched from notification, not directly*/
{
 //Data retrieved from notification payload send 
 String filed1 = bundle.getString("field1");
 String filed2 = bundle.getString("field2");
}

All of above is valid if app is not running or it's in background. If your app is foreground, no notification will be created. Instead, you will receive onMessageReceived() event so you can handle the same data there (I guess you know how).

Reference:

https://firebase.google.com/docs/cloud-messaging/http-server-ref https://github.com/firebase/quickstart-android/tree/master/messaging

animuson
  • 53,861
  • 28
  • 137
  • 147
user1209216
  • 7,404
  • 12
  • 60
  • 123
  • Thank for explanations. but how do you handle it when you send a cloud message with FCM console? creating a web UI to let controller send it? – Leon Chang Feb 21 '18 at 15:50
  • AFAIK you can't set click action with Firebase Console. To send message from your computer, I recommend curl – user1209216 Feb 21 '18 at 15:57
  • How do I handle "the background situation" like "onMessageReceived()"? I want to do some things before create the Notification. Such as, set JobScheduler, report something to server, record something into database. If users don't click the notification and just swipe it, how do I set and do these things? – Leon Chang Feb 22 '18 at 03:39
  • You can't. If you really need that, use sync adapter as @Ibrahim mentioned above and send silent push, without notification payload. This requires some more work though, you need to setup sync adapter, content provider and build notification by yourself (actually, sync adapter will create notification, not your app). – user1209216 Feb 22 '18 at 07:52
  • check this, to see how to use data payload and recieve it in `onMessageRecieved()`: https://stackoverflow.com/questions/48301350/android-notifications-when-app-is-in-background/48301893#48301893 – Peter Haddad Mar 01 '18 at 09:53
2

You need to use FCM data messages in order to create custom notification in a android app.Even your app is in background, onMessageReceived will be called, so you can process the data and show the custom notification.

https://firebase.google.com/docs/cloud-messaging/android/receive

Data message format which has to be sent from server:

{"message":{
"token":"Your Device Token",
"data":{
  "Nick" : "Mario",
  "body" : "great match!",
  "Room" : "PortugalVSDenmark"
}
}
}
Rajesh.k
  • 2,327
  • 1
  • 16
  • 19
  • I think if we do that the people who are using Firebase for both iOS and Android will have issues with the iOS part since iOS in my knowledge needs the notification block in the JSON. Correct me if I am wrong!! – FreakyAli Jan 02 '20 at 07:26
  • I think so, but did you find any solution.. for both android, ios – Md Tariqul Islam Jul 07 '20 at 06:15
0

FCM Won't send a background notification if your app is killed any more, and as you described in your answer about the handleIntent() solution It may work for some devices and for some old version of the FCM, also if you @override method that doesn't described in the official doc's of firebase you may struggle some problems here, and you use it on your own risk!.

What is the solution?

You need to use your own push-notification-service beside FCM like Telegram.

OR using SyncAdapter beside GCM like Gmail.

So if you need it to work successfully like those apps, you have to use your own hack.

-1
public class FirebaseMessageReceiver extends FirebaseMessagingService{
    private static final String TAG = "main";
    String s12;
    String channel_id = "general";
    Intent intent;

    @Override
    public void onNewToken(@NonNull String token)
    {
        Log.d(TAG, "Refreshed token: " + token);
    }
    @Override
    public void
    onMessageReceived(RemoteMessage remoteMessage) {
        s12=remoteMessage.getNotification().getClickAction();
        Log.d("tttt",(remoteMessage.getData().toString()));
        Log.d("ttttttt",(remoteMessage.getNotification().toString()));
        if (remoteMessage.getNotification() != null) {
            showNotification(remoteMessage.getNotification().getTitle(), remoteMessage.getNotification().getBody());
        }
        //
    }

    public void handleIntent(Intent intent)
    {
        try
        {
            if (intent.getExtras() != null)
            {
                RemoteMessage.Builder builder = new RemoteMessage.Builder("FirebaseMessageReceiver");

                for (String key : intent.getExtras().keySet())
                {
                    builder.addData(key, intent.getExtras().get(key).toString());
                }

                onMessageReceived(builder.build());
            }
            else
            {
                super.handleIntent(intent);
            }
        }
        catch (Exception e)
        {
            super.handleIntent(intent);
        }
    }

    private RemoteViews getCustomDesign(String title, String message) {
        RemoteViews remoteViews = new RemoteViews(getApplicationContext().getPackageName(), R.layout.notification);
        remoteViews.setTextViewText(R.id.title111, title);
        remoteViews.setTextViewText(R.id.message111, message);
        remoteViews.setImageViewResource(R.id.icon111, R.drawable.favicon);
        return remoteViews;
    }
    // Method to display the notifications
    public void showNotification(String title, String message) {
        intent  = new Intent(android.content.Intent.ACTION_VIEW, Uri.parse(s12));
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent notifyIntent = PendingIntent.getActivity(this, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
        Log.d("notifyy",notifyIntent.toString());
        NotificationCompat.Builder builder
                = new NotificationCompat
                .Builder(getApplicationContext(),
                channel_id)
                .setSmallIcon(R.drawable.favicon)
                .setAutoCancel(true)
                .setVibrate(new long[]{1000, 1000, 1000, 1000, 1000})
                .setOnlyAlertOnce(true)
                .setContentIntent(notifyIntent);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            builder = builder.setContent(getCustomDesign(title, message));
        }
        else {
            builder = builder.setContentTitle(title)
                    .setContentText(message)
                    .setSmallIcon(R.drawable.favicon);
        }
        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        // Check if the Android Version is greater than Oreo
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel notificationChannel = new NotificationChannel(channel_id, "web_app",
                    NotificationManager.IMPORTANCE_HIGH);
            notificationManager.createNotificationChannel(
                    notificationChannel);
        }
        notificationManager.notify(0, builder.build());
    } 
}
Saby
  • 718
  • 9
  • 29