Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[messaging] Ability to change the notification before it's sent to the system tray #2639

Open
helenaford opened this issue May 3, 2021 · 11 comments · May be fixed by #3492
Open

[messaging] Ability to change the notification before it's sent to the system tray #2639

helenaford opened this issue May 3, 2021 · 11 comments · May be fixed by #3492
Assignees
Labels

Comments

@helenaford
Copy link

helenaford commented May 3, 2021

What feature would you like to see?

Hi, is there anything on the roadmap to provide the ability to customise the notification before it's sent to the system tray. I'm a maintainer of notifee, and we recently added support to do this with iOS, we'd love to be able to do it with Android too.

Currently, the only option there seems to be is to send data messages. A couple of issues with this method are that you need to send two different types of messages based on whether the device is android or iOS, and having to keep track of which platform the token is registered with.

In addition to platform-dependent code, there's also different background restrictions for data-only vs notification messages (ref here).

On iOS, you can alter the notification using a Notification Service Extension where you have a brief amount of time to modify the notification before it's displayed to the user. in addition to the flexibility that message.apns.aps object offers where you can add a categoryId, sound, threadId e.t.c.

One possible solution could be to allow FirebaseMessagingService.onMessageReceived to be called for all app states, maybe a check somehow to see if the app does supports this, if not, default to handling with FCM? If this was possible, the one issue I do see with this, is that it could clash with other libraries like FlutterFire and react-native-firebase as you can only have one FirebaseMessagingService per app.

Another solution, which I think seems way easier and less intrusive is to have a separate service, like how iOS works, to alter the notification and nothing else.

I can see how this could be a big f/r, and maybe there's reasons that it's not been done?

The other possible solution is to expand the predefined set of user-visible keys for message.android, to include group and other NotificationCompat properties where possible.

To summarise, the two pain points I think there is at the moment with Android is:

  • the options allowed via message.android is limited.
  • no ability to intercept the notification before it's shown when the app is in background

How would you use it?

Tell us how you'd use this feature in your app.

This feature would allow users of this library to send notification messages and customise the notification before it's finally sent to the system tray.

@google-oss-bot
Copy link
Contributor

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

@sfuqua
Copy link

sfuqua commented Feb 23, 2022

The comparison to the iOS Notification Service Extension is a good one - I think Apple has a fairly elegant solution here in allowing an app to effectively "tamper" with a notification payload before it's handled by the system. I'd love to do exactly the same thing in Android Firebase land - mutate the RemoteMessage before the SDK handles it.

I have legacy services that are sending a notification payload with my FCM messages. It's not impossible to change these to data only, but it's costly, as it's a breaking change for existing clients and requires me to spend some political capital on getting the services updated and redeployed with new payload versions - and I have to repeat that per notification type, because nearly all of them are impacted. Allowing a client to intercept the payload and remove the "notification" blob before the SDK handles it would be a godsend.

Some extensible method on FirebaseMessagingService, like overrideMessageProperties(RemoteMessage), that an app can implement would do just the trick.

edit: It looks like this is something that could be handled fairly trivially with just changes to the existing Java code in FirebaseMessagingService.dispatchMessage, specifically before the current isNotification check that then routes the message to a DisplayNotification. We'd just need a protected function that allows inspecting the Intent and then opting out of automatic notification behavior.

I can't find docs describing the current SDK behavior for isNotification, where it's looking for "gcm.n.e" in the payload via

public static final String ENABLE_NOTIFICATION = NOTIFICATION_PREFIX + "e";

I have to assume this is based on historical GCM behavior and how the Google services format the notification before it hits the Firebase SDK client code, so it's not as trivial as letting an app receive a RemoteMessage and then tamper with it.

Is there a design reason not to allow an app to opt-out of this behavior? I'm open to potentially creating a PR if we can reach consensus on the approach?

@sfuqua
Copy link

sfuqua commented Feb 23, 2022

To spitball a surgical proposed change without more edits on my previous post, it'd basically be:

  1. New overrideable method on FirebaseMessagingService:
/**
 * Called when determining whether to opt an Intent out of default system tray behavior for a "notification message".
 * @return true if this message should fall-through to {@link #onMessageReceived}, false for default system behavior.
 */
