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

Replace flutter_local_notifications with pigeon bindings #856

Merged
merged 13 commits into from
Oct 28, 2024

Conversation

rajveermalviya
Copy link
Collaborator

@rajveermalviya rajveermalviya commented Aug 1, 2024

Fixes: #351

@gnprice
Copy link
Member

gnprice commented Aug 2, 2024

Exciting!

LMK if there are questions you have for me at this stage, or particular spots in the code you'd like an early high-level review of.

@rajveermalviya
Copy link
Collaborator Author

@gnprice, I have a question about whether this implementation is desirable. Currently it creates a pending intent using the ACTION_VIEW action with a URL payload for each notification. When the user opens the notification, the intent functions as a deeplink and is handled by the existing URL handling in Flutter. Then this url is parsed and handled here for incoming intents while the app is open and while the app is closed.

Also, when the intent is created we set explicity set the reciever component so that ensures that this instance of zulip://notificiation_open url intent is only handled by our app.

This way this implementation avoids creating a custom intent handler and reuses Flutter existing URL route handler.

@rajveermalviya
Copy link
Collaborator Author

Moved the discussion to chat thread here — https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20Drop.20.60flutter_local_notifications.60.20.23F856/near/1910352

Copy link
Collaborator

@Khader-1 Khader-1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rajveermalviya! LGTM.

@Khader-1 Khader-1 added mentor review GSoC mentor review needed. and removed buddy review GSoC buddy review needed. labels Aug 23, 2024
Copy link
Collaborator

@kenclary kenclary left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@gnprice gnprice added the maintainer review PR ready for review by Zulip maintainers label Sep 5, 2024
Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Comments below, all small. Also, I haven't done any manual testing of this; did you?

@@ -87,7 +87,7 @@ private class AndroidNotificationHost(val context: Context)
// FlutterLocalNotificationsPlugin, which handles receiving the Intent.
// TODO take care of receiving the notification-opened Intent ourselves
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

notif [nfc]: Replace intentPayload with AndroidIntent.extras

At this commit, the reference to "This […] extra name" seems like it's dangling; it's not clear from the code next to the comment that it means the string "payload". Can you make that fact clearer? It may also help to comment on the spot in the code where the string "payload" appears.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I see that this special name "payload" remains after removing flutter_local_notifications. Is that because we're happy/neutral to keep it around, or is it a sign that there's still helpful cleanup of flutter_local_notifications to do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated with main commit (notif: Handle opening of notification via URI intents) removing dangling references for payload.

HomePage.buildRoute(accountId: accountId),
InboxPage.buildRoute(accountId: accountId),
notificationRoute,
] else if (initialAccountId != null) ...[
HomePage.buildRoute(accountId: initialAccountId),
InboxPage.buildRoute(accountId: initialAccountId),
],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

notif: Introduce URL based notif open implementation

This looks like maybe a behavior change, right? Before, when you opened the app from a notification, the home page and inbox page would be for the first account in the list (the "initial account" in initialAccountId), even if that was different from the notification's account. Now, those pages are chosen to match the notification.

I agree the new behavior seems better. Let's mention the change, though, and any other user-visible behavior changes, in the commit message.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this behaviour (for now) to keep the diff smaller, this would probably be better done in a separate PR.

@chrisbobbe
Copy link
Collaborator

It looks like there's been some further discussion on these changes: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/narrow.20links.20with.20userid.3F/near/1941174

Please post here when that's resolved and this is ready for another review. 🙂

@chrisbobbe chrisbobbe removed the mentor review GSoC mentor review needed. label Oct 9, 2024
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Finished reviewing the whole branch, and generally this all looks great — just small comments below.

Would you do some manual testing of these changes, if you haven't already? In particular, see my PRs that implemented notifications and made previous changes to them; I describe there some test protocols we should repeat for these changes.

In general, notifications are one of the small number of areas where manual testing is needed to confirm things still work correctly after any significant change. That's because a key part of its behavior depends on interacting correctly with external APIs that (a) are supplied from the platform, so aren't present in a flutter test environment, and (b) have a lot of subtle details of their semantics, not all of which are necessarily even documented, and which can be easy to miss even if they are.

// throws 'TypeError'.
// Missing 'all_recipient_ids', when narrow_type == 'dm'
// throws 'TypeError'.
.throws<dynamic>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: avoid saying dynamic except where we really mean dynamic behavior (which is rare):

Suggested change
.throws<dynamic>();
.throws<Object>();

