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

[flutter_local_notifications] linux support #888

Closed
wants to merge 19 commits into from

Conversation

akemimadoka
Copy link

@akemimadoka akemimadoka commented Nov 3, 2020

  • show simple notification with payload with/without title and/or content
  • support notification with actions(buttons)
  • fetch information from notification which activates the application(not applicable due to limitations described in readme)
  • implement periodicallyShow and zonedSchedule

@MaikuB MaikuB mentioned this pull request Nov 7, 2020
2 tasks
@akemimadoka akemimadoka marked this pull request as ready for review November 11, 2020 07:23
@MaikuB
Copy link
Owner

MaikuB commented Nov 11, 2020

Thanks a lot for the PR! I'll take a look and get try to get help from the community to test it out as well

@daniel-vera-g
Copy link

Hey @akemimadoka are there any specific testing instructions?

I used the example in your forked PR repo and tried to run it: flutter run -d -v linux
But this failed.
I tested on Ubuntu 18.04.5 LTS
The relevant part of the log is probably at the end, nevertheless I included the whole log.
log.txt

@akemimadoka
Copy link
Author

Hi @daniel-vera-g This error is caused by a low version glib(the reported error suggests that the missing function requires version 2.58), I'll test the minimum required version of glib, thanks for your testing!

Copy link

@jpnurmi jpnurmi left a comment

Choose a reason for hiding this comment

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

Nice stuff! I can see notifications popping up after creating a .desktop file. 👍

I wonder if one could possibly use a CMake rule to place a .desktop file into ~/.local/share/applications. It would make the example work out of the box, and serve as a starting point for apps that want to use the package on Linux. The downside is that it kind of pollutes user's .local/share, though.

P.S. There might be something wrong with "Schedule notification to appear in 5 seconds". The notification doesn't show up, and a couple of assertion failures get printed:

(flutter_local_notifications_example:508344): GLib-GIO-CRITICAL **: 23:25:59.245: g_application_send_notification: assertion 'G_IS_NOTIFICATION (notification)' failed

(flutter_local_notifications_example:508344): GLib-GObject-CRITICAL **: 23:25:59.245: g_object_unref: assertion 'G_IS_OBJECT (object)' failed

Comment on lines 191 to 195
GValue id = G_VALUE_INIT;
g_object_get_property(G_OBJECT(getApplication()), "application-id", &id);
std::string ret = g_value_get_string(&id);
g_value_unset(&id);
return ret;
Copy link

Choose a reason for hiding this comment

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

Comment on lines 670 to 671
Map<int, Map<String, LinuxNotificationButtonHandler>> _notificationButtonHandlerMap;
Map<int, Map<String, LinuxNotificationButtonHandler>> _periodicalNotificationButtonHandlerMap;
Copy link

Choose a reason for hiding this comment

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

these could be final if they were initialized here instead of initialize()

@@ -660,3 +663,139 @@ class MacOSFlutterLocalNotificationsPlugin
}
}
}

class LinuxFlutterLocalNotificationsPlugin
Copy link

Choose a reason for hiding this comment

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

there are some analysis warnings here and there in LinuxFlutterLocalNotificationsPlugin that could be easily solved 😉

export 'src/platform_specifics/linux/enums.dart';
export 'src/platform_specifics/linux/icon.dart';
export 'src/platform_specifics/linux/initialization_settings.dart';
export 'src/platform_specifics/linux/method_channel_mappers.dart';
Copy link

Choose a reason for hiding this comment

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

"method_channel_mappers" sounds like an internal helper. is it intentionally exported?

@@ -16,6 +18,8 @@ class InitializationSettings {
/// Settings for iOS.
final IOSInitializationSettings iOS;

final LinuxInitializationSettings linux;
Copy link

Choose a reason for hiding this comment

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

add the same one-liner doc as the others around, to avoid affecting pub score?

@@ -16,6 +18,8 @@ class NotificationDetails {
/// Notification details for iOS.
final IOSNotificationDetails iOS;

final LinuxNotificationDetails linux;
Copy link

Choose a reason for hiding this comment

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

same here :)

return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
}

FlMethodResponse* show(FlMethodCall* call) {
Copy link

Choose a reason for hiding this comment

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

this is probably the whole plugin's most important function, but it's a bit hard to follow because it's so long and does many things. would it be better to try to split it up into smaller chunks?

Copy link

@jpnurmi jpnurmi left a comment

Choose a reason for hiding this comment

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

Oops I pressed the wrong button, sorry!

@MaikuB
Copy link
Owner

MaikuB commented Nov 11, 2020

@akemimadoka FYI I asked @jpnurmi to help review since he's being doing a lot of work for the Linux implementations for the Flutter plus plugins and I'm rusty when it comes to C/C++. If you could take a look at his review comments then that would be great :)