public boolean shouldOptOutOfNotificationBehavior(@NonNull RemoteMessage message) { return false; }
  1. Update to call this.shouldOptOutOfNotificationBehavior(new RemoteMessage(data)) as an && clause with the existing isNotification check.

unless I am misunderstanding, this would unblock the desired behavior of allowing an app to configure the rendered notification while backgrounded, in an opt-out fashion so existing clients are unaffected by default.

I'd love to allow modifying the Intent/Bundle before the SDK handles it, but since there seems to be a bunch of protected GCM keys stuffed into the payload (by the system?) I can see why we may not want to expose that to an application. However, allowing an app to inspect the corresponding RemoteMessage as a proxy and make a "go"/"no-go" decision for the system tray based on that payload is almost as good? I'm basically desperate for an escape hatch right now as the mandatory Notification behavior is breaking my ability to take advantage of rich Android notification features for my customers, unless I refactor production web service code to send "data only" payloads, and only for Android, and only for new app versions to avoid breaking existing clients in the wild.

@sfuqua
Copy link

sfuqua commented Feb 26, 2022

I've been neck deep in Firebase SDK code for the past couple days and am getting confused.

Specifically, these comments on a similar issue from 2018:

#46 (comment)

This is a technical restriction that's outside the scope of the Android SDK. It's really an OS-level decision.

#46 (comment)

samtstern is correct that this is a change that has to be made in Google Play Services which currently does not pass display notifications onto the SDK before being displayed. This is something the FCM team is looking into, we would like to have as unified an experience on Android and iOS as possible.

They plainly indicate that the behavior we're talking about (display messages getting auto-posted to the system tray with no ability for the app to intervene) is Android platform/OS behavior and not Firebase SDK behavior.

But in looking at SDK code and in actually testing this functionality, that does not seem to be correct. Did this change between 2018 (time of comments) and 2020 (time FCM was open sourced and committed to this repo)?

Specifically this code is what prevents calling onMessageReceived in the background and instead manually displays a Notification (using standard, public Android APIs - no magic):

I have confirmed this evening that if I override the undocumented FirebaseMessagingService.handleIntent and don't call the super implementation, no notification gets posted automatically. I tested as far back as Nougat/Android 7 on an emulator - same behavior. So unless I'm missing something it's the SDK doing this and not the Android OS, and we should be able to fix this in the SDK.

I still have my fingers crossed that a kind Googler will tell me where I've lost the plot here and misunderstood how the platform works, because from the Java in this repo and my testing, it's seeming easily fixable - I'll likely try to start work on a PR for an API to work around the current behavior as described in my previous comment.

@gsakakihara
Copy link
Contributor

Something similar has been discussed and proposed in the past internally and has come up again recently, but we don't have anything new to report at this time. Support for this is mostly a product question that comes down to prioritization, demand for the feature, and some other considerations.

@sfuqua
Copy link

sfuqua commented Feb 28, 2022

Something similar has been discussed and proposed in the past internally and has come up again recently, but we don't have anything new to report at this time. Support for this is mostly a product question that comes down to prioritization, demand for the feature, and some other considerations.

@gsakakihara PR #3492 aims to provide a tactical new API to unblock this, I'm hoping you or the team can shed some light on whether it's a feasible approach or if I'm missing something (more fundamental on an Android platform level) about why this might be unsupported.

@sfuqua
Copy link

sfuqua commented Sep 30, 2022

For anyone following this issue - since the Firebase team is not showing any interest in my PR to tweak the API for this scenario, my app team has been working around this problem with a custom FirebaseMessagingService that overrides handleIntent to intercept specific notification Intents before they're sent to Firebase SDK and automatically displayed, and instead displaying them ourselves.

This seems to work just fine despite being an undocumented workaround. I have not received any reports of it causing problems despite being live for a few months.

The approach is based on how the SDK's service works under the covers and is something like:

// this is inside a custom FirebaseMessagingService which we've hooked up in our AndroidManifest