The exact equivalent to saying dynamic here would be to say Object?, which is the supertype of all types. But then we may as well be more precise: null can't be thrown, so the object thrown will always be an instance of Object.

@@ -147,6 +153,7 @@ class MessageFcmMessage extends FcmMessageWithIdentity {
if (recipient.streamName != null) result['stream'] = recipient.streamName;
result['topic'] = recipient.topic;
}
result['realm_uri'] = realmUrl.toString(); // TODO(server-9); deprecated in FL 257
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
result['realm_uri'] = realmUrl.toString(); // TODO(server-9); deprecated in FL 257
result['realm_uri'] = realmUrl.toString(); // TODO(server-9): deprecated in FL 257

(also one below; the one above is already a colon :)

Comment on lines 157 to 159
test("${n++}", () => checkParseFails({ ...dmJson }
..remove('realm_url')
..remove('realm_uri')));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test("${n++}", () => checkParseFails({ ...dmJson }
..remove('realm_url')
..remove('realm_uri')));
test("${n++}", () => checkParseFails({ ...dmJson }
..remove('realm_url')
..remove('realm_uri'))); // TODO(server-9)

That way we'll spot this line to remove it when we're removing all the pre-server-9 compatibility code.

Comment on lines 11 to 12
"realm_uri": "https://zulip.example.com/",
"realm_url": "https://zulip.example.com/",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly:

Suggested change
"realm_uri": "https://zulip.example.com/",
"realm_url": "https://zulip.example.com/",
"realm_uri": "https://zulip.example.com/", // TODO(server-9)
"realm_url": "https://zulip.example.com/",

