MAUI Push Notifications using Azure Notification Hub for Android

Xamarin push notification with Azure

It is more than 2 weeks since I tried to configure and implement in my NET8 MAUI application the push notifications using Azure Notification Hubs for Android. Also, I paid the Azure support (not very useful), and I still can’t configure the hub for Windows and Android. So, I tried another plugin at this point but had to ignore the Windows notification (sigh!).

So, I show you everything I discovered without using external plugin but only what MAUI offers and HttpClient. I split this topic in a few posts:

In this post, I won’t explain how to configure the Azure Notification Hub; if you need more information, please read my previous post.

Setting up Firebase Cloud Messaging (FCM) for Android

First, Firebase Cloud Messaging enables you to send push notifications to Android devices. So, I have to configure it as the first action to proceed.

Create a Firebase Project

  1. Go to the Firebase console.
  2. Click on ‘Add project’.
  3. Follow the instructions and set up your project.
Projects in the Firebase console - MAUI Push Notifications using Azure Notification Hub for Android
Projects in the Firebase console

Obtain Server Key

  1. Navigate to Project settings.
  2. Click on the Cloud Messaging tab. You will see Cloud Messaging API (Legacy) Disabled message. Click on Manage API in Google Cloud Console. You will be redirected to Google Cloud Console. Click on Enable.
  1. Back to Firebase and copy your Server key.

Configure FCM with Azure

  1. Go to the Azure portal.
  2. In your Notification Hub, under Settings, select Google (GCM/FCM).
  3. Enter your Server Key.
  4. Click Save.
Google integration in the Azure Notification Hub

That’s it! You now have Azure Notification Hubs integrated with FCM.

Setting up your .NET MAUI Project

Add google-services.json

Open Firebase Console and select Add Firebase to your Android app.

On the Add Firebase to your Android app page, enter an Android package name. It should match the package name of your .NET MAUI application.

Select Register app.

Select Download google-services.json. Then save the file into a Platforms\Android folder. In the properties of the file in your project, mark this file as GoogleServicesJson

Set the file as GoogleServicesJson

Add Required Packages

<ItemGroup Condition="'$(TargetFramework)' == '$(NetVersion)-android'">
    <GoogleServicesJson Include="Platforms\Android\google-services.json" />
    <PackageReference Include="Xamarin.Firebase.Messaging" Version="122.0.0" />
    <PackageReference Include="Xamarin.Google.Dagger" Version="2.39.1" />
</ItemGroup>

JAVA0000

If you get an error like this one

Error JAVA0000 Error in C:\Users\SushmithaBanoji.nuget\packages\xamarin.androidx.collection.jvm\1.3.0.1\buildTransitive\net6.0-android31.0....\jar\androidx.collection.collection-jvm.jar:androidx/collection/ArrayMapKt.class:
Type androidx.collection.ArrayMapKt is defined multiple times: C:\Users\SushmithaBanoji.nuget\packages\xamarin.androidx.collection.jvm\1.3.0.1\buildTransitive\net6.0-android31.0....\jar\androidx.collection.collection-jvm.jar:androidx/collection/ArrayMapKt.class, C:\Users\SushmithaBanoji.nuget\packages\xamarin.androidx.collection.ktx\1.2.0.5\buildTransitive\net6.0-android31.0....\jar\androidx.collection.collection-ktx.jar:androidx/collection/ArrayMapKt.class
Compilation failed

then you have to add another package because there is an issue with one of the Xamarin NuGet package

<PackageReference Include="Xamarin.AndroidX.Fragment.Ktx" Version="1.6.2"/>

Firebase is not initialized

Another error you can get when the application is trying to get a token with FirebaseMessaging.Instance.GetToken() is

Java.Lang.IllegalStateException: ‘Default FirebaseApp is not initialized in this process com.languageinuse.app. Make sure to call FirebaseApp.initializeApp(Context) first.’

Java.Lang.IllegalStateException: Default FirebaseApp is not initialized in this process com.languageinuse.app. Make sure to call FirebaseApp.initializeApp(Context) first.
   at Java.Interop.JniEnvironment.StaticMethods.CallStaticObjectMethod(JniObjectReference type, JniMethodInfo method, JniArgumentValue* args) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/obj/Release/net7.0/JniEnvironment.g.cs:line 21452
   at Java.Interop.JniPeerMembers.JniStaticMethods.InvokeObjectMethod(String encodedMember, JniArgumentValue* parameters) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs:line 165
   at Firebase.Messaging.FirebaseMessaging.get_Instance() in C:\a\_work\1\s\generated\com.google.firebase.firebase-messaging\obj\Release\net7.0-android\generated\src\Firebase.Messaging.FirebaseMessaging.cs:line 106
   at MauiPushNotification.Platforms.Android.Notification.DeviceInstallationService.RegisterDevice(String notificationHubNamespace, String notificationHub, String key) in C:\Projects\GitHub\MauiPushNotification\MauiPushNotification\MauiPushNotification\Platforms\Android\Notification\DeviceInstallationService.cs:line 29
  --- End of managed Java.Lang.IllegalStateException stack trace ---
