-
Notifications
You must be signed in to change notification settings - Fork 397
notif: On Android handle notification taps via Pigeon API #2043
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
base: main
Are you sure you want to change the base?
notif: On Android handle notification taps via Pigeon API #2043
Conversation
2e2b67b to
3521955
Compare
chrisbobbe
left a comment
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! I'm not super familiar with this code. I think the main thing I'd like to understand is why so much in the ios/ directory is touched here, when the issue and PR description are pretty explicit that this is an Android bugfix. (There's even a name with "android" that appears in a file in ios/, which I've pointed out below; I'm confused about that.)
test/model/binding.dart
Outdated
| late final _notificationTapEventsStreamController = | ||
| StreamController<NotificationTapEvent>(); |
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.
Is late final intended, instead of just final?
| } | ||
|
|
||
| /// Generated class from Pigeon that represents data sent in messages. | ||
| struct AndroidNotificationTapEvent: NotificationTapEvent { |
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.
Why is the name AndroidNotificationTapEvent appearing in a file in ios/? That looks like a smell to me; can it be avoided?
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.
This is because of a limitation in Pigeon, AndroidNotificationTapEvent is canonically defined in pigeon/notification.dart and every other class definitions with the same name in *.g.{dart,swift,kt} is generated by Pigeon. And there doesn't seem to be a convenient annotation to use to indicate a platform-specific type.
AIUI only way to conditionally generate these types would be to have separate files for Android an iOS in pigeon/, and @ConfigurePigeon(PigeonOptions(…)) in each of those would specify paths only for their corresponding platforms. Another complexity is that NotificationTapEvent is a sealed class, and I am not sure how well Pigeon's generator supports multi-file Dart library.
Do note that these types (AndroidNotificationTapEvent in Notification.g.swift, and IosNotificationTapEvent in Notifications.g.kt) remain unused, so they probably get tree-shaken out during the build.
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've pushed 4b1198b which separates the renaming of NotificationTapEvent -> IosNotificationTapEvent in a nfc commit, for easier review of the final commit.
3521955 to
b900a61
Compare
|
Thanks for the review @chrisbobbe! Pushed an update, PTAL. Also see #2043 (comment) for the reason why there are changes being made in |
b900a61 to
c791db0
Compare
|
(Rebased to fix conflicts with #2059.) |
chrisbobbe
left a comment
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! Glad to be fixing this bug; comments below.
| ValueNotifier<String?> token = ValueNotifier(null); | ||
|
|
||
| Future<void> start() async { | ||
| await NotificationOpenService.instance.start(); |
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: Move NotificationOpenService init at the start of NotificationService init
In the commit-message summary line, do you mean s/at/to/?
lib/notifications/open.dart
Outdated
| } | ||
| } |
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: keep newline at end of file
pigeon/notifications.dart
Outdated
| /// An event that is only emitted on iOS platform when a notification is | ||
| /// tapped on. |
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: can make one line:
/// On iOS, an event emitted when a notification is tapped.| /// tapped on. | ||
| /// | ||
| /// See [notificationTapEvents]. | ||
| class IosNotificationTapEvent extends NotificationTapEvent { |
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]: Rename NotificationTapEvent to IosNotificationTapEvent
Also make NotificationTapEvent a base sealed class, because we will
soon introduce another variant for Android too.
This is really a refactor, not a rename: now there are two classes, where there was just one, and they have a certain relationship with each other. Could you look back through this commit to make sure all the code that refers to each class respects the meaning you've assigned to it?
For example, I see a dartdoc below that doesn't respect the new meaning of NotificationTapEvent—
@EventChannelApi()
abstract class NotificationEventChannelApi {
/// An event stream that emits a notification payload when the app
/// encounters a notification tap, while the app is running.
///
/// Emits an event when
/// `userNotificationCenter(_:didReceive:withCompletionHandler:)` gets
/// called, indicating that the user has tapped on a notification. The
/// emitted payload will be the raw APNs data dictionary from the
/// `UNNotificationResponse` passed to that method.
NotificationTapEvent notificationTapEvents();
}—because it assumes the context is iOS (by referring to iOS APIs) despite NotificationTapEvent now being explicitly not specific to iOS (because it has an Ios… subclass). Here, I think maybe the fix is to introduce the paragraph with "On iOS…", and have a parallel paragraph for Android saying that it's unimplemented / does nothing for now, but will soon.
pigeon/notifications.dart
Outdated
| /// An event stream that emits a notification payload when the app | ||
| /// encounters a notification tap, while the app is running. | ||
| /// encounters a notification tap, on iOS and Android while the app is | ||
| /// running, or only on Android when apps was launched by tapping a | ||
| /// notification. |
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.
Four lines is quite long for a dartdoc heading. How about:
/// An event stream that emits a notification payload
/// when a notification is tapped.and factor the rest into the "On iOS" and "On Android" paragraphs.
| * | ||
| * Do not call if `intent.action` is not ACTION_VIEW. | ||
| */ | ||
| fun maybeHandleViewNotif(intent: Intent): Boolean { |
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.
I don't love this method's name. An ACTION_VIEW intent is the thing this method will receive and decide whether to handle specially—I see that in the dartdoc and the assert—not a "view notif". In fact, "view notif" isn't really a coherent name for anything.
| case TargetPlatform.android: | ||
| // Do nothing; we do notification routing differently on Android. | ||
| // TODO migrate Android to use the new Pigeon API. | ||
| break; |
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.
I don't love having a defaultTargetPlatform == TargetPlatform.iOS condition as the first thing that runs in a case TargetPlatform.android.
I think it would be appropriate here for the .listen call to appear twice in the code (once in the iOS case, after .getNotificationDataFromLaunch(), once in the Android case). From reading your implementation comment here, it sounds like the stream effectively has different meanings between the two platforms, right? It's probably helpful to give .listen a dartdoc that documents those different meanings.
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.
lib/notifications/open.dart
Outdated
| /// the given [AndroidNotificationTapEvent] which carries | ||
| /// `zulip://notification/…` Android intent data URL. |
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.
| /// the given [AndroidNotificationTapEvent] which carries | |
| /// `zulip://notification/…` Android intent data URL. | |
| /// the given [AndroidNotificationTapEvent] which carries a | |
| /// `zulip://notification/…` Android intent data URL. |
lib/notifications/open.dart
Outdated
| /// The URL should have been generated with | ||
| /// [NotificationOpenPayload.buildAndroidNotificationUrl] | ||
| /// when creating the notification. |
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.
Can we delegate to AndroidNotificationTapEvent the job of saying what dataUrl is? (I think just by referring to the relevant methods of NotificationOpenPayload in the field's dartdoc?)
Then this method's dartdoc can be…deleted, actually:
- the "Navigate appropriately" is clear from the context the method is called in, and from its name
- the details of what the input means and looks like are all provided in the param's type,
AndroidNotificationTapEvent.
pigeon/notifications.dart
Outdated
| /// An event that is only emitted on Android platform when a notification is | ||
| /// tapped on. |
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, this can be shortened to one line.)
c791db0 to
d8cf3ef
Compare
|
Thanks for the review @chrisbobbe! Pushed an update, PTAL. CI failure is unrelated, it is failing because of dart-lang/sdk@19b06b5 in newer than checked-in version of Flutter SDK and #2086 should fix it. |
…Service init NotificationOpenService.instance.start already handles all the platforms itself, by doing nothing on all except iOS, so move it's initialization out of the platform-specific switch here.
…cription Initialize StreamController early, this will allow scheduling mock notification tap events (via `addNotificationTapEvent`) even before `notificationTapEventsStream` is called.
The event channel API declaration in Pigeon are quite weird because it's declared as a method on an abstract class but the generated method in `*.g.dart` is a global function. AIUI, the declaration in the Pigeon file cannot be a global function, because then it will need an implementation and thus the abstract class "hack". But not sure why the generated code for it is a global function as opposed to a static method on a abstract class. Anyway, this change fixes the missing dartdoc on the generated files. My guess is that each event channel API declarations are supposed to be one abstract class + one method pair, but there doesn't seem to be any error when a new method is introduced on the same abstract class.
…cationTapEvent This change refactors NotificationTapEvent to be a base sealed class. This will be helpful because we will soon introduce another variant for Android.
Instead of relying on Flutter's deeplinks implementation for routing the notification URL, handle the Android Intents generated by notification taps ourselves using Pigeon to pass those events over to the Dart layer from the Java layer. The upstream Flutter's deeplinks implementation has a bug where if the deeplink is triggered after the app was killed by the OS when it was in background, the app will get launched again but the route/link will not reach the Flutter's navigation handlers. See: flutter/flutter#178305 In the failure case we seem to be receiving the Android Intent for the notification tap from the OS via `MainActivity.onNewIntent` without any problems. So, to workaround that upstream bug this commit changes the implementation to handle these Android Intents ourselves. Fixes: zulip#1567
d8cf3ef to
d02e89c
Compare
Instead of relying on Flutter's deeplinks implementation for routing the notification URL, handle the Android Intents generated by notification taps ourselves using Pigeon to pass those events over to the Dart layer from the Java layer.
The upstream Flutter's deeplinks implementation has a bug where if the deeplink is triggered after the app was killed by the OS when it was in background, the app will get launched again but the route/link will not reach the Flutter's navigation handlers. See: flutter/flutter#178305
In the failure case we seem to be receiving the Android Intent for the notification tap from the OS via
MainActivity.onNewIntentwithout any problems. So, to workaround that upstream bug this commit changes the implementation to handle these Android Intents ourselves.Fixes: #1567