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
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion flutter_local_notifications/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ A cross platform plugin for displaying local notifications.
* **Android 4.1+**. Uses the [NotificationCompat APIs](https://developer.android.com/reference/androidx/core/app/NotificationCompat) so it can be run older Android devices
* **iOS 8.0+**. On iOS versions older than 10, the plugin will use the UILocalNotification APIs. The [UserNotification APIs](https://developer.apple.com/documentation/usernotifications) (aka the User Notifications Framework) is used on iOS 10 or newer.
* **macOS 10.11+**. On macOS versions older than 10.14, the plugin will use the [NSUserNotification APIs](https://developer.apple.com/documentation/foundation/nsusernotification). The [UserNotification APIs](https://developer.apple.com/documentation/usernotifications) (aka the User Notifications Framework) is used on macOS 10.14 or newer.
* **Linux with gnome 2.40+**. Uses the [GNotification APIs](https://developer.gnome.org/gio/stable/GNotification.html), which supported on gnome 2.40+
* **Linux with glib 2.58+**. Uses the [GNotification APIs](https://developer.gnome.org/gio/stable/GNotification.html), which supported on glib 2.40+, some time zone api requires version 2.58+.

## ✨ Features

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export 'src/platform_specifics/ios/notification_details.dart';
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';
export 'src/platform_specifics/linux/notification_details.dart';
export 'src/platform_specifics/macos/initialization_settings.dart';
export 'src/platform_specifics/macos/notification_attachment.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class InitializationSettings {
/// Settings for iOS.
final IOSInitializationSettings iOS;

/// Settings for Linux.
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?


/// Settings for iOS.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class NotificationDetails {
/// Notification details for iOS.
final IOSNotificationDetails iOS;

/// Notification details for Linux.
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 :)


/// Notification details for macOS.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import 'platform_specifics/ios/enums.dart';
import 'platform_specifics/ios/initialization_settings.dart';
import 'platform_specifics/ios/method_channel_mappers.dart';
import 'platform_specifics/ios/notification_details.dart';
import 'platform_specifics/macos/initialization_settings.dart';
import 'platform_specifics/macos/method_channel_mappers.dart';
import 'platform_specifics/macos/notification_details.dart';
import 'platform_specifics/linux/initialization_settings.dart';
import 'platform_specifics/linux/method_channel_mappers.dart';
import 'platform_specifics/linux/notification_details.dart';
import 'platform_specifics/macos/initialization_settings.dart';
import 'platform_specifics/macos/method_channel_mappers.dart';
import 'platform_specifics/macos/notification_details.dart';
import 'type_mappers.dart';
import 'typedefs.dart';
import 'types.dart';
Expand Down Expand Up @@ -667,21 +667,24 @@ 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 😉

extends MethodChannelFlutterLocalNotificationsPlugin {
SelectNotificationCallback _onSelectNotification;
Map<int, Map<String, LinuxNotificationButtonHandler>> _notificationButtonHandlerMap;
Map<int, Map<String, LinuxNotificationButtonHandler>> _periodicalNotificationButtonHandlerMap;
final Map<int, Map<String, LinuxNotificationButtonHandler>>
_notificationButtonHandlerMap =
<int, Map<String, LinuxNotificationButtonHandler>>{};
final Map<int, Map<String, LinuxNotificationButtonHandler>>
_periodicNotificationButtonHandlerMap =
<int, Map<String, LinuxNotificationButtonHandler>>{};

Future<bool> initialize(
LinuxInitializationSettings initializationSettings, {
SelectNotificationCallback onSelectNotification,
}) async {
_onSelectNotification = onSelectNotification;
_notificationButtonHandlerMap = Map();
_periodicalNotificationButtonHandlerMap = Map();
_channel.setMethodCallHandler(_handleMethod);
return await _channel.invokeMethod(
'initialize', initializationSettings?.toMap());
}

@override
Future<void> show(
int id,
String title,
Expand All @@ -690,11 +693,11 @@ class LinuxFlutterLocalNotificationsPlugin
String payload,
}) {
validateId(id);
_notificationButtonHandlerMap[id] = notificationDetails != null ? Map.fromIterable(
notificationDetails.buttons,
key: (button) => button.buttonId,
value: (button) => button.handler,
) : null;
_notificationButtonHandlerMap[id] = notificationDetails != null
? <String, LinuxNotificationButtonHandler> {
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

'id': id,
'title': title,
Expand All @@ -704,6 +707,7 @@ class LinuxFlutterLocalNotificationsPlugin
});
}

@override
Future<void> periodicallyShow(
int id,
String title,
Expand All @@ -713,11 +717,11 @@ class LinuxFlutterLocalNotificationsPlugin
String payload,
}) {
validateId(id);
_periodicalNotificationButtonHandlerMap[id] = notificationDetails != null ? Map.fromIterable(
notificationDetails.buttons,
key: (button) => button.buttonId,
value: (button) => button.handler,
) : null;
_periodicNotificationButtonHandlerMap[id] = notificationDetails != null
? <String, LinuxNotificationButtonHandler> {
for (final LinuxNotificationButton button in notificationDetails.buttons)
button.buttonId : button.handler,
} : null;
return _channel.invokeMethod('show', <String, Object> {
'id': id,
'title': title,
Expand All @@ -739,7 +743,13 @@ class LinuxFlutterLocalNotificationsPlugin
}) async {
validateId(id);
validateDateIsInTheFuture(scheduledDate);
final serializedPlatformSpecifics = notificationDetails?.toMap();
_periodicNotificationButtonHandlerMap[id] = notificationDetails != null
? <String, LinuxNotificationButtonHandler> {
for (final LinuxNotificationButton button in notificationDetails.buttons)
button.buttonId : button.handler,
} : null;
final Map<String, Object> serializedPlatformSpecifics
= notificationDetails?.toMap();
await _channel.invokeMethod(
'show',
<String, Object>{
Expand All @@ -754,48 +764,54 @@ class LinuxFlutterLocalNotificationsPlugin
..addAll(scheduledDate.toMap()));
}

@override
Future<void> cancel(int id) {
validateId(id);
if (_notificationButtonHandlerMap.remove(id) != null) {
return _channel.invokeMethod('cancel', id);
} else if (_periodicalNotificationButtonHandlerMap.remove(id) != null) {
} 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.

} else {
throw ArgumentError.value(id, "id", "id is invalid");
throw ArgumentError.value(id, 'id', 'id is invalid');
}
}

@override
Future<void> cancelAll() async {
for (final id in _notificationButtonHandlerMap.keys) {
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

await _channel.invokeMethod('cancel', id);
}
for (final id in _periodicalNotificationButtonHandlerMap.keys) {
for (final int id in _periodicNotificationButtonHandlerMap.keys) {
await _channel.invokeMethod('cancelPeriodicNotification', id);
}
_notificationButtonHandlerMap.clear();
_periodicalNotificationButtonHandlerMap.clear();
_periodicNotificationButtonHandlerMap.clear();
}

Future<void> _handleMethod(MethodCall call) {
switch (call.method) {
case 'selectNotification':
final args = call.arguments as Map;
final Map<dynamic, dynamic> args = call.arguments;
_notificationButtonHandlerMap.remove(args['id']);
_onSelectNotification(args['payload']);
break;
case 'selectNotificationButton':
final args = call.arguments as Map;
final id = args['id'] as int;
final buttonId = args['buttonId'] as String;
final buttonHandlerMap = _notificationButtonHandlerMap.remove(id) ?? _periodicalNotificationButtonHandlerMap[id];
assert(buttonHandlerMap != null, "Handler map is corrupted");
final handler = buttonHandlerMap[buttonId];
final Map<dynamic, dynamic> args = call.arguments;
final int id = args['id'];
final String buttonId = args['buttonId'];
final Map<String, LinuxNotificationButtonHandler> buttonHandlerMap
= _notificationButtonHandlerMap.remove(id)
?? _periodicNotificationButtonHandlerMap[id];
assert(buttonHandlerMap != null, 'Handler map is corrupted');
final LinuxNotificationButtonHandler handler =
buttonHandlerMap[buttonId];
if (handler != null) {
handler(id, buttonId);
}
break;
default:
return Future<void>.error('Method not defined');
}
return Future<void>.value();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

const BytesLinuxIcon(this.data);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,33 @@ import 'notification_details.dart';
extension LinuxIconMapper on LinuxIcon {
Map<String, Object> toMap() {
return <String, Object> {
"icon": content,
"iconSource": source.index,
'icon': content,
'iconSource': source.index,
};
}
}

extension LinuxInitializationSettingsMapper on LinuxInitializationSettings {
Map<String, Object> toMap() {
return <String, Object> {
"defaultIcon": defaultIcon?.toMap(),
'defaultIcon': defaultIcon?.toMap(),
};
}
}

extension LinuxNotificationButtonSetMapper on Set<LinuxNotificationButton> {
List<Map<String, String>> serializeToList() {
return map((e) => <String, String>{
"buttonLabel": e.label,
"buttonId": e.buttonId,
List<Map<String, String>> serializeToList() =>
map((LinuxNotificationButton e) => <String, String>{
'buttonLabel': e.label,
'buttonId': e.buttonId,
}).toList(growable: false);
}
}

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

return <String, Object> {
"icon": icon?.toMap(),
"buttons": buttons?.serializeToList(),
'icon': icon?.toMap(),
'buttons': buttons?.serializeToList(),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,28 @@ import 'package:meta/meta.dart';

import 'icon.dart';

/// Signature of callback which triggered when user taps a button of a notification.
typedef void LinuxNotificationButtonHandler(int id, String buttonId);
/// Signature of callback which triggered when user taps
/// a button of a notification.
typedef LinuxNotificationButtonHandler = void Function(int id, String buttonId);

/// Details of notification button on Linux.
@immutable
class LinuxNotificationButton {
const LinuxNotificationButton({@required this.label, @required this.buttonId, @required this.handler});
const LinuxNotificationButton(
{@required this.label, @required this.buttonId, @required this.handler});

/// Label of this button
final String label;
/// Identifier of this button, will be passed to callback when tapped
/// Identifier of this button, will be passed to callback
final String buttonId;
final LinuxNotificationButtonHandler handler;

bool operator==(covariant LinuxNotificationButton other) => buttonId == other.buttonId;
@override
bool operator==(covariant LinuxNotificationButton other) =>
buttonId == other.buttonId;

@override
int get hashCode => buttonId.hashCode;
}

/// Configures notification details specific to Linux.
Expand Down
Loading