thomasgalliker / Plugin.FirebasePushNotifications

Receive and handle firebase push notifications in .NET MAUI apps
MIT License
61 stars 5 forks source link

[Enhancement] Allow iOS, like Android, to customize the push notifications #83

Open GHGiampy opened 1 week ago

GHGiampy commented 1 week ago

Summary

Currently using the NotificationReceived event, only on Android, we can change the behavior of the push notification before it is processed simply modifying the Data dictionary in the FirebasePushNotificationDataEventArgs received. This request allows also iOS to customize the push notification.

API Changes

No API changes are required, the workaround is to move a simple line of code a couple of rows earlier. We are using the v2.5.13, so supposing it is the main branch, moving the line 273 after the 267 here: https://github.com/thomasgalliker/Plugin.FirebasePushNotifications/blob/92017ea181e12f6b1bc9a3e2625215e08180a6d6/Plugin.FirebasePushNotifications/Platforms/iOS/FirebasePushNotificationManager.cs#L266-L274 allows the notificationPresentationOptions to be filled based on the Data modified by the NotificationReceived event, as a consequence this allows us to customize the push notification, exactly as in Android, before it is handled or created.

The same can be achieved for the develop branch here: https://github.com/thomasgalliker/Plugin.FirebasePushNotifications/blob/4629de36b57444376d0cf05d1e0815348cded808/Plugin.FirebasePushNotifications/Platforms/iOS/FirebasePushNotificationManager.cs#L320-L328 Moving the line 327 after the 321.

Intended Use Case

With this modification we can use code like this:

_firebasePushNotification.NotificationReceived += (s, e) =>
{
  // Event "NotificationReceived" (with app in foreground)
  _logger.LogDebug("Firebase-NotificationReceived - Data: {data}", e.Data);
#if ANDROID || IOS
  // Force the high priority when the app is in foreground to show the notification as badge
  var _isInBackground = ((App)(App.Current!)).IsInBackground;
  if (!_isInBackground && !e.Data.ContainsKey("priority")) e.Data.Add("priority", "high");
#endif
};

Which works on both, Android and iOS.

thomasgalliker commented 5 days ago

Hi @GHGiampy thanks for reporting this. I was thinking about such a thing in the past too. Can you try the latest 2.5.x pre-release: I tried to harmonize exactly this behavior between the Android and the iOS implementations. In the latest 2.5.x-pre non-high-priority notifications should never be displayed with a notification popup if the app runs in foreground mode - not even then, when it contains a click_action/category. The idea is, that only high-priority notifications show a popup if the app is in foreground. If the notification is not high-priority, it's the duty of the app to display a popup/dialog/callout/whatever.

Does that make sense? Is this what you're aiming for with this issue?

GHGiampy commented 5 days ago

Hi @thomasgalliker, do you have a branch and commit hash to use for testing version 2.5.34-pre (or are we now looking at version 2.5.35)? This way, we can debug any potential issues, which isn’t possible when using the NuGet package.

I agree that a message should only be displayed in cases of high priority, regardless of other message settings. However, I would still leave the final choice on how to handle the message to the app itself. If it’s true that high priority is determined by the notification sender to enforce its visibility, it’s equally true that when the app is active, in either background or foreground, it can choose to customize the message behavior (though this is not a requirement). By allowing modification of the Data attributes, it’s possible to reuse the plugin’s code to display the message without needing to write a NotificationBuilder for Android or a custom handler for iOS that enforces high priority.

My goal was to have the message visible as a banner only if the app is in foreground, while, if the app is closed or open in the background, the message should be standard (e.g., in the tray for Android). But this is just my personal preference, it’s not necessarily the correct way to handle notifications.

BTW: I noticed that even in the latest versions of the code, HandleNotificationReceived in iOS is called after GetNotificationPresentationOptions, which still introduces different behavior between iOS and Android. In Android, the NotificationReceived event allows modifying the message, whereas in iOS, it does not.

thomasgalliker commented 5 days ago

The released version 2.5.35 uses the main branch as code base. I think your goal can be achieved with version 2.5.35: Just send push notification without any priority flag and also don't add anything to the data payload. When your app is in background you'll see the native OS notification popup and while it runs in foreground you can display your own banner whenever NotificationReceived event is fired.