// 1. excessively document for your team with links to GitHub issues, PRs, StackOverflow, etc
// 2. emphasize that this is a hack which risks breaking in future Firebase SDK updates :(
// 3. dream of an officially supported API for this scenario
@Override
public void handleIntent(Intent intent) {
  Bundle extras = intent.getExtras();
  
  // try/catch around this to be defensive; default to super.handleIntent
  RemoteMessage message = new RemoteMessage(extras);
  
  RemoteMessage.Notification notificationData = message.getNotification();
  // short-circuit if this is not a display notification
  if (notificationData == null) { super.handleIntent(intent); return; }
  
  // 1. filter on the notification however you want using the data payload, clickAction, etc
  // 2. manually display notifications that match your filter, and then return
  // 3. fall-through to this for everything else...
  super.handleIntent(intent);
}

@andreibarabas
Copy link

thanks @sfuqua for suggesting this. your approach makes total sense. will use it a starting base

@0x090909
Copy link

ping any news?

@0x090909
Copy link

@sfuqua Thank you ! It works for me as well

@aureosouza
Copy link

@sfuqua thank you for this, we were able to implement similar approach overriding the handleIntent from the class that extends FirebaseMessagingService (in our case we are using react-native-firebase, so that would be ReactNativeFirebaseMessagingService). @helenaford We're using notifee as well, so we've added an extra fetchNotificationData to getDisplayedNotifications module method, that injects the data param from firebase (if it exists):

private void createNotificationChannel() {
    // Same as Firebase SDK default channel name and ids
    NotificationChannel channel = new NotificationChannel("fcm_fallback_notification_channel", "Miscellaneous", NotificationManager.IMPORTANCE_HIGH);
    NotificationManager notificationManager = getSystemService(NotificationManager.class);
    notificationManager.createNotificationChannel(channel);
  }

  private int getNotificationIcon() {
    int iconResId;
    iconResId = getResources().getIdentifier("ic_notification", "drawable", getPackageName());
    if (iconResId == 0) {
      iconResId = getApplicationInfo().icon;
    }

    return iconResId;
  }

  @Override
  public void handleIntent(Intent intent) {
    Bundle extras = intent.getExtras();

    try {
      RemoteMessage message = new RemoteMessage(extras);
      RemoteMessage.Notification notificationData = message.getNotification();

      if (notificationData == null) {
        super.handleIntent(intent);
        return;
      }

      Bundle customExtras = new Bundle();
      for (Map.Entry<String, String> entry : message.getData().entrySet()) {
        customExtras.putString(entry.getKey(), entry.getValue());
      }
      createNotificationChannel();

      NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "fcm_fallback_notification_channel")
        .setSmallIcon(getNotificationIcon())
        .setContentTitle(notificationData.getTitle())
        .setContentText(notificationData.getBody())
        .setAutoCancel(true)
        .setPriority(NotificationCompat.PRIORITY_HIGH);

      notificationBuilder.getExtras().putBundle("data", customExtras);
      NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
      // Same as tag from Firebase SDK is built
      notificationManager.notify("FCM-Notification:" + SystemClock.uptimeMillis(), 0, notificationBuilder.build());

    } catch (Exception e) {
      super.handleIntent(intent);
    }
  }

And in Notifee module:

private List<Bundle> fetchNotificationData(List<Bundle> aBundleList) {
    NotificationManager notificationManager = (NotificationManager) getReactApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
    if (notificationManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      StatusBarNotification[] activeNotifications = notificationManager.getActiveNotifications();

      for (StatusBarNotification sbn : activeNotifications) {
        Notification notification = sbn.getNotification();
        Bundle extras = notification.extras;

        if (extras != null) {
          Bundle data = extras.getBundle("data");
          if (data != null) {
            for (Bundle originalBundle : aBundleList) {
              Bundle originalNotificationBundle = originalBundle.getBundle("notification");
              if (originalNotificationBundle != null && originalNotificationBundle.getString("id").equals(String.valueOf(sbn.getId()))) {
                originalNotificationBundle.putBundle("data", data);
              }
            }
          }
        }
      }
    }
    return aBundleList;
  }


  @ReactMethod
  public void getDisplayedNotifications(Promise promise) {
    Notifee.getInstance()
      .getDisplayedNotifications(
        (e, aBundleList) -> NotifeeReactUtils.promiseResolver(promise, e, fetchNotificationData(aBundleList)));
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
10 participants