Firebase Cloud Messaging - txgz999/Mobile GitHub Wiki

  • a*****Spring2021

Firebase Cloud Messaging (FCM) is a service provided by Google that facilitates messaging between mobile client apps (e.g. a Xamarin.Android app) and server applications (e.g. ASP.NET API site).

Set Up Firebase Cloud Messaging

Follow the following Microsoft document to set up FCM:

In Firebase Console, log in using a Google account

  • Create a Firebase project
  • Register the Android app: select the Firebase project, and add the Android package name there (multiple apps can be added to one Firebase project)
  • Download the generated config file: google-services.json, we need to include it in the Android app project
Credentials

A few credentials are used by FCM:

  • Sender ID: identifies the Firebase project, created with the project
  • API Key (also called the Web API key and the Server Key): used by the server application to connect to FCM, created with the Firebase project
  • App ID: identifies the mobile client app, created when registering the app
  • Registration Token (also called the Instance ID): identifies an instance of the mobile client app on a given device, created at run time

The google-services.json contains the sender ID (in project_info/project_number), the App ID (in client_info/mobilesdk_app_id) and the API Key (in api_key/current_key).

Enable Client App to Receive Message

Follow the following Microsoft document to enable Android App (Xamarin) projects or Mobile App (Xamarin.Forms) projects to support FCM:

Create the mobile client app project in VS2019 using the Android App (Xamarin) project template or the Mobile App (Xamarin Forms) project template.