java.lang.IllegalStateException: Default FirebaseApp is not initialized in this process com.languageinuse.app. Make sure to call FirebaseApp.initializeApp(Context) first.
	at com.google.firebase.FirebaseApp.getInstance(FirebaseApp.java:179)
	at com.google.firebase.messaging.FirebaseMessaging.getInstance(FirebaseMessaging.java:126)
	at crc648356ffd500c1cdc0.MainActivity.n_onCreate(Native Method)
	at crc648356ffd500c1cdc0.MainActivity.onCreate(MainActivity.java:39)
	at android.app.Activity.performCreate(Activity.java:8595)
	at android.app.Activity.performCreate(Activity.java:8573)
	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3764)
	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922)
	at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
	at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)
	at android.os.Handler.dispatchMessage(Handler.java:106)
	at android.os.Looper.loopOnce(Looper.java:205)
	at android.os.Looper.loop(Looper.java:294)
	at android.app.ActivityThread.main(ActivityThread.java:8177)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)

  --- End of managed Java.Lang.IllegalStateException stack trace ---
java.lang.IllegalStateException: Default FirebaseApp is not initialized in this process com.languageinuse.app. Make sure to call FirebaseApp.initializeApp(Context) first.
	at com.google.firebase.FirebaseApp.getInstance(FirebaseApp.java:179)
	at com.google.firebase.messaging.FirebaseMessaging.getInstance(FirebaseMessaging.java:126)
	at crc648356ffd500c1cdc0.MainActivity.n_onCreate(Native Method)
	at crc648356ffd500c1cdc0.MainActivity.onCreate(MainActivity.java:39)
	at android.app.Activity.performCreate(Activity.java:8595)
	at android.app.Activity.performCreate(Activity.java:8573)
	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3764)
	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922)
	at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
	at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)
	at android.os.Handler.dispatchMessage(Handler.java:106)
	at android.os.Looper.loopOnce(Looper.java:205)
	at android.os.Looper.loop(Looper.java:294)
	at android.app.ActivityThread.main(ActivityThread.java:8177)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Java.Lang.IllegalStateException: 'Default FirebaseApp is not initialized in this process com.languageinuse.app. Make sure to call FirebaseApp.initializeApp(Context) first.'

Add permissions

Update AndroidManifest.xml:

<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />

Update MainActivity.cs

Remember the connection strings from the Azure Notification Hub setup? You’ll need them now.

Search for DefaultListenSharedAccessSignature access policy and copy SharedAccessKey.

protected override async void OnCreate(Bundle? savedInstanceState)
{
    base.OnCreate(savedInstanceState);
    await DeviceInstallationService.RegisterDevice("YOUR HUB NAME", "YOUR SharedAccessKey");
}

Don’t use DefaultFullSharedAccessSignature in client applications!

Create DeviceInstallationService.cs

Azure Hotification Hub requires device registration, so it knows what device should receive a notification.

public static class DeviceInstallationService
{
    private static bool NotificationsSupported
        => GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(Application.Context) == ConnectionResult.Success;

    private static string? GetDeviceId()
        => Settings.Secure.GetString(Application.Context.ContentResolver, Settings.Secure.AndroidId);

    public static async Task RegisterDevice(string notificationHubNamespace, string notificationHub, string key)
    {
        if (!NotificationsSupported)
            return;

        try
        {
            var firebaseToken = await FirebaseMessaging.Instance.GetToken();
            var deviceInstallation = new
            {
                InstallationId = GetDeviceId(),
                Platform = "gcm",
                PushChannel = firebaseToken.ToString()
            };

            using var httpClient = new HttpClient();
            httpClient.DefaultRequestHeaders.Add("x-ms-version", "2015-01");
            httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization",
                                                CreateToken($"https://{notificationHubNamespace}.servicebus.windows.net",
                                                            "DefaultListenSharedAccessSignature", key));

            await httpClient.PutAsJsonAsync($"https://{notificationHubNamespace}.servicebus.windows.net/{notificationHub}" +
                                            $"/installations/{deviceInstallation.InstallationId}?api-version=2015-01",
                                            deviceInstallation);
        }
        catch(Exception ex)
        {
            LogCenter.Save("[Android] Push Notification Registration Error", "", exc: ex);
        }
    }

    private static string CreateToken(string resourceUri, string keyName, string key)
    {
        var sinceEpoch = DateTime.UtcNow - DateTime.UnixEpoch;
        var week = 60 * 60 * 24 * 7;
        var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + week);
        var stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
        var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
        var sasToken = string.Format(CultureInfo.InvariantCulture,
                                        "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}",
                                        HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry,
                                        keyName);
        return sasToken;
    }
}