@@ -23,7 +23,8 @@ class LinuxFileIcon implements LinuxIcon {
}

/// Represents an icon from binary data
/// The binary data should be in an image file format which supported by glib, such as common formats like jpg and png
/// The binary data should be in an image file format which supported by glib,
/// such as common formats like jpg and png
class BytesLinuxIcon implements LinuxIcon {
Copy link
Owner

Choose a reason for hiding this comment

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

This reads slight awkward. Could this perhaps be renamed to something else like ByteDataLinuxIcon? Would also make it similar to https://api.flutter.dev/flutter/dart-typed_data/ByteData-class.html

@MaikuB
Copy link
Owner

MaikuB commented Nov 12, 2020

@akemimadoka there's a .cirrus.yml file at the root of the repo. Would you be able to add a task to the build script that would be build the Linux example app? Should be able to use the macOS task here as an example to follow as it involves switching to the dev branch to allow Flutter to build for desktop platforms

@MaikuB
Copy link
Owner

MaikuB commented Nov 15, 2020

I'll need to setup a VM to be able to give this a spin but FYI that I'll be merging a PR soon (#907) to provide more info on the guidelines around contribution to make reviews easier going forward. This includes info on API documentation, which follows the guidelines from here. I mention this as some changes in the PR will be needed e.g. to follow this rule.

This PR will also turn on the rule around public member API documentation. Will let you know when it's merged, after which you can update your fork so that the analyser can highlight the issues it finds.

@MaikuB
Copy link
Owner

MaikuB commented Nov 15, 2020

You should be able to update your fork to pull in the latest changes now

@MaikuB
Copy link
Owner

MaikuB commented Nov 18, 2020

Sorry for the delay, I made some progress on getting a Ubuntu VM via Parallels on my MacBook but ran into issues with getting even the basic counter app running. A window appears but won't show the elements that it should and I'm seeing a lot GDK_IS_WINDOW (window) messages Tried both the dev and master channel but still no luck. I tried the master channel as I saw a similar issue was reported with a fix that should've landed on master. It may take some more time for me to figure out how to resolve this to give the PR a spin unless anyone here has some suggestions.

@jpnurmi
Copy link

jpnurmi commented Nov 18, 2020

This is just a guess, but perhaps either enabling 3D hardware acceleration for the VM or running Flutter in software rendering mode helps?

@MaikuB
Copy link
Owner

MaikuB commented Nov 18, 2020

Ok looks like turning off 3D acceleration setting in Parallels helped. Thanks :)

@MaikuB
Copy link
Owner

MaikuB commented Nov 18, 2020

Sorry another newbie question 😅
I've run into the same issue reported earlier around GLib. I installed Flutter via the snap store and looks like it's referring to GLib 2.0. How would I go about updating GLib? I'm assuming it's the steps in the CI script are what updates GTK to get a newer version of GLib along with it i.e.

- apt update
- apt install cmake ninja-build clang pkg-config libgtk-3-dev -y

Another thing is rather than having the app fail to build due to APIs not being available because of the use of older versions of GLib, is it possible to put a guard in the code around this (e.g. via a macro) to check that a minimum version of GLib is installed? If it's possible, then I would suggest to throw an exception that could be caught on the Flutter side when app calls an API from the plugin that needs a newer version of GLib and include details of the minimum version in the exception.

Would be good to also to add info in the docs on how to update glib too as it sounds like this could be a more common issue.

}