Comment on lines 145 to 148
// FL 257 deprecated 'realm_uri' in favor of 'realm_url'.
final jsonSansRealm =
{ ...dmJson }..remove('realm_url')..remove('realm_uri');
check(parse({ ...jsonSansRealm, 'realm_uri': 'https://zulip.example.com/' })).jsonEquals(baseline);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(OTOH no need to add a TODO comment here — this test will naturally fail when we remove the fallback logic itself, which has its own such comment, and so we'll naturally find this test and remove it.)

Comment on lines +227 to +233
test('uses deprecated fields when newer fields are missing', () {
final baseline = parse(baseJson);

// FL 257 deprecated 'realm_uri' in favor of 'realm_url'.
final jsonSansRealm =
{ ...baseJson }..remove('realm_url')..remove('realm_uri');
check(parse({ ...jsonSansRealm, 'realm_url': 'https://zulip.example.com/' })).jsonEquals(baseline);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test('uses deprecated fields when newer fields are missing', () {
final baseline = parse(baseJson);
// FL 257 deprecated 'realm_uri' in favor of 'realm_url'.
final jsonSansRealm =
{ ...baseJson }..remove('realm_url')..remove('realm_uri');
check(parse({ ...jsonSansRealm, 'realm_url': 'https://zulip.example.com/' })).jsonEquals(baseline);
test('uses deprecated fields when newer fields are missing', () {
final baseline = parse(baseJson);
// FL 257 deprecated 'realm_uri' in favor of 'realm_url'.
final jsonSansRealm =
{ ...baseJson }..remove('realm_url')..remove('realm_uri');
check(parse({ ...jsonSansRealm, 'realm_uri': 'https://zulip.example.com/' })).jsonEquals(baseline);

right?

@@ -45,8 +45,6 @@ dependencies:
firebase_core: ^3.1.0
firebase_messaging: ^15.0.1
flutter_color_models: ^1.3.3+2
flutter_local_notifications: ^17.2.1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Searching for remaining references to this package, I find just one:

$ git grep -i 'flutter.?local.?notif'
android/app/src/main/res/raw/keep.xml:     The issue is that the package:flutter_local_notifications API has us

So that should be updated too.

Comment on lines 74 to 75
/// See: https://developer.android.com/reference/android/content/Intent#constants_1
abstract class IntentFlag {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
/// See: https://developer.android.com/reference/android/content/Intent#constants_1
abstract class IntentFlag {
/// See: https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_BROUGHT_TO_FRONT
abstract class IntentFlag {

There's a ton of constants on that page, so going straight to one of the flags (the one that's currently first in the list) saves some scrolling.

/// https://developer.android.com/reference/android/content/Intent
/// https://developer.android.com/reference/android/content/Intent#Intent(java.lang.String,%20android.net.Uri,%20android.content.Context,%20java.lang.Class%3C?%3E)
class AndroidIntent {
AndroidIntent({required this.action, required this.dataUrl, required this.flags});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
AndroidIntent({required this.action, required this.dataUrl, required this.flags});
AndroidIntent({required this.action, required this.dataUrl, this.flags = 0});

This isn't a parameter of the underlying constructor, so effectively it's optional in the underlying API. (One has to set it later after construction.) So making it optional here seems the most faithful to that API.

@rajveermalviya rajveermalviya force-pushed the pr-drop-fln branch 2 times, most recently from 2e15594 to b5a7933 Compare October 27, 2024 07:56
@rajveermalviya
Copy link
Collaborator Author

Thanks for the review @gnprice! Pushed a new revision, PTAL.

Although I was unable to find the mentioned test protocol, I did some fresh manual testing of following scenarios:

  • Recieved muliple messages from two different users in two different streams and thus different topics, notifications are correctly displayed and routed to correct conversation when opened.
  • Recieved muliple messages from two different users in same stream but two different topics, notifications are correctly displayed and routed to correct conversation when opened.
  • Recieved a message in one-one DM, notification is correctly displayed and routed to the DM page when opened.
  • Recieved messages in group DM with two users (total 3 users), notification is correctly displayed and routed to the group DM page when opened.

(Tested the above cases with both foreground and background notifications)

  • Recieved messages in a stream+topic conversation, notification is correctly displayed, marked as read the conversation from web client, notifications for that specific conversation were removed.
  • Recieved messages in a one-one DM, notification is correctly displayed, marked as read the conversation from web client, notifications for that specific conversation were removed.

@rajveermalviya
Copy link
Collaborator Author

In addition to that, I also had the mobile notifications enabled for all channels in multiple (busy) servers to test the realm group summary notifications.

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks for that testing. That set of scenarios sounds solid.

The revision also looks good. Just one comment below, and then I think this is ready to merge.

// FL 257 deprecated 'realm_uri' in favor of 'realm_url'.
final jsonSansRealm =
{ ...baseJson }..remove('realm_url')..remove('realm_uri');
check(parse({ ...jsonSansRealm, 'realm_url': 'https://zulip.example.com/' })).jsonEquals(baseline);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump; and I think in fact this revision took this point in the opposite direction 🙂

I believe the point of this test is to check that when the old name is present, and not the new name, we get the data from the old name. So the input this test constructs to pass to parse should use the old name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sorry about that! Updated to use the older field.

@rajveermalviya
Copy link
Collaborator Author

Thanks for the review @gnprice. Pushed a new revision, PTAL.

Zulip server 9.0 in feature level 257, deprecated `realm_uri` in
favor of `realm_url` in push notification payloads:
  https://zulip.com/api/changelog#changes-in-zulip-90

To be compatible with older servers, added a fallback to use both
fields while parsing, otherwise use the newer field in code.
Replaces the notification-open handling provided by
`package:flutter_local_notifications` with a custom Intent data
URL-based approach. Notifications will now be created with a
`PendingIntent` containing a `zulip://notification…` URL.

When a notification is opened, the Android intent's data URL is
passed through Flutter's platform-navigation system, triggering a
route. If the app is already in the foreground, the `ZulipApp`
widget's overridden `didPushRouteInformation` function handles the
route. If the app is not running, the intent data URL becomes the
initial route, read from
`WidgetsBinding.instance.platformDispatcher.defaultRouteName` in
the `ZulipApp` widget.

In both cases, the intent data URL is then passed to
`NotificationDisplayManager.navigateForNotification`, where it is
parsed and used to generate the route to the `MessageListPage` for
the relevant conversation.
As we no longer depend on `package:flutter_local_notifications`,
which required it.
The intent data URL for notification embed the conversation
information, stream-id and topic name for topic conversation, and
recipient-ids for dm conversations. So a distinct `requestCode`
isn't necessary anymore, set it to zero (matching zulip-mobile).
Docs: https://developer.android.com/reference/android/app/PendingIntent#FLAG_UPDATE_CURRENT

According to the documentation, this flag is only necessary when we want
to update the extras data without causing a new PendingIntent entry to
be created. Since we no longer pass any extras data, this flag is no
longer required.
Matching the `requestCode` is no longer necessary due to the removal
of `pakcage:flutter_local_notifications`, which required it.
@gnprice
Copy link
Member

gnprice commented Oct 28, 2024

Looks good — merging. Thanks for all your work getting this transition finished!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Replace flutter_local_notifications with a thin API binding using Pigeon
5 participants