Here we use FirebaseInstanceId.Instance.Token to get the token and then we send a PUT HTTP Request to register our device with NotificationHub. You can find more details here: Notification Hubs REST API Methods.

Now your device is registered.

Setting up the Receivers

Now, the last step is to define our receiver. You need to set up receivers to handle notifications pushed to your app.

For Android, use FirebaseMessagingService. Override OnMessageReceived() to define how the notifications should be handled:

[Service(Exported = false)]
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
public class PushNotificationFirebaseMessagingService : FirebaseMessagingService
{
	public override void OnMessageReceived(RemoteMessage p0)
	{
		base.OnMessageReceived(p0);

        var receivedNotification = p0.GetNotification();
        // implement your logic here...
	}
}

This app is not authorized to use Firebase Authentication

Firebase auth was working fine, but the debug build suddenly started failing without any change of code, logging the following message

D/PhoneAuthActivity( 7392): signInWithCredential:failure:com.google.firebase.auth.FirebaseAuthException: This app is not authorized to use Firebase Authentication. Please verifythat the correct package name and SHA-1 are configured in the Firebase Console. [ App validation failed ].

Using a try ... catch the error detail is the following:

LanguageInUse.Platforms.Android.Notification.DeviceInstallationService.RegisterDevice(String notificationHubNamespace, String notificationHub, String key) in C:\Projects\ERDevOps\LIUApp\LanguageInUse\Platforms\Android\Notification\DeviceInstallationService.cs:line 30
  --- End of managed Java.IO.IOException stack trace ---
java.io.IOException: java.util.concurrent.ExecutionException: java.io.IOException: FIS_AUTH_ERROR
	at com.google.firebase.messaging.FirebaseMessaging.blockingGetToken(FirebaseMessaging.java:626)
	at com.google.firebase.messaging.FirebaseMessaging.lambda$getToken$4$com-google-firebase-messaging-FirebaseMessaging(FirebaseMessaging.java:382)
	at com.google.firebase.messaging.FirebaseMessaging$$ExternalSyntheticLambda9.run(Unknown Source:4)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:487)
	at java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
	at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@18.2.0:2)
	at java.lang.Thread.run(Thread.java:1012)
Caused by: java.util.concurrent.ExecutionException: java.io.IOException: FIS_AUTH_ERROR
	at com.google.android.gms.tasks.Tasks.zza(com.google.android.gms:play-services-tasks@@18.0.2:5)
	at com.google.android.gms.tasks.Tasks.await(com.google.android.gms:play-services-tasks@@18.0.2:8)
	at com.google.firebase.messaging.FirebaseMessaging.blockingGetToken(FirebaseMessaging.java:624)
	... 9 more
Caused by: java.io.IOException: FIS_AUTH_ERROR
	at com.google.firebase.messaging.GmsRpc.handleResponse(GmsRpc.java:309)
	at com.google.firebase.messaging.GmsRpc.lambda$extractResponseWhenComplete$0$com-google-firebase-messaging-GmsRpc(GmsRpc.java:320)
	at com.google.firebase.messaging.GmsRpc$$ExternalSyntheticLambda0.then(Unknown Source:2)
	at com.google.android.gms.tasks.zzc.run(com.google.android.gms:play-services-tasks@@18.0.2:3)
	at androidx.profileinstaller.ProfileInstallReceiver$$ExternalSyntheticLambda0.execute(Unknown Source:0)
	at com.google.android.gms.tasks.zzd.zzd(com.google.android.gms:play-services-tasks@@18.0.2:1)
	at com.google.android.gms.tasks.zzr.zzb(com.google.android.gms:play-services-tasks@@18.0.2:5)
	at com.google.android.gms.tasks.zzw.zzb(com.google.android.gms:play-services-tasks@@18.0.2:3)
	at com.google.android.gms.tasks.zzc.run(com.google.android.gms:play-services-tasks@@18.0.2:8)
	at com.google.android.gms.cloudmessaging.zzz.execute(Unknown Source:0)
	at com.google.android.gms.tasks.zzd.zzd(com.google.android.gms:play-services-tasks@@18.0.2:1)
	at com.google.android.gms.tasks.zzr.zzb(com.google.android.gms:play-services-tasks@@18.0.2:5)
	at com.google.android.gms.tasks.zzw.zzb(com.google.android.gms:play-services-tasks@@18.0.2:3)
	at com.google.android.gms.tasks.TaskCompletionSource.setResult(com.google.android.gms:play-services-tasks@@18.0.2:1)
	at com.google.android.gms.cloudmessaging.zzp.zzd(com.google.android.gms:play-services-cloud-messaging@@17.0.0:3)
	at com.google.android.gms.cloudmessaging.zzr.zza(com.google.android.gms:play-services-cloud-messaging@@17.0.0:2)
	at com.google.android.gms.cloudmessaging.zzf.handleMessage(com.google.android.gms:play-services-cloud-messaging@@17.0.0:14)
	at android.os.Handler.dispatchMessage(Handler.java:102)
	at android.os.Looper.loopOnce(Looper.java:205)
	at android.os.Looper.loop(Looper.java:294)
	at android.app.ActivityThread.main(ActivityThread.java:8177)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)

