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 all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions .cirrus.yml
Expand Up @@ -33,6 +33,20 @@ task:
- cd flutter_local_notifications/example
- flutter build macos

task:
name: Build Linux example app
container:
image: cirrusci/flutter:dev
pub_cache:
folder: ~/.pub-cache
setup_script:
- apt update
- apt install cmake ninja-build clang pkg-config libgtk-3-dev -y
- flutter config --enable-linux-desktop
build_script:
- cd flutter_local_notifications/example
- flutter build linux

task:
name: Run platform interface tests
container:
Expand Down
8 changes: 8 additions & 0 deletions flutter_local_notifications/README.md
Expand Up @@ -16,6 +16,7 @@ A cross platform plugin for displaying local notifications.
- [Scheduled notifications and daylight savings](#scheduled-notifications-and-daylight-savings)
- [Custom notification sounds](#custom-notification-sounds)
- [macOS differences](#macos-differences)
- [Linux limitations](#Linux-limitations)
- **[📷 Screenshots](#-screenshots)**
- **[👏 Acknowledgements](#-acknowledgements)**
- **[⚙️ Android Setup](#️-android-setup)**
Expand Down Expand Up @@ -47,6 +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 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 Expand Up @@ -110,6 +112,12 @@ Due to limitations currently within the macOS Flutter engine, `getNotificationAp

The `schedule`, `showDailyAtTime` and `showWeeklyAtDayAndTime` methods that were implemented before macOS support was added and have been marked as deprecated aren't implemented on macOS.

##### Linux limitations

Gnome-shell will only show notifications from applications registered in [desktop files](https://developer.gnome.org/integration-guide/stable/desktop-files.html), if no desktop file which base name matches the application id was found, your notification will not be shown. For more information, see [this](https://developer.gnome.org/GNotification/#Preliminaries).

To respond to notification after the program is terminated, your program should be registered as DBus activatable(see [DBusApplicationLaunching](https://wiki.gnome.org/HowDoI/DBusApplicationLaunching) for more information), and register action before activating the application. It is hard to do so in a plugin because plugins are instantiating during application activation, so `getNotificationAppLaunchDetails` cannot be implemented without altering the user's main program.

## 📷 Screenshots

| Platform | Screenshot |
Expand Down
69 changes: 67 additions & 2 deletions flutter_local_notifications/example/lib/main.dart
Expand Up @@ -13,6 +13,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:rxdart/subjects.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:shared_preferences/shared_preferences.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Expand Down Expand Up @@ -42,6 +43,35 @@ class ReceivedNotification {
final String payload;
}

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.

List<String> persistedNotifications;
if (value.containsKey('persistedNotifications')) {
persistedNotifications = value.getStringList('persistedNotifications');
} else {
persistedNotifications = <String>[];
}
await value.setStringList('persistedNotifications',
persistedNotifications..add(notificationId.toString()));
});
}

@override
void onNotificationDestroyed(int notificationId) {
SharedPreferences.getInstance().then((SharedPreferences value) async {
if (!value.containsKey('persistedNotifications')) {
return;
}
final List<String> persistedNotifications =
value.getStringList('persistedNotifications');
await value.setStringList('persistedNotifications',
persistedNotifications..remove(notificationId.toString()));
});
}
}

/// IMPORTANT: running the following code on its own won't work as there is
/// setup required for each platform head project.
///
Expand Down Expand Up @@ -71,6 +101,23 @@ Future<void> main() async {
didReceiveLocalNotificationSubject.add(ReceivedNotification(
id: id, title: title, body: body, payload: payload));
});

Set<int> knownShowingNotifications;
if (Platform.isLinux) {
final SharedPreferences pref = await SharedPreferences.getInstance();
if (pref.containsKey('persistedNotifications')) {
knownShowingNotifications = pref
.getStringList('persistedNotifications')
.map((String e) => int.parse(e))
.toSet();
debugPrint('knownShowingNotifications: $knownShowingNotifications');
}
}
final LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(
notificationNotifier: NotificationIdPersister(),
knownShowingNotifications: knownShowingNotifications,
);
const MacOSInitializationSettings initializationSettingsMacOS =
MacOSInitializationSettings(
requestAlertPermission: false,
Expand All @@ -79,6 +126,7 @@ Future<void> main() async {
final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
linux: initializationSettingsLinux,
macOS: initializationSettingsMacOS);
await flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: (String payload) async {
Expand Down Expand Up @@ -562,8 +610,25 @@ class _HomePageState extends State<HomePage> {
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker');
const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);
final ByteData iconData = await rootBundle.load('icons/coworker.png');
final LinuxNotificationDetails linuxPlatformChannelSpecifics =
LinuxNotificationDetails(
icon: ByteDataLinuxIcon(iconData.buffer.asUint8List()),
buttons: <LinuxNotificationButton>{
const LinuxNotificationButton(
label: 'label',
payload: 'button1',
),
const LinuxNotificationButton(
label: 'label2',
payload: 'button2',
),
},
);
final NotificationDetails platformChannelSpecifics = NotificationDetails(
android: androidPlatformChannelSpecifics,
linux: linuxPlatformChannelSpecifics,
);
await flutterLocalNotificationsPlugin.show(
0, 'plain title', 'plain body', platformChannelSpecifics,
payload: 'item x');
Expand Down
1 change: 1 addition & 0 deletions flutter_local_notifications/example/linux/.gitignore
@@ -0,0 +1 @@
flutter/ephemeral
106 changes: 106 additions & 0 deletions flutter_local_notifications/example/linux/CMakeLists.txt
@@ -0,0 +1,106 @@
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)

set(BINARY_NAME "flutter_local_notifications_example")
set(APPLICATION_ID "com.dexterous.flutter_local_notifications")

cmake_policy(SET CMP0063 NEW)

set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")

# Configure build options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()

# Compilation settings that should be applied to most targets.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()

set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")

# Flutter library and tool build rules.
add_subdirectory(${FLUTTER_MANAGED_DIR})

# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)

add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")

# Application build
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
apply_standard_settings(${BINARY_NAME})
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)

# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)


# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()

# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)

set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")

install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)

install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)

install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)

if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)

# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
88 changes: 88 additions & 0 deletions flutter_local_notifications/example/linux/flutter/CMakeLists.txt
@@ -0,0 +1,88 @@
cmake_minimum_required(VERSION 3.10)

set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")

# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)

# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.

# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()

# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid)

set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")

# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)

list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
PkgConfig::BLKID
)
add_dependencies(flutter flutter_assemble)

# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
linux-x64 ${CMAKE_BUILD_TYPE}
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)
@@ -0,0 +1,13 @@
//
// Generated file. Do not edit.
//

#include "generated_plugin_registrant.h"

#include <flutter_local_notifications/flutter_local_notifications_plugin.h>

void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_local_notifications_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalNotificationsPlugin");
flutter_local_notifications_plugin_register_with_registrar(flutter_local_notifications_registrar);
}
@@ -0,0 +1,13 @@
//
// Generated file. Do not edit.
//

#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_

#include <flutter_linux/flutter_linux.h>

// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);

#endif // GENERATED_PLUGIN_REGISTRANT_
@@ -0,0 +1,16 @@
#
# Generated file, do not edit.
#

list(APPEND FLUTTER_PLUGIN_LIST
flutter_local_notifications
)

set(PLUGIN_BUNDLED_LIBRARIES)

foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)