Background
My team has recently implemented push notifications for our Android MAUI (.NET Multi-platform Application UI) app. This implementation was part of our ongoing migration from Xamarin to MAUI in a cross-platform mobile healthcare application. The app is designed to help users manage their health by providing timely information, reminders, and updates.
One of the key features of this healthcare app is its ability to receive push notifications that include links directing users to specific pages within the app. This functionality ensures that users can quickly and easily access relevant information or take necessary actions directly from the notifications.
To power this feature, we are utilizing Firebase as the backend service for sending out messages. Firebase Cloud Messaging service provides a reliable and scalable solution for managing and delivering push notifications.
To provide some context, this diagram represents the part of our system responsible for sending push notifications. As you can see, we utilize Azure Notification Hub to handle this functionality.
Although the MainActivity class code given in this post is Android platform-specific, the Firebase Cloud Messaging Service and the cross-platform services executed from the MainActivity can work nicely with an iOS app implementation.
To receive push notifications on your app you need to:
- ask the user’s permission for the app to display notifications
- register the user’s device with the Firebase project
- handle the receiving of the notification
In this post, let’s discuss the last part - receiving the notification. Our app loads a single-page application that routes to different pages, so let’s focus on a notification that contains a link to one of the app’s pages. These examples could also be applied to any other data type that each app would need the data object of the Firebase message to contain.
Let’s say we want to send a notification with these JSON values to the device where the app is deployed.
{
"notification": {
"title" : "You received a message",
"body" : "Click to view your in-app messages."
},
"data": {
"url" : "https://my-app/messages"
}
}
Ultimately, we want the act of clicking the notification in the notifications tray to open the app, and if the Firebase message contains some extra data (like a URL), to pass along that data through an Intent object. Then the Android app should pick the data up and do something with it.
What is an Intent?
In Android, an Intent
is a messaging object used to request an action from another app component and share some information.
For example, the Intent
object is created by the Firebase Cloud Messaging service when a user clicks on a notification. The Intent
contains the data from the notification and is passed to the OnCreate
method of the app activity that is launched by the notification. Our code implementation decides what we do with that Intent
object. We might want to ignore it or, if the data in the Intent
makes sense, we can execute some action. In our case, if the Intent
object contains a URL field, we will try to use that URL to direct the user to the correct page in our application.
Implementation
We've learned that an Android app can operate in three different states: closed, in the background, or in the foreground. In each scenario, the push notification intent might need to be handled differently, especially if it contains a URL field for opening a specific page within the app.
Closed App
We found that receiving and handling a notification from Firebase was very easy when the app is closed. The Firebase SDK would handle the receipt of the message, create a notification, and display it for the user. Once the user clicks the notification, the default activity will be started. In your activity class, the OnCreate
method will be executed. At this stage, we can get the URL from the Intent
object that was created by FCM, and then we use it to redirect the user to a specific screen in the app.
The MainActivity class in Android is the entry point for your application. It is a subclass of the Activity class, which is a crucial component of an Android application. An activity represents a single screen with a user interface.
[Activity(Theme = "@style/Maui.SplashTheme",
MainLauncher = true,
LaunchMode = LaunchMode.SingleTop)]
public class MainActivity : MauiAppCompatActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
HandleIntent(Intent);
}
private static void HandleIntent(Intent intent)
{
if (intent == null)
{
return;
}
var url = intent.Extras?.GetString("url");
if (string.IsNullOrWhiteSpace(url))
{
return;
}
if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri))
{
Microsoft.Maui.Controls.Application.Current?.SendOnAppLinkRequestReceived(uri);
}
}
}
In this example of the MainActivity
class, we create a Uri object and pass it to the SendOnAppLinkRequestReceived
method defined in App.xaml.cs
file. This is great because the App class contains cross-platform code. So if you are also implementing an iOS notification URL handling, your iOS-specific code can call the same cross-platform method in this App class and carry on with the same redirection behavior.
public partial class App
{
private readonly IAppNavigationService _appNavigationService;
public App(IAppNavigationService appNavigationService)
{
InitializeComponent();
_appNavigationService = appNavigationService;
}
protected override void OnAppLinkRequestReceived(Uri uri)
{
base.OnAppLinkRequestReceived(uri);
var currentPage = _appNavigationService.GetCurrentPage();
currentPage.HandleDeepLinkRequest(uri);
}
}
Each page might do something different with this URI (sometimes redirect, sometimes ignore). That’s why we chose to implement and call a method for handling the URI in each page separately.
App Running in the Background
When the app is running in the background (minimized), Firebase again handles the notification creation and display. When the user clicks on this notification, the app will be foregrounded, but the OnCreate
method will not be called as the activity is already running. Instead, we need to override another method - OnNewIntent()
. This will be automatically called when the user clicks on the notification. In this method, we can get the URL from the intent
parameter and redirect.
[Activity(Theme = "@style/Maui.SplashTheme",
MainLauncher = true,
LaunchMode = LaunchMode.SingleTop)]
public class MainActivity : MauiAppCompatActivity
{
// ...
protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
HandleIntent(intent);
}
private static void HandleIntent(Intent intent)
{
if (intent == null)
{
return;
}
var url = intent.Extras?.GetString("url");
if (string.IsNullOrWhiteSpace(url))
{
return;
}
if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri))
{
Microsoft.Maui.Controls.Application.Current?.SendOnAppLinkRequestReceived(uri);
}
}
}
-
Note on the Launch mode
In .NET MAUI, the
LaunchMode = LaunchMode.SingleTop
activity configuration is used to control the behavior of your app's activity in response to push notifications or other intents. This launch mode ensures that if an instance of the activity already exists at the top of the activity stack, it will be reused instead of creating a new instance. This is exactly the scenario we want to have when the app is running in the background.
App Running in the Foreground
When the app is open and a Firebase message is sent, the Firebase SDK doesn’t create and display the notification for us. We have to do this ourselves. For this reason, we implemented a messaging service extending the FirebaseMessagingService
class. This is where the OnMessageReceived
method is automatically executed once the message is sent. By overriding this method, we were able to take the data from the remote message, build a notification ourselves, including the intent with the URL, and display it for the user in the device’s notification tray.
using _Microsoft.Android.Resource.Designer;
using Android.App;
using Android.Content;
using Android.OS;
using AndroidX.Core.App;
using AndroidX.Core.Content;
using Firebase.Messaging;
namespace MyApp.Platforms.Notifications;
[Service(Exported = false)]
[IntentFilter(["com.google.firebase.MESSAGING_EVENT"])]
public class NotificationMessagingService : FirebaseMessagingService
{
private const string NotificationUrlDataKey = "url";
private const string NotificationChannelId = "someChannelId";
public override void OnMessageReceived(RemoteMessage message)
{
var notification = message.GetNotification();
var notificationId = Guid.NewGuid().GetHashCode();
var pendingIntent = BuildIntent(notificationId, message.Data);
var builtNotification = BuildNotification(pendingIntent, notification);
CreateNotificationChannel();
SendNotification(notificationId, builtNotification);
}
private PendingIntent BuildIntent(int notificationId, IDictionary<string, string> data)
{
using var intent = new Intent(this, typeof(MainActivity));
intent.AddFlags(ActivityFlags.SingleTop);
if (data.TryGetValue(NotificationUrlDataKey, out var url) && !string.IsNullOrWhiteSpace(url))
{
intent.PutExtra(NotificationUrlDataKey, url);
}
return PendingIntent.GetActivity(
this,
notificationId,
intent,
PendingIntentFlags.OneShot | PendingIntentFlags.Immutable);
}
private Notification BuildNotification(PendingIntent intent, RemoteMessage.Notification notification)
{
using var notificationBuilder = new NotificationCompat.Builder(this, NotificationChannelId);
notificationBuilder
.SetContentTitle(notification.Title)
.SetContentText(notification.Body)
.SetContentIntent(intent)
.SetAutoCancel(true);
return notificationBuilder.Build();
}
private void CreateNotificationChannel()
{
using var channel = new NotificationChannel(
NotificationChannelId,
"MyApp Notifications",
NotificationImportance.Default);
var notificationManager = GetSystemService(NotificationService) as NotificationManager;
notificationManager?.CreateNotificationChannel(channel);
}
private void SendNotification(int notificationId, Notification notification)
{
var notificationManager = NotificationManagerCompat.From(this);
notificationManager.Notify(notificationId, notification);
}
}
This way, whenever a remote message is received from Firebase, the notification will be created and immediately displayed for the user. Once it’s clicked, OnNewIntent
method in the activity class will be executed, and the URL is going to be used for redirection.
Some readings ✨
Android Activity
Launch Modes of Android Activity
Receive Messages in an Android App | Firebase Cloud Messaging
FirebaseMessagingService class
Top comments (1)
What if the app is backgrounded, meaning they closed the app from the recent list, I am running into this issue where an older Xamarin app works when backgrounded. Thoughts?