-
Notifications
You must be signed in to change notification settings - Fork 175
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
Conversation
63714e3
to
0747551
Compare
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. |
@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 This way this implementation avoids creating a custom intent handler and reuses Flutter existing URL route handler. |
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 |
984da67
to
1825510
Compare
1825510
to
a860492
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @rajveermalviya! LGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
There was a problem hiding this 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
lib/widgets/app.dart
Outdated
HomePage.buildRoute(accountId: accountId), | ||
InboxPage.buildRoute(accountId: accountId), | ||
notificationRoute, | ||
] else if (initialAccountId != null) ...[ | ||
HomePage.buildRoute(accountId: initialAccountId), | ||
InboxPage.buildRoute(accountId: initialAccountId), | ||
], |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
a860492
to
7b44e8b
Compare
7b44e8b
to
90e62f4
Compare
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. 🙂 |
90e62f4
to
0cf3d46
Compare
There was a problem hiding this 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.
test/notifications/display_test.dart
Outdated
// throws 'TypeError'. | ||
// Missing 'all_recipient_ids', when narrow_type == 'dm' | ||
// throws 'TypeError'. | ||
.throws<dynamic>(); |
There was a problem hiding this comment.
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):
.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
.
lib/api/notifications.dart
Outdated
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
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 :
)
test/api/notifications_test.dart
Outdated
test("${n++}", () => checkParseFails({ ...dmJson } | ||
..remove('realm_url') | ||
..remove('realm_uri'))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
test/api/notifications_test.dart
Outdated
"realm_uri": "https://zulip.example.com/", | ||
"realm_url": "https://zulip.example.com/", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similarly:
"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/", |
test/api/notifications_test.dart
Outdated
// 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); |
There was a problem hiding this comment.
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.)
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
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.
lib/host/android_notifications.dart
Outdated
/// See: https://developer.android.com/reference/android/content/Intent#constants_1 | ||
abstract class IntentFlag { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
/// 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.
pigeon/notifications.dart
Outdated
/// 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}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
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.
2e15594
to
b5a7933
Compare
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:
(Tested the above cases with both foreground and background notifications)
|
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. |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
b5a7933
to
73837e4
Compare
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.
This should never be possible.
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.
Looks good — merging. Thanks for all your work getting this transition finished! |
Fixes: #351