In the Android-specific project

  • Install the Xamarin.GooglePlayServices.Base Nuget package
  • Install the Xamarin.Firebase.Messaging Nuget package
  • Add the google-services.json file (downloaded from FCM) to project and set its Build Action to GoogleServicesJson (If not found in list, close the solution and then reopen it)
  • In the Android Manifest tab of the Properties window (or click to open Properties/AndroidManifest.xml in Mac), set the package name to the name registered in FCM and select INTERNET in the Required permissions list
  • Modify the Properties/AndroidManifest.xml to add the following under the application section:
    <receiver
        android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver"
        android:exported="false" />
    <receiver
        android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
        android:exported="true"
        android:permission="com.google.android.c2dm.permission.SEND">
      <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
        <category android:name="${applicationId}" />
      </intent-filter>
    </receiver>
  • Add a FCMService.cs file with the following content:
    using Android.App;
    using Firebase.Iid;
    using Android.Util;
    
    namespace xfApp1.Droid
    {
        [Service]
        [IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
        public class FCMService : FirebaseMessagingService
        {
            const string TAG = "FCMService";
    
            public override void OnNewToken(string token)
            {
                Log.Debug(TAG, "Token: " + token);
                SendRegistrationToServer(token);
            }
            void SendRegistrationToServer(string token)
            {
                // Add custom implementation, as needed.
            }
        }
    }
  • Set a breakpoint inside the OnNewToken method and start debug, get the token there and save it
  • Run the app and put in the background (e.g. by clicking the home button)
  • Now we should be able to test the receiving of notification messages in the system tray by sending one from the FCM console. From Grow->Cloud Messaging, click "Send your first message", then "Send test message"
  • the message would appear in the system tray of the phone. When user opens the system tray and tap the message, the app comes to the foreground. To inspect the message content inside the app, we can add the following code to MainActivity:
    protected override void OnCreate(Bundle savedInstanceState)
    {
        Bundle bundle = Intent.Extras;
        if (bundle != null)
        {
            foreach (var key in bundle.KeySet())
            {
                var value = bundle.Get(key);
                Log.Debug(TAG, key + ": " + value);
            }
        }

The notification message goes to the system tray of the phone when the app is running in the background or not running. User would hear a sound when message comes. When user opens the tray and tap the message, the app comes to the foreground. When the app is in the foreground, the notification message does not go to the tray and no sound is made. In order to handle the message in this situation, we need to use the OnMessageReceived method of FirebaseMessagingService.

Notice that for Android App (Xamarin) project, I got an package version conflict error when installing the Xamarin.GooglePlayServices.Base package. I resolved the issue by uninstalling Xamarin.Essentials 1.3.1 package and installing Xamarin.Essentials 1.3.0 package instead.

FirebaseMessagingService

FirebaseMessagingService handles notification messages when the app is running in the foreground. We can handle it by adding the following code to our FCMService class:

public override void OnMessageReceived(RemoteMessage message)
{
    Log.Debug(TAG, "From: " + message.From); // SenderID
    if (message.GetNotification() != null)
    {
        Log.Debug(TAG, "Notification Message Body: " 
            + message.GetNotification().Body);
    }

    foreach (var data in message.Data)
    { 
       Log.Debug(TAG, data.Key + ": " + data.Value);
    }
}

OnMessageReceived would not be called when the notification message comes at the time that the app is running in background.

Besides notification message, there is another type of Firebase message called data message. Such message would never go to the system tray and is left to the app to handle. FirebaseMessagingService can handle data message no matter if the app is running in the foreground or background or is not running.

Broadcast Receiver

From the discussion in https://stackoverflow.com/questions/53082790/how-to-get-fcm-notification-data-when-app-is-not-in-task-or-killed, I learned that there is one more way to handle Firebase message, which is creating a Broadcast Receiver. It is called when the app is in the foreground and in background and no matter the message is notification message or data message.

[BroadcastReceiver(Enabled = true)]
[IntentFilter(new[] { "com.google.android.c2dm.intent.RECEIVE" })]
public class FirebaseDataReceiver: BroadcastReceiver
{
    const string TAG = "FirebaseDataReceiver";

    public override void OnReceive(Context context, Intent intent)
    {
        Log.Debug(TAG, "Broadcast Service Called");
        Bundle bundle = intent.Extras;
        if (bundle != null)
        {          
            foreach(var key in bundle.KeySet())
            {
                var value = bundle.Get(key);
                Log.Debug(TAG, key + ": " + value);
            }
        }
    }
}

Read more in Broadcast Receivers in Xamarin.Android

The primary difference between the manifest-registered receiver and the context-registered receiver is that a context-registered receiver will only respond to broadcasts while an application is running, while a manifest-registered receiver can respond to broadcasts even though the app may not be running.

When registration through class decorators is still considered as manifest-registered because the registration information is added to the manifest file at build time.

A broadcast receiver may not display dialogs, and it is strongly discouraged to start an activity from within a broadcast receiver. If a broadcast receiver must notify the user, then it should publish a notification.

Send Message from C#

Follow the following Google documents to enable web server to support FCM through Firebase Admin SDK:

The Admin .NET SDK requires .NET Framework 4.5+ or .Net Core 1.5+

  • Install-Package FirebaseAdmin -Version 1.9.2
  • download the server key json file from the Service Accounts tab in Project Overview/Project Settings in FCM console by clicking the "Generate new private key" button. Store the file in the project folder, and set its "Copy to Output Directory" property to "Copy Always", in this way, it will be deployed with the executable
  • for my console application, I have added the following code
    static async Task Main(string[] args)
    {
        await SendNotification();
    }
    
    static async System.Threading.Tasks.Task SendNotification()
    {
        var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        var configFile = Path.Combine(path, "serviceAccountKey.json");
    
        var credential = GoogleCredential.FromFile(configFile);
        FirebaseApp.Create(new AppOptions()
        {
            Credential = credential
        });
    
        var registrationToken = "XXXXXXXXXXXXX";
    
        var message = new Message()
        {
            Notification = new Notification()
            {
                Title = "First Test",
                Body = "Hello World",
            },
            Token = registrationToken,
        };
    
        try
        {
            string response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
            Console.WriteLine("Successfully sent message: " + response);
        }    
        catch (Exception ex)
        {
            Console.WriteLine("Cannot send message: " + ex.Message);
        }
    
        Console.ReadKey();
    }

Notice that the server key file is independent to the apps registered with the Firebase project, i.e. it is still valid when new apps added to the Firebase project after it gets generated. Every time we download this file, the content contains a different private_key_id and private_key. All of them should work.

I sometimes get the following error in office:

Newtonsoft.Json.JsonReaderException: Unexpected character encountered while parsing value: <. Path '', line 0, position 0

I though it is complaining about the server key json file, but actually it is regarding the response file. In normal situation we get a json response, but when error occurs, we may get a text response. One such situation is the my machine's internet account get locked, but there are other situations.

Similarly a data message looks like the following

var message = new Message()
{
    Data = new Dictionary<string, string>()
    {
        { "score", "850" },
        { "time", "2:45" },
    },
    Token = registrationToken,
};

Unlike a notification message can only contain a title and a body, a data message can contain any keys and values.

Send Message from Node Script

Similarly we can do the following to send message from node script (see https://www.techotopia.com/index.php/Sending_Firebase_Cloud_Messages_from_a_Node.js_Server for details)

  • create node project
    npm init my -y
  • install firebase-admin package
    nom install firebase-admin
    
  • download the server private key from from FCM Developer Console. In the following we assume the file is saved in the current folder and named serviceAccountKey.json
  • create a node script file named index.js with the following content (the registration token is obtained from client):
    var admin = require("firebase-admin");
    var serviceAccount = require("./serviceAccountKey.json");
    
    admin.initializeApp({
        credential: admin.credential.cert(serviceAccount),
        databaseURL: "https://txgz999-xfh200229.firebaseio.com"
    });
    
    var registrationToken = "XXXXXXXXX";
    
    var payload = {
      /*
      notification: {
        title: "Test 103",
        body: "A deposit to your savings account has just cleared."
      }
      */
      data: {
        account: "Saving",
        balance: "$100,000"
      }
    };
    
    var options = {
      priority: "high",
      timeToLive: 60 * 60 *24
    };
    
    admin.messaging().sendToDevice(registrationToken, payload, options)
    .then(function(response) {
      console.log(response);
      if (response.results) {
        for (var i=0; i<response.results.length; i++) {
          var result = response.results[i];
          if (result.error) console.log('error:', result.error);
          if (result.messageId) console.log('messageId:', result.messageId);
        }
      }
      process.exit(0);
    })
    .catch(function(error) {
      console.log("Error sending message:", error);
      process.exit(1);
    });
  • now we can send message from the command line
    node index.js

When I tried the code above, I got the following error:

FirebaseAppError: Credential implementation provided to initializeApp() via the "credential" property failed to fetch a valid Google OAuth access token with the following error: "Error fetching access token: Error wile making request: connect ETIMEOUT 172.217.1045:443"

Again it is the proxy that caused the error. Found the solution in https://medium.com/@tcguy/setup-firebase-admin-sdk-behind-proxy-network-47cec18a6142. I have to do the following:

  • npm install tunnel2
  • modify index.js to include
    const tunnel = require("tunnel2");
    // Create your Proxy Agent
    // Please choose your tunneling method accordingly, my case
    // is httpsoverHttp, yours might be httpsoverHttps
    const proxyAgent = tunnel.httpsOverHttp({
      proxy: {
        host: "myproxyservername",
        port: myproxyserverport,
        proxyAuth: "myproxyserverusername:myproxyserverpassword" // Optional, required only if your proxy require authentication
      }
    });
    
    admin.initializeApp({
      credential: admin.credential.cert(serviceAccount, proxyAgent),
      httpAgent: proxyAgent
    });
Heap-up Notification

So far the notification messages we sent to phone would appear in the system tray of the phone. Is it possible to make them appear on screen directly? Those messages are called head-up message. We can do that using high importance channels. We can register such channel in OnCreate of the main activity:

void CreateNotificationChannel()
{
    if (Build.VERSION.SdkInt < BuildVersionCodes.O)
    {
        // Notification channels are new in API 26 (and not a part of the
        // support library). There is no need to create a notification
        // channel on older versions of Android.
        return;
    }

    var channel = new NotificationChannel("txgz_test",
                                          "FCM Notifications",
                                          NotificationImportance.High)
    {
        Description = "Firebase Cloud Messages appear in this channel"
    };

    var notificationManager = (NotificationManager)GetSystemService(
        Android.Content.Context.NotificationService);
    notificationManager.CreateNotificationChannel(channel);
}

Then when we send a message we can specify to use the channel we just created, or we can make that channel as the default channel inside the Application node in AndroidManifest.xml and don't specify channel we create the message:

<meta-data 
  android:name="com.google.firebase.messaging.default_notification_channel_id"
  android:value="txgz_test" />
Proxy Setting

My Android emulator in office did not receive notification message for days until I heard the solution from a coworker. We need to add proxy setting for the emulator, see https://developer.android.com/studio/run/emulator-networking.html#proxy and https://stackoverflow.com/questions/1570627/how-to-set-up-android-emulator-proxy-settings. There is no need to make any setting change inside the emulator phone.

⚠️ **GitHub.com Fallback** ⚠️