If I added to the PushNotificationFirebaseMessagingService.cs the plugin for local notification called Plugin.LocalNotification, the error changed in the following one:

   at LanguageInUse.Platforms.Android.Notification.DeviceInstallationService.RegisterDevice(String notificationHubNamespace, String notificationHub, String key) in C:\Projects\ERDevOps\LIUApp\LanguageInUse\Platforms\Android\Notification\DeviceInstallationService.cs:line 30
  --- End of managed Java.IO.IOException stack trace ---
java.io.IOException: java.util.concurrent.ExecutionException: java.io.IOException: SERVICE_NOT_AVAILABLE
	at com.google.firebase.messaging.FirebaseMessaging.blockingGetToken(FirebaseMessaging.java:626)
	at com.google.firebase.messaging.FirebaseMessaging.lambda$getToken$4$com-google-firebase-messaging-FirebaseMessaging(FirebaseMessaging.java:382)
	at com.google.firebase.messaging.FirebaseMessaging$$ExternalSyntheticLambda9.run(Unknown Source:4)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:487)
	at java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
	at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@18.2.0:2)
	at java.lang.Thread.run(Thread.java:1012)
Caused by: java.util.concurrent.ExecutionException: java.io.IOException: SERVICE_NOT_AVAILABLE
	at com.google.android.gms.tasks.Tasks.zza(com.google.android.gms:play-services-tasks@@18.0.2:5)
	at com.google.android.gms.tasks.Tasks.await(com.google.android.gms:play-services-tasks@@18.0.2:8)
	at com.google.firebase.messaging.FirebaseMessaging.blockingGetToken(FirebaseMessaging.java:624)
	... 9 more
Caused by: java.io.IOException: SERVICE_NOT_AVAILABLE
	at com.google.android.gms.cloudmessaging.zzv.then(com.google.android.gms:play-services-cloud-messaging@@17.0.0:5)
	at com.google.android.gms.tasks.zzc.run(com.google.android.gms:play-services-tasks@@18.0.2:3)
	at com.google.android.gms.cloudmessaging.zzz.execute(Unknown Source:0)
	at com.google.android.gms.tasks.zzd.zzd(com.google.android.gms:play-services-tasks@@18.0.2:1)
	at com.google.android.gms.tasks.zzr.zzb(com.google.android.gms:play-services-tasks@@18.0.2:5)
	at com.google.android.gms.tasks.zzw.zza(com.google.android.gms:play-services-tasks@@18.0.2:4)
	at com.google.android.gms.tasks.TaskCompletionSource.setException(com.google.android.gms:play-services-tasks@@18.0.2:1)
	at com.google.android.gms.cloudmessaging.zzp.zzc(com.google.android.gms:play-services-cloud-messaging@@17.0.0:3)
	at com.google.android.gms.cloudmessaging.zzm.zzb(com.google.android.gms:play-services-cloud-messaging@@17.0.0:8)
	at com.google.android.gms.cloudmessaging.zzm.zza(com.google.android.gms:play-services-cloud-messaging@@17.0.0:1)
	at com.google.android.gms.cloudmessaging.zzm.zzd(com.google.android.gms:play-services-cloud-messaging@@17.0.0:1)
	at com.google.android.gms.cloudmessaging.zzi.run(Unknown Source:2)
	... 7 more
Caused by: com.google.android.gms.cloudmessaging.zzq: Timed out while binding
	at com.google.android.gms.cloudmessaging.zzm.zzb(com.google.android.gms:play-services-cloud-messaging@@17.0.0:6)
	... 10 more

Topic sync or token retrieval failed on hard failure exceptions: FIS_AUTH_ERROR. Won’t retry the operation.

I fought this error for about a week and I couldn’t understand what the problem was. In my case, the problem was related to the configuration in the Google Cloud under credentials.

Google Console Api & Service credentials

So, my advice is to try your app without any restriction and then add the restrictions and test your app.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.