extension LinuxNotificationDetailsMapper on LinuxNotificationDetails {
Map<String, Object> toMap() {
Copy link
Owner

Choose a reason for hiding this comment

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

analysis is picking up that this should use => here


/// Label of this button.
final String label;
/// Identifier of this button, will be passed to callback.
Copy link
Owner

Choose a reason for hiding this comment

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

Can you run flutter format . from the root of the plugin? Regular formatting would add empty lines between properties and running the code does seem to show that some files would be changes as a result. Note that if happen to be using VS Code that you can turn format on save on, which is an option that is off by default

return gtk_window_get_application(GTK_WINDOW(getTopLevel()));
}

[[maybe_unused]] std::string getApplicationId() const {
Copy link
Owner

Choose a reason for hiding this comment

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

Should this method be here? Haven't seen any reference to it

@akemimadoka
Copy link
Author

Hi @MaikuB It seems on Ubuntu 18 latest version of glib on the official apt source is 2.56, lower than 2.58 which some time zone API requires. I think it could be solved by loading some symbols dynamically, and so we can show a user-friendly prompt.

@MaikuB
Copy link
Owner

MaikuB commented Nov 19, 2020

@akemimadoka Is it possible to use an #if or #ifdef directive based on the glib version?

@akemimadoka
Copy link
Author

@MaikuB Yes, it's possible, but if we check the version at runtime, we can guarantee that the product can be run on a different environment from the building one.

@MaikuB
Copy link
Owner

MaikuB commented Nov 19, 2020

@akemimadoka What I'm proposing is that a runtime check still occurs. The goal is to still allow developers to still use the plugin even if they're using older versions of GLib as they may not care for being schedule a notification, in which case, the code shouldn't fail to compile because the plugin makes use of APIs in newer versions of GLib. This is where the use of something like an #if directive comes in. If they're calling zonedSchedule whilst on a lower version of GLib, then have the plugin throw an error on the C++ side that would end up trigger a PlatformException on the Flutter side with a message that says they need to upgrade GLib. If they meet the minimum version of GLib is met then the plugin will attempt to schedule their notification. Hope that is clearer.

Edit: in terms of code the C++ implementation for zonedSchedule would look something like

#ifdef <meets_minimum_glib_version>
 // schedule notification
#else
 // throw exception
 #endif

Copy link
Owner

@MaikuB MaikuB left a comment

Choose a reason for hiding this comment

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

I've left some additional comments based on what I can understand from the code whilst I'm still sorting out my Linux dev environment. Main areas are around show on the C++ side handling multiple ways of scheduling notification and questions on what happens if the app is restarted as it appears that things like cancelling all notifications would fail if the app had restarted

for (final LinuxNotificationButton button in notificationDetails.buttons)
button.buttonId : button.handler,
} : null;
return _channel.invokeMethod('show', <String, Object> {
Copy link
Owner

Choose a reason for hiding this comment

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

This should be the only method that specifies show as the method being called when calling invokeMethod. This makes it easier to match the code on the Dart side and would make it less confusing for readers of the code as convention within the ecosystem has been that the string matches the name of the method. Currently other methods are also passing show.

It also prevents the show method on the C++ side from being responsible for triggering notifications in multiple ways as is the case right now. This makes it more risky to change code as a common method is being used for the various plugin APIs. I get why it was done as I did a similar thing in the past but that caused issues with maintenance. Making the change should also resolve the comment @jpnurmi raised on how the implementation of show in C++ was rather long

if (_notificationButtonHandlerMap.remove(id) != null) {
return _channel.invokeMethod('cancel', id);
} else if (_periodicNotificationButtonHandlerMap.remove(id) != null) {
return _channel.invokeMethod('cancelPeriodicNotification', id);
Copy link
Owner

Choose a reason for hiding this comment

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

Call _channel.invokeMethod('cancel', ...) for similar reasons I mentioned about how _channel.invokeMethod( 'show', ...) was called in multiple places. An additional parameter could be passed to indicate the handler should be cancelled on the Dart side.


@override
Future<void> cancelAll() async {
for (final int id in _notificationButtonHandlerMap.keys) {
Copy link
Owner

Choose a reason for hiding this comment

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

What happens if the user triggers a notification that is displayed, leaves the notification there and restarts the app? If I'm not mistaken, looks as though cancelling notifications and handling buttons would rely on having the same app session active. FYI that cancelling notifications and handling when the user taps on a notification (note: other platforms don't support action buttons at the moment) for all other platforms can be done even if the user restarted the app so if what I described is indeed how it works right now (as I'm still having some trouble with my Linux environment to test) then a solution will need to be found for Linux to behave the same. I imagine, this would potentially need to rely on persisting some information about the notification

@akemimadoka
Copy link
Author

@MaikuB If we check the version at build time and a user uses it in an environment where the version of glib doesn't meet the requirement, it will fail at runtime with a not user-friendly prompt like "symbol not defined". Both solutions are not hard to implement. If the situation which I described above is not too serious, I will implement it with macros.😸

@MaikuB
Copy link
Owner

MaikuB commented Nov 23, 2020

FYI I still have issues running the app. The furthest I got was to remove code that makes use of timezone APIs and then I was able to run the app but couldn't get a notification, which I did by running the flutter run command. I know the docs mention creating a desktop file, which I tried to do but didn't help. I may have done it incorrectly though.

Another thing is with the recent changes I've seen, it looks like information about notification ids only remain in memory and that scheduling notifications only work if the app is still running. This is what I'd expect to be able to do with around cancelling notifications and scheduling notifications

  1. Have one or more notifications showing. Restart the app and cancelAll should remove all notifications
  2. Schedule a notification whilst the app is running. Kill the app and the notification should appear even if the app isn't running. This applies even if this was a recurring notification.

From what I can tell from this PR, neither of these are supported. Please correct me if I'm wrong

@akemimadoka
Copy link
Author

To implement these features, some limitations should be noted:

  1. We cannot remove notifications without id by GTK API. It can be solved by using some dirty methods like post some javascript script to gnome-shell, but less portability.
  2. As far as I know, It can be implemented by using cron or systemd, but it's obviously making portability worse.
    So in my opinion I think there is a trade-off between feature and portability.

@MaikuB
Copy link
Owner

MaikuB commented Nov 24, 2020

https://pub.dev/documentation/path_provider_linux/latest/path_provider_linux/PathProviderLinux/getApplicationSupportPath.html

  1. We cannot remove notifications without id by GTK API. It can be solved by using some dirty methods like post some javascript script to gnome-shell, but less portability.

Yep that is I could see from the API docs. This is why I tried to ask about storing the ids. I imagine it should be possible for applications to store data that it needs. The Linux shared_preferences plugin allows for this (see https://github.com/flutter/plugins/blob/592b5b27431689336fa4c721a099eedf787aeb56/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart#L32), which in turn calls this API. I would imagine a similar implementation could be used here to save the ids.

2. As far as I know, It can be implemented by using cron or systemd, but it's obviously making portability worse.
So in my opinion I think there is a trade-off between feature and portability.

I'm not that familiar with Linux so I'm not sure what you refer to here around portability. From a user's perspective, they shouldn't have to keep their application and device running to see a scheduled notification. This would also achieve feature parity with how other platforms work as those platforms provide ways for scheduled notifications to work regardless of the application's state. I doubt it'll be of much use if the notification can only be shown when the application is running e.g. using notifications for a periodic reminder

class NotificationIdPersister extends LinuxNotificationNotifier {
@override
void onNewNotificationCreated(int notificationId) {
SharedPreferences.getInstance().then((SharedPreferences value) async {
Copy link
Owner

Choose a reason for hiding this comment

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

To clarify, I wasn't suggesting to use the shared preferences plugin. In fact it should be avoided to minimise dependencies where possible. What I was referring is to look at where they save the info and have the plugin do the same. I would imagine these two lines of Dart code

  final processName = path.basenameWithoutExtension(
      await File('/proc/self/exe').resolveSymbolicLinks());
  final directory = Directory(path.join(xdg.dataHome.path, processName));

have an equivalent in C++

Copy link
Author

Choose a reason for hiding this comment

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

I agree, so I'm only using it in the example program, users can choose their favorite method to persist the data.
In the plugin, we are only notifying the user by LinuxNotificationNotifier.

Copy link
Owner

Choose a reason for hiding this comment

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

Sorry, I was reviewing on my phone and thought the only changes would've been to the plugin so missed that this was for the example app. Note though what I was also referring to is having the plugin manage all of this. The core reason for this is goes to why a cross-platform library/SDK is typically chosen as it allows feature parity to achieved with minimal effort In other words, allowing users to get their apps and running quicker. Having the plugin manage it removes additional burden from the user. If how data is stored is that much of a concern, then that tends to be when the data needs to be stored securely and would likely require a custom implementation.

Another issue is that the plugin is now relying the on user to provide a collection of notification ids. This makes it a bit awkward as if a user is picking a library to manage notifications, I dare say the vast majority of users would be expecting the plugin to do so on its own. Having to require users to pass this info means the user becomes the source of truth (note: they could pass erroneous values) when it should really be the plugin. I also just noticed that the method for pending notification requests isn't implemented and doing so would most likely require the plugin to save the notification details anyway. If the user needed to persist this data via the LinuxNotificationNotifier and then feed it back to the plugin for the API to get the pending notification requests to work, this makes it even more awkward.

If some of what I've raised so far including being able to have scheduled notifications occur even when the app isn't running becomes too much an effort to do, feel free to close the PR. Reason being is I imagine you're doing this on your spare time so can understand if you decide to drop working on this. Though in the case of handling scheduled notifications, perhaps one way is to simply mark those methods as not being implemented. Not sure how many applications would find it useful to be able to display a notification immediately though

cc @jpnurmi: if you happen to have time, would appreciate your thoughts on what I've raised here and in the thread so far but can understand if you're not able to do so

Copy link

Choose a reason for hiding this comment

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

Hi @MaikuB, sorry I've been a bit busy lately, but I'll try to catch up with what's been going on here later.

@MaikuB
Copy link
Owner

MaikuB commented Jan 1, 2021

Closing this as changes were requested in a number of places (e.g. how scheduling notification works and having the notification ids saved to storage by the plugin itself) but haven't been actioned for a while. Feel free to re-open for further discussion

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

Successfully merging this pull request may close these issues.

None yet

4 participants