Instead of manipulating the data payload (which I personally think is rather hacky), I'd rather prefer something similar to NotificationBuilder/Android where the iOS app can override the GetNotificationPresentationOptions method and define its own logic.

GHGiampy commented 5 days ago

Instead of manipulating the data payload (which I personally think is rather hacky)

I see and agree with you.

I'd rather prefer something similar to NotificationBuilder/Android where the iOS app can override the GetNotificationPresentationOptions

But what if someone wants to show the notification as a banner when the app is in the foreground? We need to create and configure a new NotificationBuilder, exactly as the plugin does for Android and override the GetNotificationPresentationOptions (for iOS, which is currently not overridable) to set the presentation visibility of the notification. Correct me if I'm wrong, but if we simply want to show the notification like the plugin does, we need to rewrite code that is already in the plugin. So, why not reuse the plugin code?

One idea could be to expose a method to call inside the NotificationReceived to show the notification (in iOS and Android) using the plugin's code. Or better, we could add a simple boolean like ShowNotification to FirebasePushNotificationDataEventArgs, which is initially set based on the state of the notification (app in background/foreground). By changing its value, the plugin code can be reused (to show or hide the notification) without a line of custom code.

What I've said so far is valid unless I'm overlooking a simple solution for displaying notifications with the app in the foreground.

GHGiampy commented 9 hours ago

First feedback, for now only on Android with v2.5.35. Removed my hacky code in NotificationReceived event which forces the high priority to the notification when the app is in foreground. In this configuration the NotificationReceived event is received, but obviously, the notification is shown only when the app is in background and killed, nothing when it's in foreground. I enabled the configuration in the startup code:

if (OperatingSystem.IsAndroidVersionAtLeast(24))
    o.Android.DefaultNotificationChannelImportance = AndroidApp.NotificationImportance.High;

Which I assume that it sets the default importance/priority of the received notifications; the push notification are shown also in foreground but different warnings and errors can be seen in the log:

[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] 2024/11/02 12:59:53.412|DEBUG|NotificationBuilder| OnNotificationReceived 
[CompatibilityChangeReporter] Compat change id reported: 160794467; UID 10430; state: ENABLED
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] 2024/11/02 12:59:53.424|WARN |NotificationBuilder| Notification channel 'DefaultNotificationChannel' has importance 'Default' which is lower than notification importance 'High' 
[yname.Campi_app] Invalid resource ID 0x00000000.
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] 2024/11/02 12:59:53.430|ERROR|NotificationBuilder| Failed to get small icon resourceAndroid.Content.Res.Resources+NotFoundException: Unable to find resource ID #0x0
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at Java.Interop.JniEnvironment.InstanceMethods.CallObjectMethod(JniObjectReference instance, 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 19992
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualObjectMethod(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs:line 867
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at Android.Content.Res.Resources.GetResourceName(Int32 resid) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Android.Content.Res.Resources.cs:line 1439
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at Plugin.FirebasePushNotifications.Platforms.NotificationBuilder.GetSmallIconResource(IDictionary`2 data, Context context)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]   --- End of managed Android.Content.Res.Resources+NotFoundException stack trace ---
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] android.content.res.Resources$NotFoundException: Unable to find resource ID #0x0
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.ResourcesImpl.getResourceName(ResourcesImpl.java:312)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.Resources.getResourceName(Resources.java:2355)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.n_handleIntent(Native Method)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.handleIntent(PNFirebaseMessagingService.java:32)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService.lambda$processIntent$0$com-google-firebase-messaging-EnhancedIntentService(EnhancedIntentService.java:78)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService$$ExternalSyntheticLambda0.run(Unknown Source:6)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@18.2.0:2)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.lang.Thread.run(Thread.java:1012)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] 
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]   --- End of managed Android.Content.Res.Resources+NotFoundException stack trace ---
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] android.content.res.Resources$NotFoundException: Unable to find resource ID #0x0
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.ResourcesImpl.getResourceName(ResourcesImpl.java:312)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.Resources.getResourceName(Resources.java:2355)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.n_handleIntent(Native Method)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.handleIntent(PNFirebaseMessagingService.java:32)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService.lambda$processIntent$0$com-google-firebase-messaging-EnhancedIntentService(EnhancedIntentService.java:78)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService$$ExternalSyntheticLambda0.run(Unknown Source:6)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@18.2.0:2)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.lang.Thread.run(Thread.java:1012)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] 
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] android.content.res.Resources$NotFoundException: Unable to find resource ID #0x0
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.ResourcesImpl.getResourceName(ResourcesImpl.java:312)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.Resources.getResourceName(Resources.java:2355)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.n_handleIntent(Native Method)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.handleIntent(PNFirebaseMessagingService.java:32)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService.lambda$processIntent$0$com-google-firebase-messaging-EnhancedIntentService(EnhancedIntentService.java:78)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService$$ExternalSyntheticLambda0.run(Unknown Source:6)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@18.2.0:2)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.lang.Thread.run(Thread.java:1012)
[yname.Campi_app] Invalid resource ID 0x00000000.
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] 2024/11/02 12:59:53.440|ERROR|NotificationBuilder| Failed to get large icon resourceAndroid.Content.Res.Resources+NotFoundException: Unable to find resource ID #0x0
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at Java.Interop.JniEnvironment.InstanceMethods.CallObjectMethod(JniObjectReference instance, 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 19992
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualObjectMethod(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs:line 867
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at Android.Content.Res.Resources.GetResourceName(Int32 resid) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Android.Content.Res.Resources.cs:line 1439
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at Plugin.FirebasePushNotifications.Platforms.NotificationBuilder.GetLargeIconBitmap(IDictionary`2 data, Context context)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]   --- End of managed Android.Content.Res.Resources+NotFoundException stack trace ---
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] android.content.res.Resources$NotFoundException: Unable to find resource ID #0x0
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.ResourcesImpl.getResourceName(ResourcesImpl.java:312)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.Resources.getResourceName(Resources.java:2355)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.n_handleIntent(Native Method)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.handleIntent(PNFirebaseMessagingService.java:32)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService.lambda$processIntent$0$com-google-firebase-messaging-EnhancedIntentService(EnhancedIntentService.java:78)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService$$ExternalSyntheticLambda0.run(Unknown Source:6)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@18.2.0:2)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.lang.Thread.run(Thread.java:1012)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] 
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]   --- End of managed Android.Content.Res.Resources+NotFoundException stack trace ---
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] android.content.res.Resources$NotFoundException: Unable to find resource ID #0x0
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.ResourcesImpl.getResourceName(ResourcesImpl.java:312)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.Resources.getResourceName(Resources.java:2355)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.n_handleIntent(Native Method)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.handleIntent(PNFirebaseMessagingService.java:32)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService.lambda$processIntent$0$com-google-firebase-messaging-EnhancedIntentService(EnhancedIntentService.java:78)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService$$ExternalSyntheticLambda0.run(Unknown Source:6)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@18.2.0:2)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.lang.Thread.run(Thread.java:1012)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] 
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder] android.content.res.Resources$NotFoundException: Unable to find resource ID #0x0
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.ResourcesImpl.getResourceName(ResourcesImpl.java:312)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at android.content.res.Resources.getResourceName(Resources.java:2355)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.n_handleIntent(Native Method)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at crc6473c86535e68b8309.PNFirebaseMessagingService.handleIntent(PNFirebaseMessagingService.java:32)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService.lambda$processIntent$0$com-google-firebase-messaging-EnhancedIntentService(EnhancedIntentService.java:78)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.firebase.messaging.EnhancedIntentService$$ExternalSyntheticLambda0.run(Unknown Source:6)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@18.2.0:2)
[Plugin.FirebasePushNotifications.Platforms.NotificationBuilder]    at java.lang.Thread.run(Thread.java:1012)

Also trying to create my custom default notification channel as suggested, the warnings (and the errors) remain:

_notificationChannels!.CreateChannels([
new NotificationChannelRequest() {
  ChannelId = "default_channel_id",
  ChannelName = "Default Channel",
  Description = "Default notification channel",
  LockscreenVisibility = AndroidApp.NotificationVisibility.Public,
  Importance = AndroidApp.NotificationImportance.High,
  IsDefault = true
}
]);
GHGiampy commented 9 hours ago

Ah I forgot to say that with this version I was no more able to show the notification as a banner, with the hacky code or sending an high priority push notification with:

{
  "message":{
    "token":"...token...",
    "notification":{
      "title":"My title",
      "body":"My body"
    },
    "data": {
      "priority": "high"
    }   
  }
}