From 278f9b308dab647b538a35eb6f9cb64bc0cb6d54 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 9 Feb 2022 10:41:39 +0000 Subject: [PATCH 001/112] Start impl of Windows notifications --- .../example/lib/main.dart | 2 +- .../example/pubspec.yaml | 6 + .../example/windows/.gitignore | 21 ++ .../example/windows/CMakeLists.txt | 95 +++++++ .../example/windows/flutter/CMakeLists.txt | 103 ++++++++ .../flutter/generated_plugin_registrant.cc | 17 ++ .../flutter/generated_plugin_registrant.h | 15 ++ .../windows/flutter/generated_plugins.cmake | 17 ++ .../example/windows/runner/CMakeLists.txt | 17 ++ .../example/windows/runner/Runner.rc | 121 +++++++++ .../example/windows/runner/flutter_window.cpp | 61 +++++ .../example/windows/runner/flutter_window.h | 33 +++ .../example/windows/runner/main.cpp | 43 +++ .../example/windows/runner/resource.h | 16 ++ .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 20 ++ .../example/windows/runner/utils.cpp | 64 +++++ .../example/windows/runner/utils.h | 19 ++ .../example/windows/runner/win32_window.cpp | 245 ++++++++++++++++++ .../example/windows/runner/win32_window.h | 98 +++++++ .../flutter_local_notifications_plugin.dart | 12 + .../platform_flutter_local_notifications.dart | 13 + flutter_local_notifications/pubspec.yaml | 6 + .../windows/.gitignore | 17 ++ .../windows/CMakeLists.txt | 65 +++++ .../flutter_local_notifications_plugin.cpp | 141 ++++++++++ .../flutter_local_notifications_plugin.h | 23 ++ .../flutter_local_notifications/methods.h | 7 + .../windows/methods.cpp | 6 + .../windows/utils/utils.cpp | 12 + .../windows/utils/utils.h | 11 + 31 files changed, 1325 insertions(+), 1 deletion(-) create mode 100644 flutter_local_notifications/example/windows/.gitignore create mode 100644 flutter_local_notifications/example/windows/CMakeLists.txt create mode 100644 flutter_local_notifications/example/windows/flutter/CMakeLists.txt create mode 100644 flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc create mode 100644 flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.h create mode 100644 flutter_local_notifications/example/windows/flutter/generated_plugins.cmake create mode 100644 flutter_local_notifications/example/windows/runner/CMakeLists.txt create mode 100644 flutter_local_notifications/example/windows/runner/Runner.rc create mode 100644 flutter_local_notifications/example/windows/runner/flutter_window.cpp create mode 100644 flutter_local_notifications/example/windows/runner/flutter_window.h create mode 100644 flutter_local_notifications/example/windows/runner/main.cpp create mode 100644 flutter_local_notifications/example/windows/runner/resource.h create mode 100644 flutter_local_notifications/example/windows/runner/resources/app_icon.ico create mode 100644 flutter_local_notifications/example/windows/runner/runner.exe.manifest create mode 100644 flutter_local_notifications/example/windows/runner/utils.cpp create mode 100644 flutter_local_notifications/example/windows/runner/utils.h create mode 100644 flutter_local_notifications/example/windows/runner/win32_window.cpp create mode 100644 flutter_local_notifications/example/windows/runner/win32_window.h create mode 100644 flutter_local_notifications/windows/.gitignore create mode 100644 flutter_local_notifications/windows/CMakeLists.txt create mode 100644 flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp create mode 100644 flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h create mode 100644 flutter_local_notifications/windows/include/flutter_local_notifications/methods.h create mode 100644 flutter_local_notifications/windows/methods.cpp create mode 100644 flutter_local_notifications/windows/utils/utils.cpp create mode 100644 flutter_local_notifications/windows/utils/utils.h diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index ed4c693d1..eec636a47 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -210,7 +210,7 @@ Future main() async { } Future _configureLocalTimeZone() async { - if (kIsWeb || Platform.isLinux) { + if (kIsWeb || Platform.isLinux || Platform.isWindows) { return; } tz.initializeTimeZones(); diff --git a/flutter_local_notifications/example/pubspec.yaml b/flutter_local_notifications/example/pubspec.yaml index bb5792d96..07f4f5ad9 100644 --- a/flutter_local_notifications/example/pubspec.yaml +++ b/flutter_local_notifications/example/pubspec.yaml @@ -24,6 +24,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + msix: ^2.8.17 dependency_overrides: flutter_local_notifications_platform_interface: @@ -40,3 +41,8 @@ flutter: environment: sdk: '>=2.12.0-0 <3.0.0' flutter: '>=1.26.0-0' # allows for using integration_test from SDK + +msix_config: + display_name: Flutter Local Notifications Example + identity_name: Com.Example.FlutterLocalNotificationsExample + debug: true diff --git a/flutter_local_notifications/example/windows/.gitignore b/flutter_local_notifications/example/windows/.gitignore new file mode 100644 index 000000000..571c3131e --- /dev/null +++ b/flutter_local_notifications/example/windows/.gitignore @@ -0,0 +1,21 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +.vs +out +flutter/ephemeral diff --git a/flutter_local_notifications/example/windows/CMakeLists.txt b/flutter_local_notifications/example/windows/CMakeLists.txt new file mode 100644 index 000000000..1633297a0 --- /dev/null +++ b/flutter_local_notifications/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +set(BINARY_NAME "example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + 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() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# 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 /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +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. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/flutter_local_notifications/example/windows/flutter/CMakeLists.txt b/flutter_local_notifications/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000..b2e4bd8d6 --- /dev/null +++ b/flutter_local_notifications/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +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. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# 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/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app 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. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..d5a662fbd --- /dev/null +++ b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterLocalNotificationsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterLocalNotificationsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.h b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..dc139d85a --- /dev/null +++ b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake b/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000..41753c0a3 --- /dev/null +++ b/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,17 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + flutter_local_notifications + url_launcher_windows +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/flutter_local_notifications/example/windows/runner/CMakeLists.txt b/flutter_local_notifications/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000..de2d8916b --- /dev/null +++ b/flutter_local_notifications/example/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/flutter_local_notifications/example/windows/runner/Runner.rc b/flutter_local_notifications/example/windows/runner/Runner.rc new file mode 100644 index 000000000..81d27749b --- /dev/null +++ b/flutter_local_notifications/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.dexterous" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.dexterous. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/flutter_local_notifications/example/windows/runner/flutter_window.cpp b/flutter_local_notifications/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000..b43b9095e --- /dev/null +++ b/flutter_local_notifications/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/flutter_local_notifications/example/windows/runner/flutter_window.h b/flutter_local_notifications/example/windows/runner/flutter_window.h new file mode 100644 index 000000000..6da0652f0 --- /dev/null +++ b/flutter_local_notifications/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/flutter_local_notifications/example/windows/runner/main.cpp b/flutter_local_notifications/example/windows/runner/main.cpp new file mode 100644 index 000000000..bcb57b0e2 --- /dev/null +++ b/flutter_local_notifications/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/flutter_local_notifications/example/windows/runner/resource.h b/flutter_local_notifications/example/windows/runner/resource.h new file mode 100644 index 000000000..66a65d1e4 --- /dev/null +++ b/flutter_local_notifications/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/flutter_local_notifications/example/windows/runner/resources/app_icon.ico b/flutter_local_notifications/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/flutter_local_notifications/example/windows/runner/runner.exe.manifest b/flutter_local_notifications/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000..c977c4a42 --- /dev/null +++ b/flutter_local_notifications/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/flutter_local_notifications/example/windows/runner/utils.cpp b/flutter_local_notifications/example/windows/runner/utils.cpp new file mode 100644 index 000000000..d19bdbbcc --- /dev/null +++ b/flutter_local_notifications/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/flutter_local_notifications/example/windows/runner/utils.h b/flutter_local_notifications/example/windows/runner/utils.h new file mode 100644 index 000000000..3879d5475 --- /dev/null +++ b/flutter_local_notifications/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/flutter_local_notifications/example/windows/runner/win32_window.cpp b/flutter_local_notifications/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000..c10f08dc7 --- /dev/null +++ b/flutter_local_notifications/example/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/flutter_local_notifications/example/windows/runner/win32_window.h b/flutter_local_notifications/example/windows/runner/win32_window.h new file mode 100644 index 000000000..17ba43112 --- /dev/null +++ b/flutter_local_notifications/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index c25900f19..793b71709 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -40,6 +40,9 @@ class FlutterLocalNotificationsPlugin { } else if (defaultTargetPlatform == TargetPlatform.linux) { FlutterLocalNotificationsPlatform.instance = LinuxFlutterLocalNotificationsPlugin(); + } else if (defaultTargetPlatform == TargetPlatform.windows) { + FlutterLocalNotificationsPlatform.instance = + WindowsFlutterLocalNotificationsPlugin(); } } @@ -85,6 +88,11 @@ class FlutterLocalNotificationsPlugin { FlutterLocalNotificationsPlatform.instance is LinuxFlutterLocalNotificationsPlugin) { return FlutterLocalNotificationsPlatform.instance as T?; + } else if (defaultTargetPlatform == TargetPlatform.windows && + T == WindowsFlutterLocalNotificationsPlugin && + FlutterLocalNotificationsPlatform.instance + is WindowsFlutterLocalNotificationsPlugin) { + return FlutterLocalNotificationsPlatform.instance as T?; } return null; @@ -175,6 +183,10 @@ class FlutterLocalNotificationsPlugin { return await resolvePlatformSpecificImplementation< MacOSFlutterLocalNotificationsPlugin>() ?.getNotificationAppLaunchDetails(); + } else if (defaultTargetPlatform == TargetPlatform.windows) { + return await resolvePlatformSpecificImplementation< + WindowsFlutterLocalNotificationsPlugin>() + ?.getNotificationAppLaunchDetails(); } else { return await FlutterLocalNotificationsPlatform.instance .getNotificationAppLaunchDetails() ?? diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 227bb0803..0325df60b 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -850,6 +850,19 @@ class MacOSFlutterLocalNotificationsPlugin } } +class WindowsFlutterLocalNotificationsPlugin + extends MethodChannelFlutterLocalNotificationsPlugin { + @override + Future show(int id, String? title, String? body, {String? payload}) { + return _channel.invokeMethod('show', { + 'id': id, + 'title': title, + 'body': body, + 'payload': payload ?? '', + }); + } +} + /// Checks [backgroundHandler] method, if not `null`, for eligibility to /// be used as a background callback. /// diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index e02e70747..cf9694de7 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -13,6 +13,10 @@ dependencies: flutter_local_notifications_platform_interface: ^6.0.0 timezone: ^0.8.0 +dependency_overrides: + flutter_local_notifications_platform_interface: + path: ../flutter_local_notifications_platform_interface + dev_dependencies: flutter_driver: sdk: flutter @@ -33,6 +37,8 @@ flutter: pluginClass: FlutterLocalNotificationsPlugin linux: default_package: flutter_local_notifications_linux + windows: + pluginClass: FlutterLocalNotificationsPlugin environment: sdk: '>=2.12.0 <3.0.0' diff --git a/flutter_local_notifications/windows/.gitignore b/flutter_local_notifications/windows/.gitignore new file mode 100644 index 000000000..b3eb2be16 --- /dev/null +++ b/flutter_local_notifications/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/flutter_local_notifications/windows/CMakeLists.txt b/flutter_local_notifications/windows/CMakeLists.txt new file mode 100644 index 000000000..c604cdd31 --- /dev/null +++ b/flutter_local_notifications/windows/CMakeLists.txt @@ -0,0 +1,65 @@ +cmake_minimum_required(VERSION 3.14) +set(PROJECT_NAME "flutter_local_notifications") +project(${PROJECT_NAME} LANGUAGES CXX) +include(FetchContent) + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "flutter_local_notifications_plugin") + +# nuget configuration +set(NUGET_PACKAGES_PATH "${CMAKE_BINARY_DIR}/packages") +set(CPPWINRT_VERSION "2.0.220131.2") + +FetchContent_Declare(nuget + URL "https://dist.nuget.org/win-x86-commandline/v6.0.0/nuget.exe" + URL_HASH SHA256=04eb6c4fe4213907e2773e1be1bbbd730e9a655a3c9c58387ce8d4a714a5b9e1 + DOWNLOAD_NO_EXTRACT true +) +find_program(NUGET nuget) +if (NOT NUGET) + message("Nuget.exe not found, trying to download or use cached version.") + FetchContent_MakeAvailable(nuget) + set(NUGET ${nuget_SOURCE_DIR}/nuget.exe) +endif() + +function(nuget_install pkg ver) + execute_process(COMMAND + ${NUGET} install ${pkg} -Version ${ver} -OutputDirectory ${NUGET_PACKAGES_PATH} + RESULT_VARIABLE result) + if (NOT result EQUAL 0) + message(FATAL_ERROR "Failed to install nuget package ${pkg}, version ${ver}") + endif() +endfunction() + +add_library(${PLUGIN_NAME} SHARED + "flutter_local_notifications_plugin.cpp" + "methods.cpp" + "utils/utils.cpp") + +# setup c++/winrt +nuget_install("Microsoft.Windows.CppWinRT" ${CPPWINRT_VERSION}) +set(CPPWINRT ${NUGET_PACKAGES_PATH}/Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}/bin/cppwinrt.exe) +execute_process(COMMAND + ${CPPWINRT} -input sdk -output include + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE ret) +if (NOT ret EQUAL 0) + message(FATAL_ERROR "Result ${ret} ${CPPWINRT} Failed to run cppwinrt.exe") +endif() + +include_directories(BEFORE SYSTEM ${CMAKE_BINARY_DIR}/include) + +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +# List of absolute paths to libraries that should be bundled with the plugin +set(flutter_local_notifications_bundled_libraries + "" + PARENT_SCOPE +) diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp new file mode 100644 index 000000000..354447339 --- /dev/null +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -0,0 +1,141 @@ +#include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" +#include "include/flutter_local_notifications/methods.h" +#include "utils/utils.h" + +// This must be included before many other Windows headers. +#include +#include +#include +#include +#include + +// For getPlatformVersion; remove unless needed for your plugin implementation. +#include + +#include +#include +#include + +#include +#include +#include + +using namespace winrt::Windows::Data::Xml::Dom; + +namespace { + + class FlutterLocalNotificationsPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + + FlutterLocalNotificationsPlugin(); + + virtual ~FlutterLocalNotificationsPlugin(); + + private: + winrt::Windows::UI::Notifications::ToastNotificationManager toastManager; + + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + void ShowNotification( + const std::string& title, + const std::string& body, + const std::optional& payload); + }; + + // static + void FlutterLocalNotificationsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows* registrar) { + auto channel = + std::make_unique>( + registrar->messenger(), "dexterous.com/flutter/local_notifications", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto& call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); + + SetCurrentProcessExplicitAppUserModelID(L"Com.Example.Flutter.FlutterLocalNotificationPlugin"); + } + + FlutterLocalNotificationsPlugin::FlutterLocalNotificationsPlugin() : + toastManager{} {} + + FlutterLocalNotificationsPlugin::~FlutterLocalNotificationsPlugin() {} + + void FlutterLocalNotificationsPlugin::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + std::cout << method_call.method_name() << std::endl; + std::cout << Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS << std::endl; + const auto& method_name = method_call.method_name(); + if (method_name == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { + result->Success(); + } + else if (method_name == Method::SHOW) { + const auto args = std::get_if(method_call.arguments()); + if (args != nullptr) { + const auto& title = Utils::GetString("title", args).value(); + const auto& body = Utils::GetString("body", args).value(); + const auto payload = Utils::GetString("payload", args); + + ShowNotification(title, body, payload); + result->Success(); + } + else { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + } + } + else { + result->NotImplemented(); + } + } + + void FlutterLocalNotificationsPlugin::ShowNotification( + const std::string& title, + const std::string& body, + const std::optional& payload) { + /*XmlDocument doc; + doc.LoadXml(L"\ + \ + \ + \ + \ + \ + \ + \ + "); + + if (payload.has_value()) { + doc.DocumentElement().SetAttribute(L"launch", winrt::to_hstring(payload.value())); + }*/ + //doc.SelectSingleNode(L"//text[1]").InnerText(winrt::to_hstring(title)); + //doc.SelectSingleNode(L"//text[2]").InnerText(winrt::to_hstring(body)); + + const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText01); + + XmlNodeList nodes = doc.GetElementsByTagName(L"text"); + nodes.Item(0).AppendChild(doc.CreateTextNode(winrt::to_hstring(title))); + + winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; + const auto notifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(L"example"); + + notifier.Show(notif); + } + +} // namespace + +void FlutterLocalNotificationsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + FlutterLocalNotificationsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h b/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h new file mode 100644 index 000000000..dda06d016 --- /dev/null +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ +#define FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void FlutterLocalNotificationsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h new file mode 100644 index 000000000..b890c23b9 --- /dev/null +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h @@ -0,0 +1,7 @@ +#include + +namespace Method +{ + extern const std::string GET_NOTIFICATION_APP_LAUNCH_DETAILS; + extern const std::string SHOW; +} \ No newline at end of file diff --git a/flutter_local_notifications/windows/methods.cpp b/flutter_local_notifications/windows/methods.cpp new file mode 100644 index 000000000..ccb4f0533 --- /dev/null +++ b/flutter_local_notifications/windows/methods.cpp @@ -0,0 +1,6 @@ +#include "include/flutter_local_notifications/methods.h" + +#include + +const std::string Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS = "getNotificationAppLaunchDetails"; +const std::string Method::SHOW = "show"; diff --git a/flutter_local_notifications/windows/utils/utils.cpp b/flutter_local_notifications/windows/utils/utils.cpp new file mode 100644 index 000000000..c649f5e35 --- /dev/null +++ b/flutter_local_notifications/windows/utils/utils.cpp @@ -0,0 +1,12 @@ +#include "utils.h" + +#include + +std::optional Utils::GetString(const std::string& key, const flutter::EncodableMap* m) { + const auto pair = m->find(key); + if (pair == m->end()) { + return std::nullopt; + } + const auto& str = std::get(pair->second); + return str; +} diff --git a/flutter_local_notifications/windows/utils/utils.h b/flutter_local_notifications/windows/utils/utils.h new file mode 100644 index 000000000..81483089c --- /dev/null +++ b/flutter_local_notifications/windows/utils/utils.h @@ -0,0 +1,11 @@ +#ifndef UTILS_H_ +#define UTILS_H_ + +#include +#include + +namespace Utils { + std::optional GetString(const std::string& key, const flutter::EncodableMap* m); +} + +#endif // !UTILS_H From ac1cd51a446ea40c72d3e58152543a0d81f97db1 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 9 Feb 2022 22:06:18 +0000 Subject: [PATCH 002/112] Remove dependency override --- flutter_local_notifications/pubspec.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index cf9694de7..522fc2107 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -13,10 +13,6 @@ dependencies: flutter_local_notifications_platform_interface: ^6.0.0 timezone: ^0.8.0 -dependency_overrides: - flutter_local_notifications_platform_interface: - path: ../flutter_local_notifications_platform_interface - dev_dependencies: flutter_driver: sdk: flutter From e0370c0e9a80f57ac555f9eb86002c283ba4fb5e Mon Sep 17 00:00:00 2001 From: Kenneth Date: Thu, 10 Feb 2022 21:40:27 +0000 Subject: [PATCH 003/112] Fix notification content not showing --- .../example/windows/runner/RCa04060 | Bin 0 -> 6140 bytes .../example/windows/runner/RCb04060 | Bin 0 -> 6140 bytes .../example/windows/runner/main.cpp | 3 ++ .../windows/CMakeLists.txt | 7 ++-- .../flutter_local_notifications_plugin.cpp | 35 ++++++------------ .../windows/utils/utils.cpp | 2 +- 6 files changed, 19 insertions(+), 28 deletions(-) create mode 100644 flutter_local_notifications/example/windows/runner/RCa04060 create mode 100644 flutter_local_notifications/example/windows/runner/RCb04060 diff --git a/flutter_local_notifications/example/windows/runner/RCa04060 b/flutter_local_notifications/example/windows/runner/RCa04060 new file mode 100644 index 0000000000000000000000000000000000000000..a890c5b8c104a80079cadbf1a154fdc8e88c6819 GIT binary patch literal 6140 zcmdUzTTfd@5Xa}aQoqBgd1=+k#oXHTjf+jJ5R5Q(S}Br+upv~zMi`P*>WA;|zjKyz z@iDPUAB>LdJ-cUTb}s*!o!vG2XxBEjktNo!n(f;k_MWF_Q^xN2S8r@rc0q3xdMCV1 z>~DG(cAw6;gwF%L`#?3IH{t6!?EvZt4bde^fxqh9o3v~Y8zOyxWWOmTVuRMvp4KhpLDZfi(Ppr?_6h6`}!D2u2 zBv@#at|JXcHe^f|6Wv2tNj9Fc;vMw2%&CW!wT?@0%a?Qb-0+_;Zvu}I_PY(EBm2>I zY}?j(8un94BbhId;~~sV(5Ad*-9A+4xRcUr1^LAhuxlsQwKKebU~TKL_Ng^(mtMz? z?a2OOq|5(X+hgR2_h>0;v3gIGF1CpsqOYSN^U@_Yn_%?>pM2nZf(4rPC;x4<-bYW_ zqsjONzghc6?rPV9EsGv;R61R?HyXD20xY=}V#g7afQsi}r5I<9!qvk~lqh;O!TZXA z$`MmAk`RkMMy?+ty~~>;YD#R*qGX!JTRzLYO2?EL6Z;`VaE!$vE9_be6nYGHiMJln zxQ`?Q(0Ih7b^hf^=2D));}pyfpgM-$v&Ipl$Bg!94+E~_h|fEOcbnLLZNI>~%e*~C zoTfZuz*o-#dCzVTDKQ!@Q|YP@qxkBh*&C!dcmWY0Tx6K2&Jyvv>!101!+sC64Qx7} zat})!hP)&C>TLd=9l&=jdDZQfxBh0DO_Z$`U#Ti{)!0Y1H{^3yja^iG={S`^{jbU6 zNZ(vao@H0rVsCVtE5i?wTT$hY{I(eW@^;)xJr=XU>a}^SxsL5nH;f-qT1Me^Zl_7W znW~I2wvnH;`z7Q!RZps0RO__CS7M*YZK^S}H>%n?ht4Y;(f;F%4VIDe2z{>t7uod|i^`W*)D`!SHl6*V)6{-A&1~Q~ogIAlueN-h zue->p{XgTbiovT??NgtSV~F3LwrMA&7uuwG^qP8k6cjB`4_FXJgr9j<^*gd5SkWC> z8COM)%W(QX{GXi~vbSOs_OZa-qdlIAzas9>pi(4Fv1WG9=O|SM@aWX3vVoVk)4Y;N zk-g>X@CK;xvy-x2b#32d{M*mxt5@Mii9CB}r^yBu^gAQ3wPY3DYVr!%MLh3po#eD9jKsxnYqd-bdnEi*q3{H}d56#NHi#3p+H literal 0 HcmV?d00001 diff --git a/flutter_local_notifications/example/windows/runner/RCb04060 b/flutter_local_notifications/example/windows/runner/RCb04060 new file mode 100644 index 0000000000000000000000000000000000000000..a890c5b8c104a80079cadbf1a154fdc8e88c6819 GIT binary patch literal 6140 zcmdUzTTfd@5Xa}aQoqBgd1=+k#oXHTjf+jJ5R5Q(S}Br+upv~zMi`P*>WA;|zjKyz z@iDPUAB>LdJ-cUTb}s*!o!vG2XxBEjktNo!n(f;k_MWF_Q^xN2S8r@rc0q3xdMCV1 z>~DG(cAw6;gwF%L`#?3IH{t6!?EvZt4bde^fxqh9o3v~Y8zOyxWWOmTVuRMvp4KhpLDZfi(Ppr?_6h6`}!D2u2 zBv@#at|JXcHe^f|6Wv2tNj9Fc;vMw2%&CW!wT?@0%a?Qb-0+_;Zvu}I_PY(EBm2>I zY}?j(8un94BbhId;~~sV(5Ad*-9A+4xRcUr1^LAhuxlsQwKKebU~TKL_Ng^(mtMz? z?a2OOq|5(X+hgR2_h>0;v3gIGF1CpsqOYSN^U@_Yn_%?>pM2nZf(4rPC;x4<-bYW_ zqsjONzghc6?rPV9EsGv;R61R?HyXD20xY=}V#g7afQsi}r5I<9!qvk~lqh;O!TZXA z$`MmAk`RkMMy?+ty~~>;YD#R*qGX!JTRzLYO2?EL6Z;`VaE!$vE9_be6nYGHiMJln zxQ`?Q(0Ih7b^hf^=2D));}pyfpgM-$v&Ipl$Bg!94+E~_h|fEOcbnLLZNI>~%e*~C zoTfZuz*o-#dCzVTDKQ!@Q|YP@qxkBh*&C!dcmWY0Tx6K2&Jyvv>!101!+sC64Qx7} zat})!hP)&C>TLd=9l&=jdDZQfxBh0DO_Z$`U#Ti{)!0Y1H{^3yja^iG={S`^{jbU6 zNZ(vao@H0rVsCVtE5i?wTT$hY{I(eW@^;)xJr=XU>a}^SxsL5nH;f-qT1Me^Zl_7W znW~I2wvnH;`z7Q!RZps0RO__CS7M*YZK^S}H>%n?ht4Y;(f;F%4VIDe2z{>t7uod|i^`W*)D`!SHl6*V)6{-A&1~Q~ogIAlueN-h zue->p{XgTbiovT??NgtSV~F3LwrMA&7uuwG^qP8k6cjB`4_FXJgr9j<^*gd5SkWC> z8COM)%W(QX{GXi~vbSOs_OZa-qdlIAzas9>pi(4Fv1WG9=O|SM@aWX3vVoVk)4Y;N zk-g>X@CK;xvy-x2b#32d{M*mxt5@Mii9CB}r^yBu^gAQ3wPY3DYVr!%MLh3po#eD9jKsxnYqd-bdnEi*q3{H}d56#NHi#3p+H literal 0 HcmV?d00001 diff --git a/flutter_local_notifications/example/windows/runner/main.cpp b/flutter_local_notifications/example/windows/runner/main.cpp index bcb57b0e2..cb42a21ae 100644 --- a/flutter_local_notifications/example/windows/runner/main.cpp +++ b/flutter_local_notifications/example/windows/runner/main.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "flutter_window.h" #include "utils.h" @@ -17,6 +18,8 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + SetCurrentProcessExplicitAppUserModelID(L"com.dexterous.example"); + flutter::DartProject project(L"data"); std::vector command_line_arguments = diff --git a/flutter_local_notifications/windows/CMakeLists.txt b/flutter_local_notifications/windows/CMakeLists.txt index c604cdd31..8a2a3fcfd 100644 --- a/flutter_local_notifications/windows/CMakeLists.txt +++ b/flutter_local_notifications/windows/CMakeLists.txt @@ -9,7 +9,6 @@ set(PLUGIN_NAME "flutter_local_notifications_plugin") # nuget configuration set(NUGET_PACKAGES_PATH "${CMAKE_BINARY_DIR}/packages") -set(CPPWINRT_VERSION "2.0.220131.2") FetchContent_Declare(nuget URL "https://dist.nuget.org/win-x86-commandline/v6.0.0/nuget.exe" @@ -28,7 +27,7 @@ function(nuget_install pkg ver) ${NUGET} install ${pkg} -Version ${ver} -OutputDirectory ${NUGET_PACKAGES_PATH} RESULT_VARIABLE result) if (NOT result EQUAL 0) - message(FATAL_ERROR "Failed to install nuget package ${pkg}, version ${ver}") + message(FATAL_ERROR "Failed to install nuget package ${pkg}, version ${ver}, ${result}") endif() endfunction() @@ -38,8 +37,10 @@ add_library(${PLUGIN_NAME} SHARED "utils/utils.cpp") # setup c++/winrt -nuget_install("Microsoft.Windows.CppWinRT" ${CPPWINRT_VERSION}) +set(CPPWINRT_VERSION "2.0.220131.2") set(CPPWINRT ${NUGET_PACKAGES_PATH}/Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}/bin/cppwinrt.exe) + +nuget_install("Microsoft.Windows.CppWinRT" ${CPPWINRT_VERSION}) execute_process(COMMAND ${CPPWINRT} -input sdk -output include WORKING_DIRECTORY ${CMAKE_BINARY_DIR} diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 354447339..12216797c 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -83,8 +83,8 @@ namespace { else if (method_name == Method::SHOW) { const auto args = std::get_if(method_call.arguments()); if (args != nullptr) { - const auto& title = Utils::GetString("title", args).value(); - const auto& body = Utils::GetString("body", args).value(); + const auto title = Utils::GetString("title", args).value(); + const auto body = Utils::GetString("body", args).value(); const auto payload = Utils::GetString("payload", args); ShowNotification(title, body, payload); @@ -103,34 +103,21 @@ namespace { const std::string& title, const std::string& body, const std::optional& payload) { - /*XmlDocument doc; - doc.LoadXml(L"\ - \ - \ - \ - \ - \ - \ - \ - "); - - if (payload.has_value()) { - doc.DocumentElement().SetAttribute(L"launch", winrt::to_hstring(payload.value())); - }*/ - //doc.SelectSingleNode(L"//text[1]").InnerText(winrt::to_hstring(title)); - //doc.SelectSingleNode(L"//text[2]").InnerText(winrt::to_hstring(body)); - - const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText01); - - XmlNodeList nodes = doc.GetElementsByTagName(L"text"); + + // obtain a notification template with a title and a body + const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); + // find all tags + const auto nodes = doc.GetElementsByTagName(L"text"); + // change the text of the first nodes.Item(0).AppendChild(doc.CreateTextNode(winrt::to_hstring(title))); + // change the text of the second + nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body))); winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; - const auto notifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(L"example"); + const auto notifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(L"com.dexterous.example"); notifier.Show(notif); } - } // namespace void FlutterLocalNotificationsPluginRegisterWithRegistrar( diff --git a/flutter_local_notifications/windows/utils/utils.cpp b/flutter_local_notifications/windows/utils/utils.cpp index c649f5e35..40a35915d 100644 --- a/flutter_local_notifications/windows/utils/utils.cpp +++ b/flutter_local_notifications/windows/utils/utils.cpp @@ -3,7 +3,7 @@ #include std::optional Utils::GetString(const std::string& key, const flutter::EncodableMap* m) { - const auto pair = m->find(key); + const auto pair = m->find(flutter::EncodableValue(key)); if (pair == m->end()) { return std::nullopt; } From 7b5dfa37695fb3d048312f962dbf48b0ca21440f Mon Sep 17 00:00:00 2001 From: Kenneth Date: Thu, 10 Feb 2022 22:32:55 +0000 Subject: [PATCH 004/112] Implement initialize method --- .../example/lib/main.dart | 12 ++++--- .../lib/flutter_local_notifications.dart | 1 + .../flutter_local_notifications_plugin.dart | 5 +++ .../lib/src/initialization_settings.dart | 5 +++ .../platform_flutter_local_notifications.dart | 33 ++++++++++++++----- .../windows/initialization_settings.dart | 10 ++++++ .../windows/method_channel_mappers.dart | 10 ++++++ .../flutter_local_notifications_plugin.cpp | 28 ++++++++++++---- .../flutter_local_notifications/methods.h | 5 +++ .../windows/methods.cpp | 1 + .../windows/utils/utils.h | 6 ++++ 11 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 2af9972e9..a07902f4b 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -182,11 +182,16 @@ Future main() async { defaultActionName: 'Open notification', defaultIcon: AssetsLinuxIcon('icons/app_icon.png'), ); + const WindowsInitializationSettings initializationSettingsWindows = + WindowsInitializationSettings( + appName: 'Flutter Local Notifications Example', + ); final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS, macOS: initializationSettingsMacOS, linux: initializationSettingsLinux, + windows: initializationSettingsWindows, ); await flutterLocalNotificationsPlugin.initialize( initializationSettings, @@ -733,7 +738,7 @@ class _HomePageState extends State { onPressed: () async { await _stopForegroundService(); }, - ), + ), ], if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) ...[ @@ -2153,8 +2158,6 @@ class _HomePageState extends State { ?.stopForegroundService(); } - - Future _createNotificationChannel() async { const AndroidNotificationChannel androidNotificationChannel = AndroidNotificationChannel( @@ -2417,7 +2420,8 @@ Future _showLinuxNotificationWithByteDataIcon() async { data: iconBytes, width: iconData.width, height: iconData.height, - channels: 4, // The icon has an alpha channel + channels: 4, + // The icon has an alpha channel hasAlpha: true, ), ), diff --git a/flutter_local_notifications/lib/flutter_local_notifications.dart b/flutter_local_notifications/lib/flutter_local_notifications.dart index 92740ee2e..a261b56c6 100644 --- a/flutter_local_notifications/lib/flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/flutter_local_notifications.dart @@ -43,5 +43,6 @@ export 'src/platform_specifics/ios/notification_details.dart'; export 'src/platform_specifics/macos/initialization_settings.dart'; export 'src/platform_specifics/macos/notification_attachment.dart'; export 'src/platform_specifics/macos/notification_details.dart'; +export 'src/platform_specifics/windows/initialization_settings.dart'; export 'src/typedefs.dart'; export 'src/types.dart'; diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index 793b71709..1882c511b 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -150,6 +150,11 @@ class FlutterLocalNotificationsPlugin { LinuxFlutterLocalNotificationsPlugin>() ?.initialize(initializationSettings.linux!, onSelectNotification: onSelectNotification); + } else if (defaultTargetPlatform == TargetPlatform.windows) { + return await resolvePlatformSpecificImplementation< + WindowsFlutterLocalNotificationsPlugin>() + ?.initialize(initializationSettings.windows!, + onSelectNotification: onSelectNotification); } return true; } diff --git a/flutter_local_notifications/lib/src/initialization_settings.dart b/flutter_local_notifications/lib/src/initialization_settings.dart index 801ae6082..0b6565523 100644 --- a/flutter_local_notifications/lib/src/initialization_settings.dart +++ b/flutter_local_notifications/lib/src/initialization_settings.dart @@ -1,5 +1,6 @@ import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart'; +import '../flutter_local_notifications.dart'; import 'platform_specifics/android/initialization_settings.dart'; import 'platform_specifics/ios/initialization_settings.dart'; import 'platform_specifics/macos/initialization_settings.dart'; @@ -12,6 +13,7 @@ class InitializationSettings { this.iOS, this.macOS, this.linux, + this.windows, }); /// Settings for Android. @@ -25,4 +27,7 @@ class InitializationSettings { /// Settings for Linux. final LinuxInitializationSettings? linux; + + /// Settings for Windows. + final WindowsInitializationSettings? windows; } diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 0325df60b..b076f5336 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -25,6 +25,8 @@ 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/windows/initialization_settings.dart'; +import 'platform_specifics/windows/method_channel_mappers.dart'; import 'type_mappers.dart'; import 'typedefs.dart'; import 'types.dart'; @@ -850,17 +852,32 @@ class MacOSFlutterLocalNotificationsPlugin } } +/// Windows implementation of the flutter_local_notifications plugin. class WindowsFlutterLocalNotificationsPlugin extends MethodChannelFlutterLocalNotificationsPlugin { + /// Initializes the plugin. + /// + /// Call this method on application before using the plugin further. + /// This should only be done once. When a notification created by this plugin + /// was used to launch the app, calling [initialize] is what will trigger to + /// the [onSelectNotification] callback to be fire. + /// + /// To handle when a notification launched an application, use + /// [getNotificationAppLaunchDetails]. + Future initialize( + WindowsInitializationSettings settings, { + SelectNotificationCallback? onSelectNotification, + }) => + _channel.invokeMethod('initialize', settings.toMap()); + @override - Future show(int id, String? title, String? body, {String? payload}) { - return _channel.invokeMethod('show', { - 'id': id, - 'title': title, - 'body': body, - 'payload': payload ?? '', - }); - } + Future show(int id, String? title, String? body, {String? payload}) => + _channel.invokeMethod('show', { + 'id': id, + 'title': title, + 'body': body, + 'payload': payload ?? '', + }); } /// Checks [backgroundHandler] method, if not `null`, for eligibility to diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart new file mode 100644 index 000000000..fbbf6b405 --- /dev/null +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart @@ -0,0 +1,10 @@ +/// Plugin initialization settings for Windows. +class WindowsInitializationSettings { + /// Creates a new settings object for initializing this plugin on Windows. + const WindowsInitializationSettings({ + required this.appName, + }); + + /// The name of the app that should be shown in the notification toast. + final String appName; +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart new file mode 100644 index 000000000..87c0b7e27 --- /dev/null +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart @@ -0,0 +1,10 @@ +import 'initialization_settings.dart'; + +/// An extension on [WindowsInitializationSettings] that provides mapping +/// to method channel serializable values. +extension WindowsInitializationSettingsMapper on WindowsInitializationSettings { + /// Maps [WindowsInitializationSettings] to a [Map]. + Map toMap() => { + 'appName': appName, + }; +} diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 12216797c..406fc5c6e 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -33,13 +33,15 @@ namespace { virtual ~FlutterLocalNotificationsPlugin(); private: - winrt::Windows::UI::Notifications::ToastNotificationManager toastManager; + std::optional toastNotifier; // Called when a method is called on this plugin's channel from Dart. void HandleMethodCall( const flutter::MethodCall& method_call, std::unique_ptr> result); + void Initialize(const std::string& appName); + void ShowNotification( const std::string& title, const std::string& body, @@ -62,12 +64,9 @@ namespace { }); registrar->AddPlugin(std::move(plugin)); - - SetCurrentProcessExplicitAppUserModelID(L"Com.Example.Flutter.FlutterLocalNotificationPlugin"); } - FlutterLocalNotificationsPlugin::FlutterLocalNotificationsPlugin() : - toastManager{} {} + FlutterLocalNotificationsPlugin::FlutterLocalNotificationsPlugin() {} FlutterLocalNotificationsPlugin::~FlutterLocalNotificationsPlugin() {} @@ -76,10 +75,23 @@ namespace { std::unique_ptr> result) { std::cout << method_call.method_name() << std::endl; std::cout << Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS << std::endl; + const auto& method_name = method_call.method_name(); if (method_name == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { result->Success(); } + else if (method_name == Method::INITIALIZE) { + const auto args = std::get_if(method_call.arguments()); + if (args != nullptr) { + const auto appName = Utils::GetString("appName", args).value(); + + Initialize(appName); + result->Success(true); + } + else { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + } + } else if (method_name == Method::SHOW) { const auto args = std::get_if(method_call.arguments()); if (args != nullptr) { @@ -99,6 +111,10 @@ namespace { } } + void FlutterLocalNotificationsPlugin::Initialize(const std::string& appName) { + toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(appName)); + } + void FlutterLocalNotificationsPlugin::ShowNotification( const std::string& title, const std::string& body, @@ -114,7 +130,7 @@ namespace { nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body))); winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; - const auto notifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(L"com.dexterous.example"); + const auto notifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(L"Test App Name"); notifier.Show(notif); } diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h index b890c23b9..7bbbb1c38 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h @@ -1,7 +1,12 @@ #include +/// +/// Defines names of methods of this plugin that are callable +/// through Flutter's method channel. +/// namespace Method { + extern const std::string INITIALIZE; extern const std::string GET_NOTIFICATION_APP_LAUNCH_DETAILS; extern const std::string SHOW; } \ No newline at end of file diff --git a/flutter_local_notifications/windows/methods.cpp b/flutter_local_notifications/windows/methods.cpp index ccb4f0533..5763cb3f0 100644 --- a/flutter_local_notifications/windows/methods.cpp +++ b/flutter_local_notifications/windows/methods.cpp @@ -4,3 +4,4 @@ const std::string Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS = "getNotificationAppLaunchDetails"; const std::string Method::SHOW = "show"; +const std::string Method::INITIALIZE = "initialize"; diff --git a/flutter_local_notifications/windows/utils/utils.h b/flutter_local_notifications/windows/utils/utils.h index 81483089c..b3b01ac66 100644 --- a/flutter_local_notifications/windows/utils/utils.h +++ b/flutter_local_notifications/windows/utils/utils.h @@ -5,6 +5,12 @@ #include namespace Utils { + /// + /// Retrieves the string value stored with the given key in the given EncodableMap. + /// + /// The key that maps to the desired string value. + /// The EncodabeMap that stores the key-value pair. + /// The string value that the key maps to, or nullopt if none is found. std::optional GetString(const std::string& key, const flutter::EncodableMap* m); } From 1fd635ed52434d7a6d2df82fe9d8aa0077b50223 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Thu, 10 Feb 2022 23:45:24 +0000 Subject: [PATCH 005/112] WIP impl of cancelling notifications --- .../flutter_local_notifications_plugin.cpp | 41 +++++++++++++++---- .../flutter_local_notifications/methods.h | 1 + .../windows/methods.cpp | 1 + .../windows/utils/utils.cpp | 5 ++- .../windows/utils/utils.h | 3 +- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 406fc5c6e..9a831bdd8 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -33,6 +33,7 @@ namespace { virtual ~FlutterLocalNotificationsPlugin(); private: + std::map activeNotifications; std::optional toastNotifier; // Called when a method is called on this plugin's channel from Dart. @@ -43,9 +44,12 @@ namespace { void Initialize(const std::string& appName); void ShowNotification( + const int id, const std::string& title, const std::string& body, const std::optional& payload); + + void CancelNotification(const int id); }; // static @@ -83,7 +87,7 @@ namespace { else if (method_name == Method::INITIALIZE) { const auto args = std::get_if(method_call.arguments()); if (args != nullptr) { - const auto appName = Utils::GetString("appName", args).value(); + const auto appName = Utils::GetMapValue("appName", args).value(); Initialize(appName); result->Success(true); @@ -94,12 +98,24 @@ namespace { } else if (method_name == Method::SHOW) { const auto args = std::get_if(method_call.arguments()); - if (args != nullptr) { - const auto title = Utils::GetString("title", args).value(); - const auto body = Utils::GetString("body", args).value(); - const auto payload = Utils::GetString("payload", args); + if (args != nullptr && toastNotifier.has_value()) { + const auto id = Utils::GetMapValue("id", args).value(); + const auto title = Utils::GetMapValue("title", args).value(); + const auto body = Utils::GetMapValue("body", args).value(); + const auto payload = Utils::GetMapValue("payload", args); - ShowNotification(title, body, payload); + ShowNotification(id, title, body, payload); + result->Success(); + } + else { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + } + } + else if (method_name == Method::CANCEL && toastNotifier.has_value()) { + const auto args = method_call.arguments(); + if (std::holds_alternative(*args)) { + const auto id = std::get(*args); + CancelNotification(id); result->Success(); } else { @@ -116,6 +132,7 @@ namespace { } void FlutterLocalNotificationsPlugin::ShowNotification( + const int id, const std::string& title, const std::string& body, const std::optional& payload) { @@ -130,9 +147,17 @@ namespace { nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body))); winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; - const auto notifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(L"Test App Name"); - notifier.Show(notif); + toastNotifier.value().Show(notif); + activeNotifications[id] = notif; + } + + void FlutterLocalNotificationsPlugin::CancelNotification(const int id) { + const auto p = activeNotifications.find(id); + if (p != activeNotifications.end()) { + const auto& notif = p->second; + toastNotifier.value().Hide(notif); + } } } // namespace diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h index 7bbbb1c38..f6c4d288b 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h @@ -9,4 +9,5 @@ namespace Method extern const std::string INITIALIZE; extern const std::string GET_NOTIFICATION_APP_LAUNCH_DETAILS; extern const std::string SHOW; + extern const std::string CANCEL; } \ No newline at end of file diff --git a/flutter_local_notifications/windows/methods.cpp b/flutter_local_notifications/windows/methods.cpp index 5763cb3f0..36820194b 100644 --- a/flutter_local_notifications/windows/methods.cpp +++ b/flutter_local_notifications/windows/methods.cpp @@ -5,3 +5,4 @@ const std::string Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS = "getNotificationAppLaunchDetails"; const std::string Method::SHOW = "show"; const std::string Method::INITIALIZE = "initialize"; +const std::string Method::CANCEL = "cancel"; diff --git a/flutter_local_notifications/windows/utils/utils.cpp b/flutter_local_notifications/windows/utils/utils.cpp index 40a35915d..e1780bdc3 100644 --- a/flutter_local_notifications/windows/utils/utils.cpp +++ b/flutter_local_notifications/windows/utils/utils.cpp @@ -2,11 +2,12 @@ #include -std::optional Utils::GetString(const std::string& key, const flutter::EncodableMap* m) { +template +std::optional Utils::GetMapValue(const std::string& key, const flutter::EncodableMap* m) { const auto pair = m->find(flutter::EncodableValue(key)); if (pair == m->end()) { return std::nullopt; } - const auto& str = std::get(pair->second); + const auto& str = std::get(pair->second); return str; } diff --git a/flutter_local_notifications/windows/utils/utils.h b/flutter_local_notifications/windows/utils/utils.h index b3b01ac66..190572778 100644 --- a/flutter_local_notifications/windows/utils/utils.h +++ b/flutter_local_notifications/windows/utils/utils.h @@ -11,7 +11,8 @@ namespace Utils { /// The key that maps to the desired string value. /// The EncodabeMap that stores the key-value pair. /// The string value that the key maps to, or nullopt if none is found. - std::optional GetString(const std::string& key, const flutter::EncodableMap* m); + template + std::optional GetMapValue(const std::string& key, const flutter::EncodableMap* m); } #endif // !UTILS_H From 1fad545f2c01cf103204e6dcdc0a5ad739019597 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Fri, 11 Feb 2022 21:49:21 +0000 Subject: [PATCH 006/112] Add support for optional title/body --- .../windows/CMakeLists.txt | 2 +- .../flutter_local_notifications_plugin.cpp | 31 +++++++++++-------- .../windows/utils/utils.cpp | 13 -------- .../windows/utils/utils.h | 12 ++++++- 4 files changed, 30 insertions(+), 28 deletions(-) delete mode 100644 flutter_local_notifications/windows/utils/utils.cpp diff --git a/flutter_local_notifications/windows/CMakeLists.txt b/flutter_local_notifications/windows/CMakeLists.txt index 8a2a3fcfd..ae8838cac 100644 --- a/flutter_local_notifications/windows/CMakeLists.txt +++ b/flutter_local_notifications/windows/CMakeLists.txt @@ -34,7 +34,7 @@ endfunction() add_library(${PLUGIN_NAME} SHARED "flutter_local_notifications_plugin.cpp" "methods.cpp" - "utils/utils.cpp") + "utils/utils.h") # setup c++/winrt set(CPPWINRT_VERSION "2.0.220131.2") diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 9a831bdd8..8c8e4d0a7 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -33,7 +33,7 @@ namespace { virtual ~FlutterLocalNotificationsPlugin(); private: - std::map activeNotifications; + std::map activeNotifications; std::optional toastNotifier; // Called when a method is called on this plugin's channel from Dart. @@ -45,8 +45,8 @@ namespace { void ShowNotification( const int id, - const std::string& title, - const std::string& body, + const std::optional& title, + const std::optional& body, const std::optional& payload); void CancelNotification(const int id); @@ -100,8 +100,8 @@ namespace { const auto args = std::get_if(method_call.arguments()); if (args != nullptr && toastNotifier.has_value()) { const auto id = Utils::GetMapValue("id", args).value(); - const auto title = Utils::GetMapValue("title", args).value(); - const auto body = Utils::GetMapValue("body", args).value(); + const auto title = Utils::GetMapValue("title", args); + const auto body = Utils::GetMapValue("body", args); const auto payload = Utils::GetMapValue("payload", args); ShowNotification(id, title, body, payload); @@ -133,30 +133,35 @@ namespace { void FlutterLocalNotificationsPlugin::ShowNotification( const int id, - const std::string& title, - const std::string& body, + const std::optional& title, + const std::optional& body, const std::optional& payload) { // obtain a notification template with a title and a body const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); // find all tags const auto nodes = doc.GetElementsByTagName(L"text"); - // change the text of the first - nodes.Item(0).AppendChild(doc.CreateTextNode(winrt::to_hstring(title))); - // change the text of the second - nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body))); + if (title.has_value()) { + // change the text of the first , which will be the title + nodes.Item(0).AppendChild(doc.CreateTextNode(winrt::to_hstring(title.value()))); + } + if (body.has_value()) { + // change the text of the second , which will be the body + nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body.value()))); + } + winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; toastNotifier.value().Show(notif); - activeNotifications[id] = notif; + activeNotifications[id] = ¬if; } void FlutterLocalNotificationsPlugin::CancelNotification(const int id) { const auto p = activeNotifications.find(id); if (p != activeNotifications.end()) { const auto& notif = p->second; - toastNotifier.value().Hide(notif); + toastNotifier.value().Hide(*notif); } } } // namespace diff --git a/flutter_local_notifications/windows/utils/utils.cpp b/flutter_local_notifications/windows/utils/utils.cpp deleted file mode 100644 index e1780bdc3..000000000 --- a/flutter_local_notifications/windows/utils/utils.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "utils.h" - -#include - -template -std::optional Utils::GetMapValue(const std::string& key, const flutter::EncodableMap* m) { - const auto pair = m->find(flutter::EncodableValue(key)); - if (pair == m->end()) { - return std::nullopt; - } - const auto& str = std::get(pair->second); - return str; -} diff --git a/flutter_local_notifications/windows/utils/utils.h b/flutter_local_notifications/windows/utils/utils.h index 190572778..08766348c 100644 --- a/flutter_local_notifications/windows/utils/utils.h +++ b/flutter_local_notifications/windows/utils/utils.h @@ -12,7 +12,17 @@ namespace Utils { /// The EncodabeMap that stores the key-value pair. /// The string value that the key maps to, or nullopt if none is found. template - std::optional GetMapValue(const std::string& key, const flutter::EncodableMap* m); + std::optional GetMapValue(const std::string& key, const flutter::EncodableMap* m) { + const auto pair = m->find(flutter::EncodableValue(key)); + if (pair == m->end()) { + return std::nullopt; + } + const auto val = pair->second; + if (std::holds_alternative(val)) { + return std::get(val); + } + return std::nullopt; + } } #endif // !UTILS_H From 8c53f574ea820122d5aca0f66457ac11530e75e7 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Sun, 13 Feb 2022 23:42:28 +0000 Subject: [PATCH 007/112] Impl Registry logic for notification handling --- .../example/lib/main.dart | 1 + .../example/windows/runner/main.cpp | 2 - .../windows/initialization_settings.dart | 16 ++ .../windows/method_channel_mappers.dart | 3 + .../windows/CMakeLists.txt | 3 +- .../flutter_local_notifications_plugin.cpp | 69 ++++- .../flutter_local_notifications/methods.h | 1 + .../windows/methods.cpp | 1 + .../windows/registration.cpp | 236 ++++++++++++++++++ .../windows/registration.h | 22 ++ 10 files changed, 339 insertions(+), 15 deletions(-) create mode 100644 flutter_local_notifications/windows/registration.cpp create mode 100644 flutter_local_notifications/windows/registration.h diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index a07902f4b..bbe5483c5 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -185,6 +185,7 @@ Future main() async { const WindowsInitializationSettings initializationSettingsWindows = WindowsInitializationSettings( appName: 'Flutter Local Notifications Example', + appUserModelId: 'Com.Dexterous.FlutterLocalNotificationsExample', ); final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, diff --git a/flutter_local_notifications/example/windows/runner/main.cpp b/flutter_local_notifications/example/windows/runner/main.cpp index cb42a21ae..87602243e 100644 --- a/flutter_local_notifications/example/windows/runner/main.cpp +++ b/flutter_local_notifications/example/windows/runner/main.cpp @@ -18,8 +18,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - SetCurrentProcessExplicitAppUserModelID(L"com.dexterous.example"); - flutter::DartProject project(L"data"); std::vector command_line_arguments = diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart index fbbf6b405..a4036e483 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart @@ -1,10 +1,26 @@ +import 'package:flutter/widgets.dart'; + /// Plugin initialization settings for Windows. class WindowsInitializationSettings { /// Creates a new settings object for initializing this plugin on Windows. const WindowsInitializationSettings({ required this.appName, + required this.appUserModelId, + this.iconPath, + this.iconBackgroundColor, }); /// The name of the app that should be shown in the notification toast. final String appName; + + /// The unique app user model ID that identifies the app, + /// in the form of CompanyName.ProductName.SubProduct.VersionInformation. + /// + /// See https://docs.microsoft.com/en-us/windows/win32/shell/appids + /// for more information. + final String appUserModelId; + + final String? iconPath; + + final Color? iconBackgroundColor; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart index 87c0b7e27..01c577cc0 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart @@ -6,5 +6,8 @@ extension WindowsInitializationSettingsMapper on WindowsInitializationSettings { /// Maps [WindowsInitializationSettings] to a [Map]. Map toMap() => { 'appName': appName, + 'aumid': appUserModelId, + 'iconPath': iconPath, + 'iconBgColor': iconBackgroundColor, }; } diff --git a/flutter_local_notifications/windows/CMakeLists.txt b/flutter_local_notifications/windows/CMakeLists.txt index ae8838cac..16807d156 100644 --- a/flutter_local_notifications/windows/CMakeLists.txt +++ b/flutter_local_notifications/windows/CMakeLists.txt @@ -34,7 +34,8 @@ endfunction() add_library(${PLUGIN_NAME} SHARED "flutter_local_notifications_plugin.cpp" "methods.cpp" - "utils/utils.h") + "utils/utils.h" + "registration.cpp") # setup c++/winrt set(CPPWINRT_VERSION "2.0.220131.2") diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 8c8e4d0a7..ee440d911 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -1,10 +1,12 @@ #include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" #include "include/flutter_local_notifications/methods.h" #include "utils/utils.h" +#include "registration.h" // This must be included before many other Windows headers. #include #include +#include #include #include #include @@ -33,23 +35,47 @@ namespace { virtual ~FlutterLocalNotificationsPlugin(); private: - std::map activeNotifications; std::optional toastNotifier; + std::optional toastNotificationHistory; // Called when a method is called on this plugin's channel from Dart. void HandleMethodCall( const flutter::MethodCall& method_call, std::unique_ptr> result); - void Initialize(const std::string& appName); - + /// + /// Initializes this plugin. + /// + /// The display name of this app that should be shown in the notification toast. + void Initialize( + const std::string& appName, + const std::string& aumid, + const std::optional& iconPath, + const std::optional& iconBgColor); + + /// + /// Displays a single notification toast. + /// + /// A unique ID that identifies this notification. It can be used to cancel/dismiss the notification. + /// An optional title of the notification. + /// An optional body of the notification. + /// void ShowNotification( const int id, const std::optional& title, const std::optional& body, const std::optional& payload); + /// + /// Dismisses the notification that has the given ID. + /// + /// The ID of the notification to be dismissed. void CancelNotification(const int id); + + /// + /// Dismisses all currently active notifications. + /// + void CancelAllNotifications(); }; // static @@ -78,7 +104,6 @@ namespace { const flutter::MethodCall& method_call, std::unique_ptr> result) { std::cout << method_call.method_name() << std::endl; - std::cout << Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS << std::endl; const auto& method_name = method_call.method_name(); if (method_name == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { @@ -88,8 +113,11 @@ namespace { const auto args = std::get_if(method_call.arguments()); if (args != nullptr) { const auto appName = Utils::GetMapValue("appName", args).value(); + const auto aumid = Utils::GetMapValue("aumid", args).value(); + const auto iconPath = Utils::GetMapValue("iconPath", args); + const auto iconBgColor = Utils::GetMapValue("iconBgColor", args); - Initialize(appName); + Initialize(appName, aumid, iconPath, iconBgColor); result->Success(true); } else { @@ -122,13 +150,23 @@ namespace { result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); } } + else if (method_name == Method::CANCEL_ALL && toastNotifier.has_value()) { + CancelAllNotifications(); + result->Success(); + } else { result->NotImplemented(); } } - void FlutterLocalNotificationsPlugin::Initialize(const std::string& appName) { - toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(appName)); + void FlutterLocalNotificationsPlugin::Initialize( + const std::string& appName, + const std::string& aumid, + const std::optional& iconPath, + const std::optional& iconBgColor) { + std::cout << "Initialize" << std::endl; + PluginRegistration::RegisterApp(aumid, appName, iconPath, iconBgColor); + toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); } void FlutterLocalNotificationsPlugin::ShowNotification( @@ -152,17 +190,24 @@ namespace { } winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; + notif.Tag(winrt::to_hstring(id)); toastNotifier.value().Show(notif); - activeNotifications[id] = ¬if; } void FlutterLocalNotificationsPlugin::CancelNotification(const int id) { - const auto p = activeNotifications.find(id); - if (p != activeNotifications.end()) { - const auto& notif = p->second; - toastNotifier.value().Hide(*notif); + if (!toastNotificationHistory.has_value()) { + toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); + } + toastNotificationHistory.value().Remove(winrt::to_hstring(id)); + } + + void FlutterLocalNotificationsPlugin::CancelAllNotifications() { + + if (!toastNotificationHistory.has_value()) { + toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); } + toastNotificationHistory.value().Clear(); } } // namespace diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h index f6c4d288b..ed60d8039 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h @@ -10,4 +10,5 @@ namespace Method extern const std::string GET_NOTIFICATION_APP_LAUNCH_DETAILS; extern const std::string SHOW; extern const std::string CANCEL; + extern const std::string CANCEL_ALL; } \ No newline at end of file diff --git a/flutter_local_notifications/windows/methods.cpp b/flutter_local_notifications/windows/methods.cpp index 36820194b..b4a9f5064 100644 --- a/flutter_local_notifications/windows/methods.cpp +++ b/flutter_local_notifications/windows/methods.cpp @@ -6,3 +6,4 @@ const std::string Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS = "getNotification const std::string Method::SHOW = "show"; const std::string Method::INITIALIZE = "initialize"; const std::string Method::CANCEL = "cancel"; +const std::string Method::CANCEL_ALL = "cancelAll"; diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp new file mode 100644 index 000000000..bd704f190 --- /dev/null +++ b/flutter_local_notifications/windows/registration.cpp @@ -0,0 +1,236 @@ +// Huge credit to these StackOverflow answers: +// https://stackoverflow.com/questions/51947833/activation-from-c-winrt-dll +// https://stackoverflow.com/questions/67005337/how-works-notifications-on-windows-registry-no-shortlink + +#include "registration.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/// +/// The GUID that identifies the notification activation callback. +/// 68d0c89d-760f-4f79-a067-ae8d4220ccc1 +/// +static constexpr winrt::guid CALLBACK_GUID{ + 0x68d0c89d, 0x760f, 0x4f79, {0xa0, 0x67, 0xae, 0x8d, 0x42, 0x20, 0xcc, 0xc1} +}; + +const std::string CALLBACK_GUID_STR = "{68d0c89d-760f-4f79-a067-ae8d4220ccc1}"; + +/// +/// This callback will be called when a notification sent by this plugin is clicked on. +/// +struct NotificationActivationCallback : winrt::implements +{ + HRESULT __stdcall Activate( + LPCWSTR app, + LPCWSTR args, + [[maybe_unused]] NOTIFICATION_USER_INPUT_DATA const* data, + [[maybe_unused]] ULONG count) noexcept final + { + try { + std::wcout << L"Example" << L" has been called back from a notification." << std::endl; + std::wcout << L"Value of the 'app' parameter is '" << app << L"'." << std::endl; + std::wcout << L"Value of the 'args' parameter is '" << args << L"'." << std::endl; + return S_OK; + } + catch (...) { + return winrt::to_hresult(); + } + } +}; + +/// +/// A class factory that creates an instance of NotificationActivationCallback. +/// +struct NotificationActivationCallbackFactory : winrt::implements +{ + HRESULT __stdcall CreateInstance( + IUnknown* outer, + GUID const& iid, + void** result) noexcept final + { + *result = nullptr; + + if (outer) { + return CLASS_E_NOAGGREGATION; + } + + return winrt::make()->QueryInterface(iid, result); + } + + HRESULT __stdcall LockServer(BOOL) noexcept final { + return S_OK; + } +}; + +struct RegistryHandle +{ + using type = HKEY; + + static void close(type value) noexcept { + WINRT_VERIFY_(ERROR_SUCCESS, RegCloseKey(value)); + } + + static constexpr type invalid() noexcept { + return nullptr; + } +}; + +/// +/// A handle to a registry key. +/// +using RegistryKey = winrt::handle_type; + +/// +/// Updates the Registry to enable notifications. +/// +/// The app user model ID of the app. Provided during initialization of the plugin. +/// The display name of the app. The name will be shown on the notification toasts. +/// An optional path to the icon of the app. The icon will be shown on the notification toasts +/// An optional string that specifies the background color of the icon, in the format of AARRGGBB. +void UpdateRegistry( + const std::string& aumid, + const std::string& appName, + const std::optional& iconPath, + const std::optional& iconBgColor +) { + std::cout << "Update registry" << std::endl; + + std::stringstream ss; + ss << "Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications\\Backup\\" << aumid; + const auto key_path = ss.str(); + RegistryKey key; + + // create registry key + winrt::check_win32(RegCreateKeyExA( + HKEY_CURRENT_USER, + key_path.c_str(), + 0, + nullptr, + 0, + KEY_WRITE, + nullptr, + key.put(), + nullptr)); + + // put the following key values under the key + // appType = app:desktop + // Setting = s:banner,s:toast,s:audio,c:toast,c:ringing + // wnsId = NonImmersivePackage + const std::string appType = "app:desktop"; + const std::string setting = "s:banner,s:toast,s:audio,c:toast,c:ringing"; + const std::string wnsId = "NonImmersivePackage"; + winrt::check_win32(RegSetValueExA( + key.get(), + "appType", + 0, + REG_SZ, + reinterpret_cast(appType.c_str()), + static_cast(appType.size() + 1 * sizeof(char)))); + + winrt::check_win32(RegSetValueExA( + key.get(), + "Setting", + 0, + REG_SZ, + reinterpret_cast(setting.c_str()), + static_cast(setting.size() + 1 * sizeof(char)))); + + winrt::check_win32(RegSetValueExA( + key.get(), + "wnsId", + 0, + REG_SZ, + reinterpret_cast(wnsId.c_str()), + static_cast(wnsId.size() + 1 * sizeof(char)))); + + ss.clear(); + ss.str(std::string()); + ss << "Software\\Classes\\AppUserModelId\\" << aumid; + const auto appInfoKeyPath = ss.str(); + std::cout << "aumid " << appInfoKeyPath << std::endl; + RegistryKey appInfoKey; + + // create registry key + winrt::check_win32(RegCreateKeyExA( + HKEY_CURRENT_USER, + appInfoKeyPath.c_str(), + 0, + nullptr, + 0, + KEY_WRITE, + nullptr, + appInfoKey.put(), + nullptr)); + + winrt::check_win32(RegSetValueExA( + appInfoKey.get(), + "DisplayName", + 0, + REG_SZ, + reinterpret_cast(appName.c_str()), + static_cast(appName.size() + 1 * sizeof(char)))); + + if (iconPath.has_value()) { + const auto v = iconPath.value(); + winrt::check_win32(RegSetValueExA( + appInfoKey.get(), + "IconUri", + 0, + REG_SZ, + reinterpret_cast(v.c_str()), + static_cast(v.size() + 1 * sizeof(char)))); + } + + if (iconBgColor.has_value()) { + const auto v = iconBgColor.value(); + winrt::check_win32(RegSetValueExA( + appInfoKey.get(), + "IconBackgroundColor", + 0, + REG_SZ, + reinterpret_cast(v.c_str()), + static_cast(v.size() + 1 * sizeof(char)))); + } + + winrt::check_win32(RegSetValueExA( + appInfoKey.get(), + "CustomActivator", + 0, + REG_SZ, + reinterpret_cast(CALLBACK_GUID_STR.c_str()), + static_cast(CALLBACK_GUID_STR.size() + 1 * sizeof(char)))); +} + +void RegisterCallback() { + DWORD registration{}; + + winrt::check_hresult(CoRegisterClassObject( + CALLBACK_GUID, + winrt::make().get(), + CLSCTX_LOCAL_SERVER, + REGCLS_SINGLEUSE, + ®istration)); +} + +void PluginRegistration::RegisterApp( + const std::string& aumid, + const std::string& appName, + const std::optional& iconPath, + const std::optional& iconBgColor +) { + std::cout << "register app" << std::endl; + UpdateRegistry(aumid, appName, iconPath, iconBgColor); + RegisterCallback(); +} diff --git a/flutter_local_notifications/windows/registration.h b/flutter_local_notifications/windows/registration.h new file mode 100644 index 000000000..c5a99b1e4 --- /dev/null +++ b/flutter_local_notifications/windows/registration.h @@ -0,0 +1,22 @@ +#ifndef PLUGIN_REGISTRATRION_H_ +#define PLUGIN_REGISTRATRION_H_ + +#include +#include + +namespace PluginRegistration { + /// + /// Registers the running app to the Windows Registry. + /// + /// The app user model ID that identifies the app. + /// The display name of the app. + /// An optional path to the icon of the app. + /// An optional background color of the icon, in AARRGGBB format. + void RegisterApp( + const std::string& aumid, + const std::string& appName, + const std::optional& iconPath, + const std::optional& iconBgColor); +} + +#endif // !PLUGIN_REGISTRATRION_H_ From b8beedd9ca4e58776ad9805f6df74fe27143fdee Mon Sep 17 00:00:00 2001 From: Kenneth Date: Sun, 13 Feb 2022 23:54:43 +0000 Subject: [PATCH 008/112] Tidy up code --- .../flutter_local_notifications_plugin.cpp | 21 +++++++------- .../windows/registration.cpp | 29 ++++++++++++++----- .../windows/registration.h | 3 ++ 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index ee440d911..917ed5345 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -46,7 +46,10 @@ namespace { /// /// Initializes this plugin. /// - /// The display name of this app that should be shown in the notification toast. + /// The app user model ID that identifies the app. + /// The display name of the app. + /// An optional path to the icon of the app. + /// An optional background color of the icon, in AARRGGBB format. void Initialize( const std::string& appName, const std::string& aumid, @@ -102,9 +105,8 @@ namespace { void FlutterLocalNotificationsPlugin::HandleMethodCall( const flutter::MethodCall& method_call, - std::unique_ptr> result) { - std::cout << method_call.method_name() << std::endl; - + std::unique_ptr> result + ) { const auto& method_name = method_call.method_name(); if (method_name == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { result->Success(); @@ -163,8 +165,8 @@ namespace { const std::string& appName, const std::string& aumid, const std::optional& iconPath, - const std::optional& iconBgColor) { - std::cout << "Initialize" << std::endl; + const std::optional& iconBgColor + ) { PluginRegistration::RegisterApp(aumid, appName, iconPath, iconBgColor); toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); } @@ -173,8 +175,8 @@ namespace { const int id, const std::optional& title, const std::optional& body, - const std::optional& payload) { - + const std::optional& payload + ) { // obtain a notification template with a title and a body const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); // find all tags @@ -188,7 +190,7 @@ namespace { // change the text of the second , which will be the body nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body.value()))); } - + winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; notif.Tag(winrt::to_hstring(id)); @@ -203,7 +205,6 @@ namespace { } void FlutterLocalNotificationsPlugin::CancelAllNotifications() { - if (!toastNotificationHistory.has_value()) { toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); } diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index bd704f190..9c9ee87f9 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -25,6 +25,9 @@ static constexpr winrt::guid CALLBACK_GUID{ 0x68d0c89d, 0x760f, 0x4f79, {0xa0, 0x67, 0xae, 0x8d, 0x42, 0x20, 0xcc, 0xc1} }; +/// +/// String representation of the callback GUID. +/// const std::string CALLBACK_GUID_STR = "{68d0c89d-760f-4f79-a067-ae8d4220ccc1}"; /// @@ -94,6 +97,11 @@ using RegistryKey = winrt::handle_type; /// /// Updates the Registry to enable notifications. +/// +/// Related resources: +///
    +///
  • https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast-other-apps
  • +///
///
/// The app user model ID of the app. Provided during initialization of the plugin. /// The display name of the app. The name will be shown on the notification toasts. @@ -105,17 +113,16 @@ void UpdateRegistry( const std::optional& iconPath, const std::optional& iconBgColor ) { - std::cout << "Update registry" << std::endl; - std::stringstream ss; ss << "Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications\\Backup\\" << aumid; - const auto key_path = ss.str(); + const auto notifSettingsKeyPath = ss.str(); RegistryKey key; // create registry key + // HKEY_CURRENT_USER\Software\Microsoft\\Windows\CurrentVersion\PushNotifications\Backup winrt::check_win32(RegCreateKeyExA( HKEY_CURRENT_USER, - key_path.c_str(), + notifSettingsKeyPath.c_str(), 0, nullptr, 0, @@ -125,9 +132,12 @@ void UpdateRegistry( nullptr)); // put the following key values under the key + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\PushNotifications\Backup\ + // // appType = app:desktop // Setting = s:banner,s:toast,s:audio,c:toast,c:ringing // wnsId = NonImmersivePackage + const std::string appType = "app:desktop"; const std::string setting = "s:banner,s:toast,s:audio,c:toast,c:ringing"; const std::string wnsId = "NonImmersivePackage"; @@ -138,7 +148,6 @@ void UpdateRegistry( REG_SZ, reinterpret_cast(appType.c_str()), static_cast(appType.size() + 1 * sizeof(char)))); - winrt::check_win32(RegSetValueExA( key.get(), "Setting", @@ -146,7 +155,6 @@ void UpdateRegistry( REG_SZ, reinterpret_cast(setting.c_str()), static_cast(setting.size() + 1 * sizeof(char)))); - winrt::check_win32(RegSetValueExA( key.get(), "wnsId", @@ -155,14 +163,16 @@ void UpdateRegistry( reinterpret_cast(wnsId.c_str()), static_cast(wnsId.size() + 1 * sizeof(char)))); + // now, we register app info to the Registry. + ss.clear(); ss.str(std::string()); ss << "Software\\Classes\\AppUserModelId\\" << aumid; const auto appInfoKeyPath = ss.str(); - std::cout << "aumid " << appInfoKeyPath << std::endl; RegistryKey appInfoKey; // create registry key + // HKEY_CURRENT_USER\Software\Classes\AppUserModelId\ winrt::check_win32(RegCreateKeyExA( HKEY_CURRENT_USER, appInfoKeyPath.c_str(), @@ -204,6 +214,7 @@ void UpdateRegistry( static_cast(v.size() + 1 * sizeof(char)))); } + // register the guid of the notification activation callback winrt::check_win32(RegSetValueExA( appInfoKey.get(), "CustomActivator", @@ -213,6 +224,10 @@ void UpdateRegistry( static_cast(CALLBACK_GUID_STR.size() + 1 * sizeof(char)))); } +/// +/// Register the notificatio activation callback factory +/// and the guid of the callback. +/// void RegisterCallback() { DWORD registration{}; diff --git a/flutter_local_notifications/windows/registration.h b/flutter_local_notifications/windows/registration.h index c5a99b1e4..c9127e7c8 100644 --- a/flutter_local_notifications/windows/registration.h +++ b/flutter_local_notifications/windows/registration.h @@ -4,6 +4,9 @@ #include #include +/// +/// Contains logic for handling Registry values. +/// namespace PluginRegistration { /// /// Registers the running app to the Windows Registry. From 34e187cfca039604ca66c68058846f277c4375c6 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Mon, 14 Feb 2022 00:06:25 +0000 Subject: [PATCH 009/112] Remove dependency overrides --- flutter_local_notifications/example/pubspec.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/flutter_local_notifications/example/pubspec.yaml b/flutter_local_notifications/example/pubspec.yaml index 07f4f5ad9..562f115b3 100644 --- a/flutter_local_notifications/example/pubspec.yaml +++ b/flutter_local_notifications/example/pubspec.yaml @@ -26,12 +26,6 @@ dev_dependencies: sdk: flutter msix: ^2.8.17 -dependency_overrides: - flutter_local_notifications_platform_interface: - path: ../../flutter_local_notifications_platform_interface/ - flutter_local_notifications_linux: - path: ../../flutter_local_notifications_linux/ - flutter: uses-material-design: true assets: From 5400c0bcaccc00b7f01caf77d1a0e45fa4c23721 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Mon, 14 Feb 2022 11:39:48 +0000 Subject: [PATCH 010/112] Implement cancel/cancelAll --- .../platform_flutter_local_notifications.dart | 18 +++++++- .../flutter_local_notifications_plugin.cpp | 42 ++++++++++++++----- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index b076f5336..be526da02 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -871,13 +871,27 @@ class WindowsFlutterLocalNotificationsPlugin _channel.invokeMethod('initialize', settings.toMap()); @override - Future show(int id, String? title, String? body, {String? payload}) => - _channel.invokeMethod('show', { + Future show( + int id, + String? title, + String? body, { + String? payload, + String? group, + }) => + _channel.invokeMethod('show', { 'id': id, 'title': title, 'body': body, + 'group': group, 'payload': payload ?? '', }); + + @override + Future cancel(int id, {String? group}) => + _channel.invokeMethod('cancel', { + 'id': id, + 'group': group, + }); } /// Checks [backgroundHandler] method, if not `null`, for eligibility to diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 917ed5345..012a443d6 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -35,6 +35,7 @@ namespace { virtual ~FlutterLocalNotificationsPlugin(); private: + std::wstring _aumid; std::optional toastNotifier; std::optional toastNotificationHistory; @@ -67,13 +68,15 @@ namespace { const int id, const std::optional& title, const std::optional& body, - const std::optional& payload); + const std::optional& payload, + const std::optional& group); /// /// Dismisses the notification that has the given ID. /// /// The ID of the notification to be dismissed. - void CancelNotification(const int id); + /// The group the notification is in. Default is the aumid of this app. + void CancelNotification(const int id, const std::optional& group); /// /// Dismisses all currently active notifications. @@ -133,8 +136,9 @@ namespace { const auto title = Utils::GetMapValue("title", args); const auto body = Utils::GetMapValue("body", args); const auto payload = Utils::GetMapValue("payload", args); + const auto group = Utils::GetMapValue("group", args); - ShowNotification(id, title, body, payload); + ShowNotification(id, title, body, payload, group); result->Success(); } else { @@ -142,10 +146,12 @@ namespace { } } else if (method_name == Method::CANCEL && toastNotifier.has_value()) { - const auto args = method_call.arguments(); - if (std::holds_alternative(*args)) { - const auto id = std::get(*args); - CancelNotification(id); + const auto args = std::get_if(method_call.arguments()); + if (args != nullptr) { + const auto id = Utils::GetMapValue("id", args).value(); + const auto group = Utils::GetMapValue("group", args); + + CancelNotification(id, group); result->Success(); } else { @@ -167,6 +173,7 @@ namespace { const std::optional& iconPath, const std::optional& iconBgColor ) { + _aumid = winrt::to_hstring(aumid); PluginRegistration::RegisterApp(aumid, appName, iconPath, iconBgColor); toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); } @@ -175,7 +182,8 @@ namespace { const int id, const std::optional& title, const std::optional& body, - const std::optional& payload + const std::optional& payload, + const std::optional& group ) { // obtain a notification template with a title and a body const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); @@ -193,22 +201,34 @@ namespace { winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; notif.Tag(winrt::to_hstring(id)); + if (group.has_value()) { + notif.Group(winrt::to_hstring(group.value())); + } + else { + notif.Group(_aumid); + } toastNotifier.value().Show(notif); } - void FlutterLocalNotificationsPlugin::CancelNotification(const int id) { + void FlutterLocalNotificationsPlugin::CancelNotification(const int id, const std::optional& group) { if (!toastNotificationHistory.has_value()) { toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); } - toastNotificationHistory.value().Remove(winrt::to_hstring(id)); + + if (group.has_value()) { + toastNotificationHistory.value().Remove(winrt::to_hstring(id), winrt::to_hstring(group.value()), _aumid); + } + else { + toastNotificationHistory.value().Remove(winrt::to_hstring(id), _aumid, _aumid); + } } void FlutterLocalNotificationsPlugin::CancelAllNotifications() { if (!toastNotificationHistory.has_value()) { toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); } - toastNotificationHistory.value().Clear(); + toastNotificationHistory.value().Clear(_aumid); } } // namespace From 0c21c9d57d4f4966a8eaba71943f49d1adebed1d Mon Sep 17 00:00:00 2001 From: Kenneth Date: Sat, 5 Mar 2022 14:20:34 +0000 Subject: [PATCH 011/112] WIP: Impl onSelectNotification --- .../platform_flutter_local_notifications.dart | 19 +- .../flutter_local_notifications_plugin.cpp | 316 ++++++++---------- .../flutter_local_notifications_plugin.h | 77 +++++ .../flutter_local_notifications/methods.h | 3 +- .../windows/methods.cpp | 1 + .../windows/registration.cpp | 35 +- .../windows/registration.h | 6 +- 7 files changed, 263 insertions(+), 194 deletions(-) diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index be526da02..a73eb0b1e 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -855,6 +855,8 @@ class MacOSFlutterLocalNotificationsPlugin /// Windows implementation of the flutter_local_notifications plugin. class WindowsFlutterLocalNotificationsPlugin extends MethodChannelFlutterLocalNotificationsPlugin { + SelectNotificationCallback? onSelectNotification; + /// Initializes the plugin. /// /// Call this method on application before using the plugin further. @@ -867,8 +869,12 @@ class WindowsFlutterLocalNotificationsPlugin Future initialize( WindowsInitializationSettings settings, { SelectNotificationCallback? onSelectNotification, - }) => - _channel.invokeMethod('initialize', settings.toMap()); + }) { + this.onSelectNotification = onSelectNotification; + _channel.setMethodCallHandler(_onMethodCallFromNative); + + return _channel.invokeMethod('initialize', settings.toMap()); + } @override Future show( @@ -892,6 +898,15 @@ class WindowsFlutterLocalNotificationsPlugin 'id': id, 'group': group, }); + + Future _onMethodCallFromNative(MethodCall call) async { + print('call $call'); + switch (call.method) { + case 'selectNotification': + print('notification selected'); + break; + } + } } /// Checks [backgroundHandler] method, if not `null`, for eligibility to diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 012a443d6..51456dfc4 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -24,213 +24,159 @@ using namespace winrt::Windows::Data::Xml::Dom; -namespace { - - class FlutterLocalNotificationsPlugin : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); - - FlutterLocalNotificationsPlugin(); - - virtual ~FlutterLocalNotificationsPlugin(); - - private: - std::wstring _aumid; - std::optional toastNotifier; - std::optional toastNotificationHistory; - - // Called when a method is called on this plugin's channel from Dart. - void HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - - /// - /// Initializes this plugin. - /// - /// The app user model ID that identifies the app. - /// The display name of the app. - /// An optional path to the icon of the app. - /// An optional background color of the icon, in AARRGGBB format. - void Initialize( - const std::string& appName, - const std::string& aumid, - const std::optional& iconPath, - const std::optional& iconBgColor); - - /// - /// Displays a single notification toast. - /// - /// A unique ID that identifies this notification. It can be used to cancel/dismiss the notification. - /// An optional title of the notification. - /// An optional body of the notification. - /// - void ShowNotification( - const int id, - const std::optional& title, - const std::optional& body, - const std::optional& payload, - const std::optional& group); - - /// - /// Dismisses the notification that has the given ID. - /// - /// The ID of the notification to be dismissed. - /// The group the notification is in. Default is the aumid of this app. - void CancelNotification(const int id, const std::optional& group); - - /// - /// Dismisses all currently active notifications. - /// - void CancelAllNotifications(); - }; - - // static - void FlutterLocalNotificationsPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarWindows* registrar) { - auto channel = - std::make_unique>( - registrar->messenger(), "dexterous.com/flutter/local_notifications", - &flutter::StandardMethodCodec::GetInstance()); - - auto plugin = std::make_unique(); - - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto& call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - - registrar->AddPlugin(std::move(plugin)); - } +// static +void FlutterLocalNotificationsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows* registrar) { + auto channel = + std::make_unique( + registrar->messenger(), "dexterous.com/flutter/local_notifications", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(*channel); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto& call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); +} - FlutterLocalNotificationsPlugin::FlutterLocalNotificationsPlugin() {} +FlutterLocalNotificationsPlugin::FlutterLocalNotificationsPlugin(PluginMethodChannel& channel) : + channel(channel) {} - FlutterLocalNotificationsPlugin::~FlutterLocalNotificationsPlugin() {} +FlutterLocalNotificationsPlugin::~FlutterLocalNotificationsPlugin() {} - void FlutterLocalNotificationsPlugin::HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result - ) { - const auto& method_name = method_call.method_name(); - if (method_name == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { - result->Success(); +PluginMethodChannel& FlutterLocalNotificationsPlugin::GetPluginMethodChannel() { + return channel; +} + +void FlutterLocalNotificationsPlugin::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result +) { + const auto& method_name = method_call.method_name(); + if (method_name == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { + result->Success(); + } + else if (method_name == Method::INITIALIZE) { + const auto args = std::get_if(method_call.arguments()); + if (args != nullptr) { + const auto appName = Utils::GetMapValue("appName", args).value(); + const auto aumid = Utils::GetMapValue("aumid", args).value(); + const auto iconPath = Utils::GetMapValue("iconPath", args); + const auto iconBgColor = Utils::GetMapValue("iconBgColor", args); + + Initialize(appName, aumid, iconPath, iconBgColor); + result->Success(true); } - else if (method_name == Method::INITIALIZE) { - const auto args = std::get_if(method_call.arguments()); - if (args != nullptr) { - const auto appName = Utils::GetMapValue("appName", args).value(); - const auto aumid = Utils::GetMapValue("aumid", args).value(); - const auto iconPath = Utils::GetMapValue("iconPath", args); - const auto iconBgColor = Utils::GetMapValue("iconBgColor", args); - - Initialize(appName, aumid, iconPath, iconBgColor); - result->Success(true); - } - else { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); - } + else { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); } - else if (method_name == Method::SHOW) { - const auto args = std::get_if(method_call.arguments()); - if (args != nullptr && toastNotifier.has_value()) { - const auto id = Utils::GetMapValue("id", args).value(); - const auto title = Utils::GetMapValue("title", args); - const auto body = Utils::GetMapValue("body", args); - const auto payload = Utils::GetMapValue("payload", args); - const auto group = Utils::GetMapValue("group", args); - - ShowNotification(id, title, body, payload, group); - result->Success(); - } - else { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); - } + } + else if (method_name == Method::SHOW) { + channel.InvokeMethod("test", nullptr, nullptr); + + const auto args = std::get_if(method_call.arguments()); + if (args != nullptr && toastNotifier.has_value()) { + const auto id = Utils::GetMapValue("id", args).value(); + const auto title = Utils::GetMapValue("title", args); + const auto body = Utils::GetMapValue("body", args); + const auto payload = Utils::GetMapValue("payload", args); + const auto group = Utils::GetMapValue("group", args); + + ShowNotification(id, title, body, payload, group); + result->Success(); } - else if (method_name == Method::CANCEL && toastNotifier.has_value()) { - const auto args = std::get_if(method_call.arguments()); - if (args != nullptr) { - const auto id = Utils::GetMapValue("id", args).value(); - const auto group = Utils::GetMapValue("group", args); - - CancelNotification(id, group); - result->Success(); - } - else { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); - } + else { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); } - else if (method_name == Method::CANCEL_ALL && toastNotifier.has_value()) { - CancelAllNotifications(); + } + else if (method_name == Method::CANCEL && toastNotifier.has_value()) { + const auto args = std::get_if(method_call.arguments()); + if (args != nullptr) { + const auto id = Utils::GetMapValue("id", args).value(); + const auto group = Utils::GetMapValue("group", args); + + CancelNotification(id, group); result->Success(); } else { - result->NotImplemented(); + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); } } - - void FlutterLocalNotificationsPlugin::Initialize( - const std::string& appName, - const std::string& aumid, - const std::optional& iconPath, - const std::optional& iconBgColor - ) { - _aumid = winrt::to_hstring(aumid); - PluginRegistration::RegisterApp(aumid, appName, iconPath, iconBgColor); - toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); + else if (method_name == Method::CANCEL_ALL && toastNotifier.has_value()) { + CancelAllNotifications(); + result->Success(); + } + else { + result->NotImplemented(); } +} - void FlutterLocalNotificationsPlugin::ShowNotification( - const int id, - const std::optional& title, - const std::optional& body, - const std::optional& payload, - const std::optional& group - ) { - // obtain a notification template with a title and a body - const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); - // find all tags - const auto nodes = doc.GetElementsByTagName(L"text"); - - if (title.has_value()) { - // change the text of the first , which will be the title - nodes.Item(0).AppendChild(doc.CreateTextNode(winrt::to_hstring(title.value()))); - } - if (body.has_value()) { - // change the text of the second , which will be the body - nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body.value()))); - } +void FlutterLocalNotificationsPlugin::Initialize( + const std::string& appName, + const std::string& aumid, + const std::optional& iconPath, + const std::optional& iconBgColor +) { + _aumid = winrt::to_hstring(aumid); + PluginRegistration::RegisterApp(aumid, appName, iconPath, iconBgColor, this); + toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); +} - winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; - notif.Tag(winrt::to_hstring(id)); - if (group.has_value()) { - notif.Group(winrt::to_hstring(group.value())); - } - else { - notif.Group(_aumid); - } +void FlutterLocalNotificationsPlugin::ShowNotification( + const int id, + const std::optional& title, + const std::optional& body, + const std::optional& payload, + const std::optional& group +) { + // obtain a notification template with a title and a body + const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); + // find all tags + const auto nodes = doc.GetElementsByTagName(L"text"); + + if (title.has_value()) { + // change the text of the first , which will be the title + nodes.Item(0).AppendChild(doc.CreateTextNode(winrt::to_hstring(title.value()))); + } + if (body.has_value()) { + // change the text of the second , which will be the body + nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body.value()))); + } - toastNotifier.value().Show(notif); + winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; + notif.Tag(winrt::to_hstring(id)); + if (group.has_value()) { + notif.Group(winrt::to_hstring(group.value())); + } + else { + notif.Group(_aumid); } - void FlutterLocalNotificationsPlugin::CancelNotification(const int id, const std::optional& group) { - if (!toastNotificationHistory.has_value()) { - toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); - } + toastNotifier.value().Show(notif); +} - if (group.has_value()) { - toastNotificationHistory.value().Remove(winrt::to_hstring(id), winrt::to_hstring(group.value()), _aumid); - } - else { - toastNotificationHistory.value().Remove(winrt::to_hstring(id), _aumid, _aumid); - } +void FlutterLocalNotificationsPlugin::CancelNotification(const int id, const std::optional& group) { + if (!toastNotificationHistory.has_value()) { + toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); } - void FlutterLocalNotificationsPlugin::CancelAllNotifications() { - if (!toastNotificationHistory.has_value()) { - toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); - } - toastNotificationHistory.value().Clear(_aumid); + if (group.has_value()) { + toastNotificationHistory.value().Remove(winrt::to_hstring(id), winrt::to_hstring(group.value()), _aumid); + } + else { + toastNotificationHistory.value().Remove(winrt::to_hstring(id), _aumid, _aumid); } -} // namespace +} + +void FlutterLocalNotificationsPlugin::CancelAllNotifications() { + if (!toastNotificationHistory.has_value()) { + toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); + } + toastNotificationHistory.value().Clear(_aumid); +} void FlutterLocalNotificationsPluginRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar) { diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h b/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h index dda06d016..37be0aac4 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h @@ -1,7 +1,18 @@ #ifndef FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ #define FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ +#pragma once +#include +#include + #include +#include +#include +#include +#include +#include + +#include #ifdef FLUTTER_PLUGIN_IMPL #define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) @@ -20,4 +31,70 @@ FLUTTER_PLUGIN_EXPORT void FlutterLocalNotificationsPluginRegisterWithRegistrar( } // extern "C" #endif +/// +/// Defines the type of the method channel used by this plugin. +/// +typedef flutter::MethodChannel PluginMethodChannel; + +class FlutterLocalNotificationsPlugin : public flutter::Plugin { +public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + + PluginMethodChannel& GetPluginMethodChannel(); + + FlutterLocalNotificationsPlugin(PluginMethodChannel& channel); + + virtual ~FlutterLocalNotificationsPlugin(); + +private: + std::wstring _aumid; + std::optional toastNotifier; + std::optional toastNotificationHistory; + PluginMethodChannel& channel; + + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + /// + /// Initializes this plugin. + /// + /// The app user model ID that identifies the app. + /// The display name of the app. + /// An optional path to the icon of the app. + /// An optional background color of the icon, in AARRGGBB format. + void Initialize( + const std::string& appName, + const std::string& aumid, + const std::optional& iconPath, + const std::optional& iconBgColor); + + /// + /// Displays a single notification toast. + /// + /// A unique ID that identifies this notification. It can be used to cancel/dismiss the notification. + /// An optional title of the notification. + /// An optional body of the notification. + /// + void ShowNotification( + const int id, + const std::optional& title, + const std::optional& body, + const std::optional& payload, + const std::optional& group); + + /// + /// Dismisses the notification that has the given ID. + /// + /// The ID of the notification to be dismissed. + /// The group the notification is in. Default is the aumid of this app. + void CancelNotification(const int id, const std::optional& group); + + /// + /// Dismisses all currently active notifications. + /// + void CancelAllNotifications(); +}; + #endif // FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h index ed60d8039..0286ff96d 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h @@ -11,4 +11,5 @@ namespace Method extern const std::string SHOW; extern const std::string CANCEL; extern const std::string CANCEL_ALL; -} \ No newline at end of file + extern const std::string SELECT_NOTIFICATION; +} diff --git a/flutter_local_notifications/windows/methods.cpp b/flutter_local_notifications/windows/methods.cpp index b4a9f5064..295d46885 100644 --- a/flutter_local_notifications/windows/methods.cpp +++ b/flutter_local_notifications/windows/methods.cpp @@ -7,3 +7,4 @@ const std::string Method::SHOW = "show"; const std::string Method::INITIALIZE = "initialize"; const std::string Method::CANCEL = "cancel"; const std::string Method::CANCEL_ALL = "cancelAll"; +const std::string Method::SELECT_NOTIFICATION = "selectNotification"; diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index 9c9ee87f9..654380778 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -3,6 +3,8 @@ // https://stackoverflow.com/questions/67005337/how-works-notifications-on-windows-registry-no-shortlink #include "registration.h" +#include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" +#include "include/flutter_local_notifications/methods.h" #include #include @@ -35,6 +37,8 @@ const std::string CALLBACK_GUID_STR = "{68d0c89d-760f-4f79-a067-ae8d4220ccc1}"; /// struct NotificationActivationCallback : winrt::implements { + FlutterLocalNotificationsPlugin* plugin = nullptr; + HRESULT __stdcall Activate( LPCWSTR app, LPCWSTR args, @@ -45,6 +49,10 @@ struct NotificationActivationCallback : winrt::implementsGetPluginMethodChannel().InvokeMethod(Method::SELECT_NOTIFICATION, nullptr, nullptr); + } return S_OK; } catch (...) { @@ -58,18 +66,27 @@ struct NotificationActivationCallback : winrt::implements struct NotificationActivationCallbackFactory : winrt::implements { + FlutterLocalNotificationsPlugin* plugin; + HRESULT __stdcall CreateInstance( IUnknown* outer, GUID const& iid, void** result) noexcept final { + std::cout << "CreateInstance" << std::endl; + *result = nullptr; if (outer) { return CLASS_E_NOAGGREGATION; } - return winrt::make()->QueryInterface(iid, result); + const auto cb = winrt::make_self(); + cb.get()->plugin = plugin; + + std::cout << plugin << std::endl; + + return cb->QueryInterface(iid, result); } HRESULT __stdcall LockServer(BOOL) noexcept final { @@ -228,12 +245,19 @@ void UpdateRegistry( /// Register the notificatio activation callback factory /// and the guid of the callback. /// -void RegisterCallback() { +void RegisterCallback(FlutterLocalNotificationsPlugin* plugin) { DWORD registration{}; + const auto factory_ref = winrt::make_self(); + const auto factory = factory_ref.get(); + factory->plugin = plugin; + + std::cout << factory->plugin << std::endl; + std::cout << plugin << std::endl; + winrt::check_hresult(CoRegisterClassObject( CALLBACK_GUID, - winrt::make().get(), + factory, CLSCTX_LOCAL_SERVER, REGCLS_SINGLEUSE, ®istration)); @@ -243,9 +267,10 @@ void PluginRegistration::RegisterApp( const std::string& aumid, const std::string& appName, const std::optional& iconPath, - const std::optional& iconBgColor + const std::optional& iconBgColor, + FlutterLocalNotificationsPlugin* plugin ) { std::cout << "register app" << std::endl; UpdateRegistry(aumid, appName, iconPath, iconBgColor); - RegisterCallback(); + RegisterCallback(plugin); } diff --git a/flutter_local_notifications/windows/registration.h b/flutter_local_notifications/windows/registration.h index c9127e7c8..cebbc9930 100644 --- a/flutter_local_notifications/windows/registration.h +++ b/flutter_local_notifications/windows/registration.h @@ -1,6 +1,8 @@ #ifndef PLUGIN_REGISTRATRION_H_ #define PLUGIN_REGISTRATRION_H_ +#include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" + #include #include @@ -15,11 +17,13 @@ namespace PluginRegistration { /// The display name of the app. /// An optional path to the icon of the app. /// An optional background color of the icon, in AARRGGBB format. + /// The instance of the plugin calling this function void RegisterApp( const std::string& aumid, const std::string& appName, const std::optional& iconPath, - const std::optional& iconBgColor); + const std::optional& iconBgColor, + FlutterLocalNotificationsPlugin* plugin); } #endif // !PLUGIN_REGISTRATRION_H_ From d2f66e925bd83c927aebc52b01d4598cb4b34d32 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Sat, 5 Mar 2022 23:57:39 +0000 Subject: [PATCH 012/112] Fix read access violation --- .../flutter_local_notifications_plugin.cpp | 319 +++++++++++------- .../flutter_local_notifications_plugin.h | 61 ---- .../windows/registration.cpp | 22 +- .../windows/registration.h | 3 +- 4 files changed, 197 insertions(+), 208 deletions(-) diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 51456dfc4..f9acbd40a 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -23,159 +23,216 @@ #include using namespace winrt::Windows::Data::Xml::Dom; +namespace { + + class FlutterLocalNotificationsPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + + FlutterLocalNotificationsPlugin(std::shared_ptr channel); + + virtual ~FlutterLocalNotificationsPlugin(); + + private: + std::wstring _aumid; + std::optional toastNotifier; + std::optional toastNotificationHistory; + std::shared_ptr channel; + + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + /// + /// Initializes this plugin. + /// + /// The app user model ID that identifies the app. + /// The display name of the app. + /// An optional path to the icon of the app. + /// An optional background color of the icon, in AARRGGBB format. + void Initialize( + const std::string& appName, + const std::string& aumid, + const std::optional& iconPath, + const std::optional& iconBgColor); + + /// + /// Displays a single notification toast. + /// + /// A unique ID that identifies this notification. It can be used to cancel/dismiss the notification. + /// An optional title of the notification. + /// An optional body of the notification. + /// + void ShowNotification( + const int id, + const std::optional& title, + const std::optional& body, + const std::optional& payload, + const std::optional& group); + + /// + /// Dismisses the notification that has the given ID. + /// + /// The ID of the notification to be dismissed. + /// The group the notification is in. Default is the aumid of this app. + void CancelNotification(const int id, const std::optional& group); + + /// + /// Dismisses all currently active notifications. + /// + void CancelAllNotifications(); + }; + + // static + void FlutterLocalNotificationsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows* registrar) { + auto channel = + std::make_shared( + registrar->messenger(), "dexterous.com/flutter/local_notifications", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(channel); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto& call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); + } -// static -void FlutterLocalNotificationsPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarWindows* registrar) { - auto channel = - std::make_unique( - registrar->messenger(), "dexterous.com/flutter/local_notifications", - &flutter::StandardMethodCodec::GetInstance()); - - auto plugin = std::make_unique(*channel); - - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto& call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - - registrar->AddPlugin(std::move(plugin)); -} - -FlutterLocalNotificationsPlugin::FlutterLocalNotificationsPlugin(PluginMethodChannel& channel) : - channel(channel) {} - -FlutterLocalNotificationsPlugin::~FlutterLocalNotificationsPlugin() {} + FlutterLocalNotificationsPlugin::FlutterLocalNotificationsPlugin(std::shared_ptr channel) : + channel(channel) {} -PluginMethodChannel& FlutterLocalNotificationsPlugin::GetPluginMethodChannel() { - return channel; -} + FlutterLocalNotificationsPlugin::~FlutterLocalNotificationsPlugin() {} -void FlutterLocalNotificationsPlugin::HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result -) { - const auto& method_name = method_call.method_name(); - if (method_name == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { - result->Success(); - } - else if (method_name == Method::INITIALIZE) { - const auto args = std::get_if(method_call.arguments()); - if (args != nullptr) { - const auto appName = Utils::GetMapValue("appName", args).value(); - const auto aumid = Utils::GetMapValue("aumid", args).value(); - const auto iconPath = Utils::GetMapValue("iconPath", args); - const auto iconBgColor = Utils::GetMapValue("iconBgColor", args); - - Initialize(appName, aumid, iconPath, iconBgColor); - result->Success(true); + void FlutterLocalNotificationsPlugin::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result + ) { + const auto& method_name = method_call.method_name(); + if (method_name == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { + result->Success(); } - else { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + else if (method_name == Method::INITIALIZE) { + const auto args = std::get_if(method_call.arguments()); + if (args != nullptr) { + const auto appName = Utils::GetMapValue("appName", args).value(); + const auto aumid = Utils::GetMapValue("aumid", args).value(); + const auto iconPath = Utils::GetMapValue("iconPath", args); + const auto iconBgColor = Utils::GetMapValue("iconBgColor", args); + + Initialize(appName, aumid, iconPath, iconBgColor); + result->Success(true); + } + else { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + } } - } - else if (method_name == Method::SHOW) { - channel.InvokeMethod("test", nullptr, nullptr); - - const auto args = std::get_if(method_call.arguments()); - if (args != nullptr && toastNotifier.has_value()) { - const auto id = Utils::GetMapValue("id", args).value(); - const auto title = Utils::GetMapValue("title", args); - const auto body = Utils::GetMapValue("body", args); - const auto payload = Utils::GetMapValue("payload", args); - const auto group = Utils::GetMapValue("group", args); - - ShowNotification(id, title, body, payload, group); - result->Success(); + else if (method_name == Method::SHOW) { + channel->InvokeMethod("test", nullptr); + + const auto args = std::get_if(method_call.arguments()); + if (args != nullptr && toastNotifier.has_value()) { + const auto id = Utils::GetMapValue("id", args).value(); + const auto title = Utils::GetMapValue("title", args); + const auto body = Utils::GetMapValue("body", args); + const auto payload = Utils::GetMapValue("payload", args); + const auto group = Utils::GetMapValue("group", args); + + ShowNotification(id, title, body, payload, group); + result->Success(); + } + else { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + } } - else { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + else if (method_name == Method::CANCEL && toastNotifier.has_value()) { + const auto args = std::get_if(method_call.arguments()); + if (args != nullptr) { + const auto id = Utils::GetMapValue("id", args).value(); + const auto group = Utils::GetMapValue("group", args); + + CancelNotification(id, group); + result->Success(); + } + else { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + } } - } - else if (method_name == Method::CANCEL && toastNotifier.has_value()) { - const auto args = std::get_if(method_call.arguments()); - if (args != nullptr) { - const auto id = Utils::GetMapValue("id", args).value(); - const auto group = Utils::GetMapValue("group", args); - - CancelNotification(id, group); + else if (method_name == Method::CANCEL_ALL && toastNotifier.has_value()) { + CancelAllNotifications(); result->Success(); } else { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + result->NotImplemented(); } } - else if (method_name == Method::CANCEL_ALL && toastNotifier.has_value()) { - CancelAllNotifications(); - result->Success(); - } - else { - result->NotImplemented(); - } -} -void FlutterLocalNotificationsPlugin::Initialize( - const std::string& appName, - const std::string& aumid, - const std::optional& iconPath, - const std::optional& iconBgColor -) { - _aumid = winrt::to_hstring(aumid); - PluginRegistration::RegisterApp(aumid, appName, iconPath, iconBgColor, this); - toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); -} - -void FlutterLocalNotificationsPlugin::ShowNotification( - const int id, - const std::optional& title, - const std::optional& body, - const std::optional& payload, - const std::optional& group -) { - // obtain a notification template with a title and a body - const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); - // find all tags - const auto nodes = doc.GetElementsByTagName(L"text"); - - if (title.has_value()) { - // change the text of the first , which will be the title - nodes.Item(0).AppendChild(doc.CreateTextNode(winrt::to_hstring(title.value()))); - } - if (body.has_value()) { - // change the text of the second , which will be the body - nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body.value()))); + void FlutterLocalNotificationsPlugin::Initialize( + const std::string& appName, + const std::string& aumid, + const std::optional& iconPath, + const std::optional& iconBgColor + ) { + _aumid = winrt::to_hstring(aumid); + PluginRegistration::RegisterApp(aumid, appName, iconPath, iconBgColor, channel); + toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); } - winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; - notif.Tag(winrt::to_hstring(id)); - if (group.has_value()) { - notif.Group(winrt::to_hstring(group.value())); - } - else { - notif.Group(_aumid); - } + void FlutterLocalNotificationsPlugin::ShowNotification( + const int id, + const std::optional& title, + const std::optional& body, + const std::optional& payload, + const std::optional& group + ) { + // obtain a notification template with a title and a body + const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); + // find all tags + const auto nodes = doc.GetElementsByTagName(L"text"); + + if (title.has_value()) { + // change the text of the first , which will be the title + nodes.Item(0).AppendChild(doc.CreateTextNode(winrt::to_hstring(title.value()))); + } + if (body.has_value()) { + // change the text of the second , which will be the body + nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body.value()))); + } - toastNotifier.value().Show(notif); -} + winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; + notif.Tag(winrt::to_hstring(id)); + if (group.has_value()) { + notif.Group(winrt::to_hstring(group.value())); + } + else { + notif.Group(_aumid); + } -void FlutterLocalNotificationsPlugin::CancelNotification(const int id, const std::optional& group) { - if (!toastNotificationHistory.has_value()) { - toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); + toastNotifier.value().Show(notif); } - if (group.has_value()) { - toastNotificationHistory.value().Remove(winrt::to_hstring(id), winrt::to_hstring(group.value()), _aumid); - } - else { - toastNotificationHistory.value().Remove(winrt::to_hstring(id), _aumid, _aumid); + void FlutterLocalNotificationsPlugin::CancelNotification(const int id, const std::optional& group) { + if (!toastNotificationHistory.has_value()) { + toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); + } + + if (group.has_value()) { + toastNotificationHistory.value().Remove(winrt::to_hstring(id), winrt::to_hstring(group.value()), _aumid); + } + else { + toastNotificationHistory.value().Remove(winrt::to_hstring(id), _aumid, _aumid); + } } -} -void FlutterLocalNotificationsPlugin::CancelAllNotifications() { - if (!toastNotificationHistory.has_value()) { - toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); + void FlutterLocalNotificationsPlugin::CancelAllNotifications() { + if (!toastNotificationHistory.has_value()) { + toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); + } + toastNotificationHistory.value().Clear(_aumid); } - toastNotificationHistory.value().Clear(_aumid); } void FlutterLocalNotificationsPluginRegisterWithRegistrar( diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h b/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h index 37be0aac4..d50c367fc 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h @@ -36,65 +36,4 @@ FLUTTER_PLUGIN_EXPORT void FlutterLocalNotificationsPluginRegisterWithRegistrar( ///
typedef flutter::MethodChannel PluginMethodChannel; -class FlutterLocalNotificationsPlugin : public flutter::Plugin { -public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); - - PluginMethodChannel& GetPluginMethodChannel(); - - FlutterLocalNotificationsPlugin(PluginMethodChannel& channel); - - virtual ~FlutterLocalNotificationsPlugin(); - -private: - std::wstring _aumid; - std::optional toastNotifier; - std::optional toastNotificationHistory; - PluginMethodChannel& channel; - - // Called when a method is called on this plugin's channel from Dart. - void HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - - /// - /// Initializes this plugin. - /// - /// The app user model ID that identifies the app. - /// The display name of the app. - /// An optional path to the icon of the app. - /// An optional background color of the icon, in AARRGGBB format. - void Initialize( - const std::string& appName, - const std::string& aumid, - const std::optional& iconPath, - const std::optional& iconBgColor); - - /// - /// Displays a single notification toast. - /// - /// A unique ID that identifies this notification. It can be used to cancel/dismiss the notification. - /// An optional title of the notification. - /// An optional body of the notification. - /// - void ShowNotification( - const int id, - const std::optional& title, - const std::optional& body, - const std::optional& payload, - const std::optional& group); - - /// - /// Dismisses the notification that has the given ID. - /// - /// The ID of the notification to be dismissed. - /// The group the notification is in. Default is the aumid of this app. - void CancelNotification(const int id, const std::optional& group); - - /// - /// Dismisses all currently active notifications. - /// - void CancelAllNotifications(); -}; - #endif // FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index 654380778..62a4301a2 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -37,7 +37,7 @@ const std::string CALLBACK_GUID_STR = "{68d0c89d-760f-4f79-a067-ae8d4220ccc1}"; /// struct NotificationActivationCallback : winrt::implements { - FlutterLocalNotificationsPlugin* plugin = nullptr; + std::shared_ptr channel; HRESULT __stdcall Activate( LPCWSTR app, @@ -49,10 +49,7 @@ struct NotificationActivationCallback : winrt::implementsGetPluginMethodChannel().InvokeMethod(Method::SELECT_NOTIFICATION, nullptr, nullptr); - } + channel->InvokeMethod(Method::SELECT_NOTIFICATION, nullptr, nullptr); return S_OK; } catch (...) { @@ -66,7 +63,7 @@ struct NotificationActivationCallback : winrt::implements struct NotificationActivationCallbackFactory : winrt::implements { - FlutterLocalNotificationsPlugin* plugin; + std::shared_ptr channel; HRESULT __stdcall CreateInstance( IUnknown* outer, @@ -82,9 +79,7 @@ struct NotificationActivationCallbackFactory : winrt::implements(); - cb.get()->plugin = plugin; - - std::cout << plugin << std::endl; + cb.get()->channel = channel; return cb->QueryInterface(iid, result); } @@ -245,15 +240,12 @@ void UpdateRegistry( /// Register the notificatio activation callback factory /// and the guid of the callback. /// -void RegisterCallback(FlutterLocalNotificationsPlugin* plugin) { +void RegisterCallback(std::shared_ptr channel) { DWORD registration{}; const auto factory_ref = winrt::make_self(); const auto factory = factory_ref.get(); - factory->plugin = plugin; - - std::cout << factory->plugin << std::endl; - std::cout << plugin << std::endl; + factory->channel = channel; winrt::check_hresult(CoRegisterClassObject( CALLBACK_GUID, @@ -268,7 +260,7 @@ void PluginRegistration::RegisterApp( const std::string& appName, const std::optional& iconPath, const std::optional& iconBgColor, - FlutterLocalNotificationsPlugin* plugin + std::shared_ptr plugin ) { std::cout << "register app" << std::endl; UpdateRegistry(aumid, appName, iconPath, iconBgColor); diff --git a/flutter_local_notifications/windows/registration.h b/flutter_local_notifications/windows/registration.h index cebbc9930..247f8c399 100644 --- a/flutter_local_notifications/windows/registration.h +++ b/flutter_local_notifications/windows/registration.h @@ -5,6 +5,7 @@ #include #include +#include /// /// Contains logic for handling Registry values. @@ -23,7 +24,7 @@ namespace PluginRegistration { const std::string& appName, const std::optional& iconPath, const std::optional& iconBgColor, - FlutterLocalNotificationsPlugin* plugin); + std::shared_ptr plugin); } #endif // !PLUGIN_REGISTRATRION_H_ From dd813e12f852b3ab165190c96b1e7513c42b8ab9 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Mon, 28 Mar 2022 14:31:13 -0700 Subject: [PATCH 013/112] Small fix on notification. --- .../lib/src/platform_flutter_local_notifications.dart | 9 +++++---- flutter_local_notifications/windows/registration.cpp | 6 ++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index a73eb0b1e..7347b8675 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -855,7 +855,7 @@ class MacOSFlutterLocalNotificationsPlugin /// Windows implementation of the flutter_local_notifications plugin. class WindowsFlutterLocalNotificationsPlugin extends MethodChannelFlutterLocalNotificationsPlugin { - SelectNotificationCallback? onSelectNotification; + SelectNotificationCallback? _onSelectNotification; /// Initializes the plugin. /// @@ -870,8 +870,8 @@ class WindowsFlutterLocalNotificationsPlugin WindowsInitializationSettings settings, { SelectNotificationCallback? onSelectNotification, }) { - this.onSelectNotification = onSelectNotification; - _channel.setMethodCallHandler(_onMethodCallFromNative); + this._onSelectNotification = onSelectNotification; + _channel.setMethodCallHandler(_handleMethod); return _channel.invokeMethod('initialize', settings.toMap()); } @@ -899,11 +899,12 @@ class WindowsFlutterLocalNotificationsPlugin 'group': group, }); - Future _onMethodCallFromNative(MethodCall call) async { + Future _handleMethod(MethodCall call) async { print('call $call'); switch (call.method) { case 'selectNotification': print('notification selected'); + _onSelectNotification?.call(call.arguments); break; } } diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index 62a4301a2..59171d439 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -9,8 +9,6 @@ #include #include #include -#include -#include #include #include #include @@ -131,7 +129,7 @@ void UpdateRegistry( RegistryKey key; // create registry key - // HKEY_CURRENT_USER\Software\Microsoft\\Windows\CurrentVersion\PushNotifications\Backup + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\PushNotifications\Backup winrt::check_win32(RegCreateKeyExA( HKEY_CURRENT_USER, notifSettingsKeyPath.c_str(), @@ -251,7 +249,7 @@ void RegisterCallback(std::shared_ptr channel) { CALLBACK_GUID, factory, CLSCTX_LOCAL_SERVER, - REGCLS_SINGLEUSE, + REGCLS_MULTIPLEUSE, ®istration)); } From 57ecbf0a6fbb54493ec3e09368df07594db90561 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Thu, 31 Mar 2022 12:24:33 +0100 Subject: [PATCH 014/112] Pass payload to Dart on notification selected --- .../example/pubspec.yaml | 45 +++++++++++++++++++ .../flutter_local_notifications/pubspec.yaml | 41 +++++++++++++++++ .../pubspec.yaml | 22 +++++++++ .../pubspec.yaml | 16 +++++++ .../example/lib/main.dart | 1 + .../flutter_local_notifications_plugin.dart | 4 ++ .../platform_flutter_local_notifications.dart | 11 +++-- .../flutter_local_notifications_plugin.cpp | 32 ++++++++++--- .../windows/registration.cpp | 6 ++- 9 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 .dart_tool/melos_tool/flutter_local_notifications/example/pubspec.yaml create mode 100644 .dart_tool/melos_tool/flutter_local_notifications/pubspec.yaml create mode 100644 .dart_tool/melos_tool/flutter_local_notifications_linux/pubspec.yaml create mode 100644 .dart_tool/melos_tool/flutter_local_notifications_platform_interface/pubspec.yaml diff --git a/.dart_tool/melos_tool/flutter_local_notifications/example/pubspec.yaml b/.dart_tool/melos_tool/flutter_local_notifications/example/pubspec.yaml new file mode 100644 index 000000000..98237739c --- /dev/null +++ b/.dart_tool/melos_tool/flutter_local_notifications/example/pubspec.yaml @@ -0,0 +1,45 @@ +# Generated file - do not commit this file. +description: "Demonstrates how to use the flutter_local_notifications plugin." +name: "flutter_local_notifications_example" +publish_to: "none" +dependencies: + cupertino_icons: "^1.0.2" + device_info: "^2.0.2" + flutter_native_timezone: "^2.0.0" + http: "^0.13.4" + image: "^3.0.8" + path_provider: "^2.0.0" + rxdart: "^0.27.2" + shared_preferences: "^2.0.1" + url_launcher: "^6.0.17" + flutter: + sdk: "flutter" + flutter_local_notifications: + path: "../" +dependency_overrides: + flutter_local_notifications: + path: ".." + flutter_local_notifications_linux: + path: "..\\..\\flutter_local_notifications_linux" + flutter_local_notifications_platform_interface: + path: "..\\..\\flutter_local_notifications_platform_interface" +dev_dependencies: + msix: "^2.8.17" + flutter_driver: + sdk: "flutter" + flutter_test: + sdk: "flutter" + integration_test: + sdk: "flutter" +environment: + flutter: ">=1.26.0-0" + sdk: ">=2.12.0-0 <3.0.0" +flutter: + assets: + - "icons/" + - "sound/" + uses-material-design: true +msix_config: + display_name: "Flutter Local Notifications Example" + identity_name: "Com.Example.FlutterLocalNotificationsExample" + debug: true diff --git a/.dart_tool/melos_tool/flutter_local_notifications/pubspec.yaml b/.dart_tool/melos_tool/flutter_local_notifications/pubspec.yaml new file mode 100644 index 000000000..5ad81bdd3 --- /dev/null +++ b/.dart_tool/melos_tool/flutter_local_notifications/pubspec.yaml @@ -0,0 +1,41 @@ +# Generated file - do not commit this file. +description: "A cross platform plugin for displaying and scheduling local notifications for Flutter applications with the ability to customise for each platform." +homepage: "https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications" +name: "flutter_local_notifications" +version: "9.2.0" +dependencies: + clock: "^1.1.0" + flutter_local_notifications_linux: "^0.4.1" + flutter_local_notifications_platform_interface: "^6.0.0" + timezone: "^0.8.0" + flutter: + sdk: "flutter" +dependency_overrides: + flutter_local_notifications_linux: + path: "..\\flutter_local_notifications_linux" + flutter_local_notifications_platform_interface: + path: "..\\flutter_local_notifications_platform_interface" +dev_dependencies: + mockito: "^5.0.8" + plugin_platform_interface: "^2.0.0" + flutter_driver: + sdk: "flutter" + flutter_test: + sdk: "flutter" +environment: + flutter: ">=2.2.0" + sdk: ">=2.12.0 <3.0.0" +flutter: + plugin: + platforms: + android: + package: "com.dexterous.flutterlocalnotifications" + pluginClass: "FlutterLocalNotificationsPlugin" + ios: + pluginClass: "FlutterLocalNotificationsPlugin" + linux: + default_package: "flutter_local_notifications_linux" + macos: + pluginClass: "FlutterLocalNotificationsPlugin" + windows: + pluginClass: "FlutterLocalNotificationsPlugin" diff --git a/.dart_tool/melos_tool/flutter_local_notifications_linux/pubspec.yaml b/.dart_tool/melos_tool/flutter_local_notifications_linux/pubspec.yaml new file mode 100644 index 000000000..1b8919c40 --- /dev/null +++ b/.dart_tool/melos_tool/flutter_local_notifications_linux/pubspec.yaml @@ -0,0 +1,22 @@ +# Generated file - do not commit this file. +description: "Linux implementation of the flutter_local_notifications plugin" +homepage: "https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications" +name: "flutter_local_notifications_linux" +version: "0.4.1+1" +dependencies: + dbus: "^0.6.0" + flutter_local_notifications_platform_interface: "^6.0.0" + path: "^1.8.0" + xdg_directories: "^0.2.0" + flutter: + sdk: "flutter" +dependency_overrides: + flutter_local_notifications_platform_interface: + path: "..\\flutter_local_notifications_platform_interface" +dev_dependencies: + mocktail: "^0.1.4" + flutter_test: + sdk: "flutter" +environment: + flutter: ">=2.2.0" + sdk: ">=2.12.0 <3.0.0" diff --git a/.dart_tool/melos_tool/flutter_local_notifications_platform_interface/pubspec.yaml b/.dart_tool/melos_tool/flutter_local_notifications_platform_interface/pubspec.yaml new file mode 100644 index 000000000..cdbbafdd6 --- /dev/null +++ b/.dart_tool/melos_tool/flutter_local_notifications_platform_interface/pubspec.yaml @@ -0,0 +1,16 @@ +# Generated file - do not commit this file. +description: "A common platform interface for the flutter_local_notifications plugin." +homepage: "https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_platform_interface" +name: "flutter_local_notifications_platform_interface" +version: "6.0.0" +dependencies: + plugin_platform_interface: "^2.0.0" + flutter: + sdk: "flutter" +dev_dependencies: + mockito: "^5.0.8" + flutter_test: + sdk: "flutter" +environment: + flutter: ">=2.2.0" + sdk: ">=2.12.0 <3.0.0" diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index bbe5483c5..370955ebe 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -919,6 +919,7 @@ class _HomePageState extends State { ); Future _showNotification() async { + print('show notification'); const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails('your channel id', 'your channel name', channelDescription: 'your channel description', diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index 1882c511b..fdda763be 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -234,6 +234,10 @@ class FlutterLocalNotificationsPlugin { ?.show(id, title, body, notificationDetails: notificationDetails?.linux, payload: payload); + } else if (defaultTargetPlatform == TargetPlatform.windows) { + await resolvePlatformSpecificImplementation< + WindowsFlutterLocalNotificationsPlugin>() + ?.show(id, title, body, payload: payload); } else { await FlutterLocalNotificationsPlatform.instance.show(id, title, body); } diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 7347b8675..9b6f4fa01 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -4,7 +4,6 @@ import 'dart:ui'; import 'package:clock/clock.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; import 'package:timezone/timezone.dart'; @@ -870,7 +869,7 @@ class WindowsFlutterLocalNotificationsPlugin WindowsInitializationSettings settings, { SelectNotificationCallback? onSelectNotification, }) { - this._onSelectNotification = onSelectNotification; + _onSelectNotification = onSelectNotification; _channel.setMethodCallHandler(_handleMethod); return _channel.invokeMethod('initialize', settings.toMap()); @@ -889,7 +888,7 @@ class WindowsFlutterLocalNotificationsPlugin 'title': title, 'body': body, 'group': group, - 'payload': payload ?? '', + 'payload': payload, }); @override @@ -900,11 +899,11 @@ class WindowsFlutterLocalNotificationsPlugin }); Future _handleMethod(MethodCall call) async { - print('call $call'); switch (call.method) { case 'selectNotification': - print('notification selected'); - _onSelectNotification?.call(call.arguments); + if (call.arguments is String) { + _onSelectNotification?.call(call.arguments); + } break; } } diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index f9acbd40a..1d9783002 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -189,23 +189,45 @@ namespace { const std::optional& group ) { // obtain a notification template with a title and a body - const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); + //const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); // find all tags - const auto nodes = doc.GetElementsByTagName(L"text"); + //const auto nodes = doc.GetElementsByTagName(L"text"); + + XmlDocument doc; + doc.LoadXml(L"\ + \ + \ + \ + \ + \ + "); + + const auto bindingNode = doc.SelectSingleNode(L"//binding[1]"); if (title.has_value()) { // change the text of the first , which will be the title - nodes.Item(0).AppendChild(doc.CreateTextNode(winrt::to_hstring(title.value()))); + const auto textNode = doc.CreateElement(L"text"); + textNode.InnerText(winrt::to_hstring(*title)); + bindingNode.AppendChild(textNode); } if (body.has_value()) { // change the text of the second , which will be the body - nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body.value()))); + //nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body.value()))); + const auto textNode = doc.CreateElement(L"text"); + textNode.InnerText(winrt::to_hstring(*body)); + bindingNode.AppendChild(textNode); } + if (payload.has_value()) { + std::cout << "payload: " << *payload << std::endl; + doc.DocumentElement().SetAttribute(L"launch", winrt::to_hstring(*payload)); + } + + std::cout << winrt::to_string(doc.GetXml()) << std::endl; winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; notif.Tag(winrt::to_hstring(id)); if (group.has_value()) { - notif.Group(winrt::to_hstring(group.value())); + notif.Group(winrt::to_hstring(*group)); } else { notif.Group(_aumid); diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index 59171d439..14ded76ca 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -6,11 +6,12 @@ #include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" #include "include/flutter_local_notifications/methods.h" +#include +#include #include #include #include #include -#include #include #include @@ -47,7 +48,8 @@ struct NotificationActivationCallback : winrt::implementsInvokeMethod(Method::SELECT_NOTIFICATION, nullptr, nullptr); + const std::string payload = CW2A(args); + channel->InvokeMethod(Method::SELECT_NOTIFICATION, std::make_unique(payload), nullptr); return S_OK; } catch (...) { From 0baa7db0b641e18158e7690c874e1b2458649543 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Thu, 31 Mar 2022 12:26:23 +0100 Subject: [PATCH 015/112] Remove melos files --- .../example/pubspec.yaml | 45 ------------------- .../flutter_local_notifications/pubspec.yaml | 41 ----------------- .../pubspec.yaml | 22 --------- .../pubspec.yaml | 16 ------- 4 files changed, 124 deletions(-) delete mode 100644 .dart_tool/melos_tool/flutter_local_notifications/example/pubspec.yaml delete mode 100644 .dart_tool/melos_tool/flutter_local_notifications/pubspec.yaml delete mode 100644 .dart_tool/melos_tool/flutter_local_notifications_linux/pubspec.yaml delete mode 100644 .dart_tool/melos_tool/flutter_local_notifications_platform_interface/pubspec.yaml diff --git a/.dart_tool/melos_tool/flutter_local_notifications/example/pubspec.yaml b/.dart_tool/melos_tool/flutter_local_notifications/example/pubspec.yaml deleted file mode 100644 index 98237739c..000000000 --- a/.dart_tool/melos_tool/flutter_local_notifications/example/pubspec.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Generated file - do not commit this file. -description: "Demonstrates how to use the flutter_local_notifications plugin." -name: "flutter_local_notifications_example" -publish_to: "none" -dependencies: - cupertino_icons: "^1.0.2" - device_info: "^2.0.2" - flutter_native_timezone: "^2.0.0" - http: "^0.13.4" - image: "^3.0.8" - path_provider: "^2.0.0" - rxdart: "^0.27.2" - shared_preferences: "^2.0.1" - url_launcher: "^6.0.17" - flutter: - sdk: "flutter" - flutter_local_notifications: - path: "../" -dependency_overrides: - flutter_local_notifications: - path: ".." - flutter_local_notifications_linux: - path: "..\\..\\flutter_local_notifications_linux" - flutter_local_notifications_platform_interface: - path: "..\\..\\flutter_local_notifications_platform_interface" -dev_dependencies: - msix: "^2.8.17" - flutter_driver: - sdk: "flutter" - flutter_test: - sdk: "flutter" - integration_test: - sdk: "flutter" -environment: - flutter: ">=1.26.0-0" - sdk: ">=2.12.0-0 <3.0.0" -flutter: - assets: - - "icons/" - - "sound/" - uses-material-design: true -msix_config: - display_name: "Flutter Local Notifications Example" - identity_name: "Com.Example.FlutterLocalNotificationsExample" - debug: true diff --git a/.dart_tool/melos_tool/flutter_local_notifications/pubspec.yaml b/.dart_tool/melos_tool/flutter_local_notifications/pubspec.yaml deleted file mode 100644 index 5ad81bdd3..000000000 --- a/.dart_tool/melos_tool/flutter_local_notifications/pubspec.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Generated file - do not commit this file. -description: "A cross platform plugin for displaying and scheduling local notifications for Flutter applications with the ability to customise for each platform." -homepage: "https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications" -name: "flutter_local_notifications" -version: "9.2.0" -dependencies: - clock: "^1.1.0" - flutter_local_notifications_linux: "^0.4.1" - flutter_local_notifications_platform_interface: "^6.0.0" - timezone: "^0.8.0" - flutter: - sdk: "flutter" -dependency_overrides: - flutter_local_notifications_linux: - path: "..\\flutter_local_notifications_linux" - flutter_local_notifications_platform_interface: - path: "..\\flutter_local_notifications_platform_interface" -dev_dependencies: - mockito: "^5.0.8" - plugin_platform_interface: "^2.0.0" - flutter_driver: - sdk: "flutter" - flutter_test: - sdk: "flutter" -environment: - flutter: ">=2.2.0" - sdk: ">=2.12.0 <3.0.0" -flutter: - plugin: - platforms: - android: - package: "com.dexterous.flutterlocalnotifications" - pluginClass: "FlutterLocalNotificationsPlugin" - ios: - pluginClass: "FlutterLocalNotificationsPlugin" - linux: - default_package: "flutter_local_notifications_linux" - macos: - pluginClass: "FlutterLocalNotificationsPlugin" - windows: - pluginClass: "FlutterLocalNotificationsPlugin" diff --git a/.dart_tool/melos_tool/flutter_local_notifications_linux/pubspec.yaml b/.dart_tool/melos_tool/flutter_local_notifications_linux/pubspec.yaml deleted file mode 100644 index 1b8919c40..000000000 --- a/.dart_tool/melos_tool/flutter_local_notifications_linux/pubspec.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Generated file - do not commit this file. -description: "Linux implementation of the flutter_local_notifications plugin" -homepage: "https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications" -name: "flutter_local_notifications_linux" -version: "0.4.1+1" -dependencies: - dbus: "^0.6.0" - flutter_local_notifications_platform_interface: "^6.0.0" - path: "^1.8.0" - xdg_directories: "^0.2.0" - flutter: - sdk: "flutter" -dependency_overrides: - flutter_local_notifications_platform_interface: - path: "..\\flutter_local_notifications_platform_interface" -dev_dependencies: - mocktail: "^0.1.4" - flutter_test: - sdk: "flutter" -environment: - flutter: ">=2.2.0" - sdk: ">=2.12.0 <3.0.0" diff --git a/.dart_tool/melos_tool/flutter_local_notifications_platform_interface/pubspec.yaml b/.dart_tool/melos_tool/flutter_local_notifications_platform_interface/pubspec.yaml deleted file mode 100644 index cdbbafdd6..000000000 --- a/.dart_tool/melos_tool/flutter_local_notifications_platform_interface/pubspec.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Generated file - do not commit this file. -description: "A common platform interface for the flutter_local_notifications plugin." -homepage: "https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_platform_interface" -name: "flutter_local_notifications_platform_interface" -version: "6.0.0" -dependencies: - plugin_platform_interface: "^2.0.0" - flutter: - sdk: "flutter" -dev_dependencies: - mockito: "^5.0.8" - flutter_test: - sdk: "flutter" -environment: - flutter: ">=2.2.0" - sdk: ">=2.12.0 <3.0.0" From 5ad64641640be66a7f2eb96b63ae94aa53bfc43a Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 13 Apr 2022 22:01:52 +0100 Subject: [PATCH 016/112] Attempting to handle msix --- .../windows/notification_action.dart | 0 .../windows/notification_details.dart | 0 .../flutter_local_notifications_plugin.cpp | 38 ++++++++++++++++++- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart new file mode 100644 index 000000000..e69de29bb diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart new file mode 100644 index 000000000..e69de29bb diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 1d9783002..9d01bcf0b 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -6,10 +6,13 @@ // This must be included before many other Windows headers. #include #include +#include +#include #include #include #include #include +#include // For getPlatformVersion; remove unless needed for your plugin implementation. #include @@ -82,6 +85,8 @@ namespace { /// Dismisses all currently active notifications. /// void CancelAllNotifications(); + + std::optional HasIdentity(); }; // static @@ -170,6 +175,34 @@ namespace { } } + std::optional FlutterLocalNotificationsPlugin::HasIdentity() { + if (!IsWindows8OrGreater()) { + // OS is windows 7 or lower + return false; + } + + UINT32 length; + auto err = GetCurrentPackageFullName(&length, nullptr); + if (err != ERROR_INSUFFICIENT_BUFFER) { + if (err == APPMODEL_ERROR_NO_PACKAGE) + return false; + + return std::nullopt; + } + + PWSTR fullName = (PWSTR)malloc(length * sizeof(*fullName)); + if (fullName == nullptr) + return std::nullopt; + + err = GetCurrentPackageFullName(&length, fullName); + if (err != ERROR_SUCCESS) + return std::nullopt; + + free(fullName); + + return true; + } + void FlutterLocalNotificationsPlugin::Initialize( const std::string& appName, const std::string& aumid, @@ -178,7 +211,10 @@ namespace { ) { _aumid = winrt::to_hstring(aumid); PluginRegistration::RegisterApp(aumid, appName, iconPath, iconBgColor, channel); - toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); + if (HasIdentity()) + toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(); + else + toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); } void FlutterLocalNotificationsPlugin::ShowNotification( From effca5aaab1bcabc567c39a1f8da79ab239bf6d5 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 13 Apr 2022 22:06:44 +0100 Subject: [PATCH 017/112] Forgot to check for optional value --- .../windows/flutter_local_notifications_plugin.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 9d01bcf0b..9ed3d4f03 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -54,7 +54,8 @@ namespace { /// The display name of the app. /// An optional path to the icon of the app. /// An optional background color of the icon, in AARRGGBB format. - void Initialize( + /// Whether the initialization was successful. + bool Initialize( const std::string& appName, const std::string& aumid, const std::optional& iconPath, @@ -203,7 +204,7 @@ namespace { return true; } - void FlutterLocalNotificationsPlugin::Initialize( + bool FlutterLocalNotificationsPlugin::Initialize( const std::string& appName, const std::string& aumid, const std::optional& iconPath, @@ -211,7 +212,12 @@ namespace { ) { _aumid = winrt::to_hstring(aumid); PluginRegistration::RegisterApp(aumid, appName, iconPath, iconBgColor, channel); - if (HasIdentity()) + + const auto hasIdentity = HasIdentity(); + if (!hasIdentity.has_value()) + return false; + + if (hasIdentity.value()) toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(); else toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); From 31b1ee7f5a4b3e0f5986054e5a0e55b03b79e83e Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 19 Apr 2022 12:56:06 +0100 Subject: [PATCH 018/112] Fix Initialize not returning value --- .../windows/flutter_local_notifications_plugin.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 9ed3d4f03..792cfe576 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -129,8 +129,7 @@ namespace { const auto iconPath = Utils::GetMapValue("iconPath", args); const auto iconBgColor = Utils::GetMapValue("iconBgColor", args); - Initialize(appName, aumid, iconPath, iconBgColor); - result->Success(true); + result->Success(Initialize(appName, aumid, iconPath, iconBgColor)); } else { result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); @@ -221,6 +220,8 @@ namespace { toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(); else toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); + + return true; } void FlutterLocalNotificationsPlugin::ShowNotification( From adf957445fa06fb480358ab38bfa6360d90e12f4 Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Sun, 18 Sep 2022 16:41:15 +0800 Subject: [PATCH 019/112] temp fix issue about GetCurrentPackageFullName --- .../windows/flutter_local_notifications_plugin.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 792cfe576..1d2882db6 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -182,12 +182,12 @@ namespace { } UINT32 length; - auto err = GetCurrentPackageFullName(&length, nullptr); + auto err = GetCurrentPackageFullName(&length, NULL); if (err != ERROR_INSUFFICIENT_BUFFER) { if (err == APPMODEL_ERROR_NO_PACKAGE) return false; - - return std::nullopt; + + return false; } PWSTR fullName = (PWSTR)malloc(length * sizeof(*fullName)); From 0d5035ccc87251e82c71eb994a15a9f04902f5d4 Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Mon, 26 Sep 2022 15:27:22 +0800 Subject: [PATCH 020/112] fix: windows impl with upstream merge --- .../windows/flutter/generated_plugin_registrant.cc | 3 --- .../example/windows/flutter/generated_plugins.cmake | 9 ++++++++- .../lib/src/flutter_local_notifications_plugin.dart | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc index d5a662fbd..3ef053e07 100644 --- a/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc +++ b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,8 @@ #include "generated_plugin_registrant.h" #include -#include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterLocalNotificationsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterLocalNotificationsPlugin")); - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake b/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake index 41753c0a3..87bfe6c13 100644 --- a/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake +++ b/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake @@ -4,7 +4,9 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_local_notifications - url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) @@ -15,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index fc81ae1a8..3897d8d39 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -190,7 +190,8 @@ class FlutterLocalNotificationsPlugin { return await resolvePlatformSpecificImplementation< WindowsFlutterLocalNotificationsPlugin>() ?.initialize(initializationSettings.windows!, - onSelectNotification: onDidReceiveNotificationResponse); + onDidReceiveNotificationResponse: + onDidReceiveNotificationResponse); } return true; } From 168f9bae2a369504204ff9ecb7ee7ffe7620754d Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Mon, 26 Sep 2022 17:12:20 +0800 Subject: [PATCH 021/112] feat: support specify guid in windows --- .../example/lib/main.dart | 1 + .../windows/initialization_settings.dart | 4 +++ .../windows/method_channel_mappers.dart | 1 + .../flutter_local_notifications_plugin.cpp | 27 ++++++++++----- .../windows/registration.cpp | 34 ++++++++----------- .../windows/registration.h | 2 ++ 6 files changed, 41 insertions(+), 28 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index e5a56f629..a3787cb81 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -175,6 +175,7 @@ Future main() async { WindowsInitializationSettings( appName: 'Flutter Local Notifications Example', appUserModelId: 'Com.Dexterous.FlutterLocalNotificationsExample', + guid: '68d0c89d-760f-4f79-a067-ae8d4220ccc1', ); final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart index a4036e483..aac46bf60 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart @@ -6,6 +6,7 @@ class WindowsInitializationSettings { const WindowsInitializationSettings({ required this.appName, required this.appUserModelId, + required this.guid, this.iconPath, this.iconBackgroundColor, }); @@ -20,6 +21,9 @@ class WindowsInitializationSettings { /// for more information. final String appUserModelId; + /// The GUID that identifies the notification activation callback. + final String guid; + final String? iconPath; final Color? iconBackgroundColor; diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart index 01c577cc0..d52980b3c 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart @@ -7,6 +7,7 @@ extension WindowsInitializationSettingsMapper on WindowsInitializationSettings { Map toMap() => { 'appName': appName, 'aumid': appUserModelId, + 'guid': guid, 'iconPath': iconPath, 'iconBgColor': iconBackgroundColor, }; diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 1d2882db6..5a5601406 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -58,6 +58,7 @@ namespace { bool Initialize( const std::string& appName, const std::string& aumid, + const std::string& guid, const std::optional& iconPath, const std::optional& iconBgColor); @@ -124,12 +125,19 @@ namespace { else if (method_name == Method::INITIALIZE) { const auto args = std::get_if(method_call.arguments()); if (args != nullptr) { - const auto appName = Utils::GetMapValue("appName", args).value(); - const auto aumid = Utils::GetMapValue("aumid", args).value(); - const auto iconPath = Utils::GetMapValue("iconPath", args); - const auto iconBgColor = Utils::GetMapValue("iconBgColor", args); - - result->Success(Initialize(appName, aumid, iconPath, iconBgColor)); + try { + const auto appName = Utils::GetMapValue("appName", args).value(); + const auto aumid = Utils::GetMapValue("aumid", args).value(); + const auto guid = Utils::GetMapValue("guid", args).value(); + const auto iconPath = Utils::GetMapValue("iconPath", args); + const auto iconBgColor = Utils::GetMapValue("iconBgColor", args); + + result->Success(Initialize(appName, aumid, guid, iconPath, iconBgColor)); + } + // handle exception when user provide a invalid guid. + catch (std::invalid_argument err) { + result->Error("INVALID_ARGUMENT", err.what()); + } } else { result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); @@ -186,7 +194,7 @@ namespace { if (err != ERROR_INSUFFICIENT_BUFFER) { if (err == APPMODEL_ERROR_NO_PACKAGE) return false; - + return false; } @@ -206,12 +214,13 @@ namespace { bool FlutterLocalNotificationsPlugin::Initialize( const std::string& appName, const std::string& aumid, + const std::string& guid, const std::optional& iconPath, const std::optional& iconBgColor ) { _aumid = winrt::to_hstring(aumid); - PluginRegistration::RegisterApp(aumid, appName, iconPath, iconBgColor, channel); - + PluginRegistration::RegisterApp(aumid, appName, guid, iconPath, iconBgColor, channel); + const auto hasIdentity = HasIdentity(); if (!hasIdentity.has_value()) return false; diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index 14ded76ca..9e9dfc83f 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -18,19 +18,6 @@ #include #include -/// -/// The GUID that identifies the notification activation callback. -/// 68d0c89d-760f-4f79-a067-ae8d4220ccc1 -/// -static constexpr winrt::guid CALLBACK_GUID{ - 0x68d0c89d, 0x760f, 0x4f79, {0xa0, 0x67, 0xae, 0x8d, 0x42, 0x20, 0xcc, 0xc1} -}; - -/// -/// String representation of the callback GUID. -/// -const std::string CALLBACK_GUID_STR = "{68d0c89d-760f-4f79-a067-ae8d4220ccc1}"; - /// /// This callback will be called when a notification sent by this plugin is clicked on. /// @@ -122,6 +109,7 @@ using RegistryKey = winrt::handle_type; void UpdateRegistry( const std::string& aumid, const std::string& appName, + const std::string& guid, const std::optional& iconPath, const std::optional& iconBgColor ) { @@ -226,29 +214,36 @@ void UpdateRegistry( static_cast(v.size() + 1 * sizeof(char)))); } + // combine guid to class id + ss.clear(); + ss.str(std::string()); + ss << '{' << guid << '}'; + const auto clsid = ss.str(); + // register the guid of the notification activation callback winrt::check_win32(RegSetValueExA( appInfoKey.get(), "CustomActivator", 0, REG_SZ, - reinterpret_cast(CALLBACK_GUID_STR.c_str()), - static_cast(CALLBACK_GUID_STR.size() + 1 * sizeof(char)))); + reinterpret_cast(clsid.c_str()), + static_cast(clsid.size() + 1 * sizeof(char)))); } /// /// Register the notificatio activation callback factory /// and the guid of the callback. /// -void RegisterCallback(std::shared_ptr channel) { +void RegisterCallback(std::shared_ptr channel, const std::string& guid) { DWORD registration{}; const auto factory_ref = winrt::make_self(); const auto factory = factory_ref.get(); + winrt::guid rclsid(guid); factory->channel = channel; winrt::check_hresult(CoRegisterClassObject( - CALLBACK_GUID, + rclsid, factory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, @@ -258,11 +253,12 @@ void RegisterCallback(std::shared_ptr channel) { void PluginRegistration::RegisterApp( const std::string& aumid, const std::string& appName, + const std::string& guid, const std::optional& iconPath, const std::optional& iconBgColor, std::shared_ptr plugin ) { std::cout << "register app" << std::endl; - UpdateRegistry(aumid, appName, iconPath, iconBgColor); - RegisterCallback(plugin); + UpdateRegistry(aumid, appName, guid, iconPath, iconBgColor); + RegisterCallback(plugin, guid); } diff --git a/flutter_local_notifications/windows/registration.h b/flutter_local_notifications/windows/registration.h index 247f8c399..f2ea4d5fd 100644 --- a/flutter_local_notifications/windows/registration.h +++ b/flutter_local_notifications/windows/registration.h @@ -16,12 +16,14 @@ namespace PluginRegistration { /// /// The app user model ID that identifies the app. /// The display name of the app. + /// The display name of the app. /// An optional path to the icon of the app. /// An optional background color of the icon, in AARRGGBB format. /// The instance of the plugin calling this function void RegisterApp( const std::string& aumid, const std::string& appName, + const std::string& guid, const std::optional& iconPath, const std::optional& iconBgColor, std::shared_ptr plugin); From d2da530b621552eec16fd891736e12f539086687 Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Tue, 27 Sep 2022 16:58:34 +0800 Subject: [PATCH 022/112] fix: ERROR_INVALID_PARAMETER when GetCurrentPackageFullName. --- .../windows/flutter_local_notifications_plugin.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 5a5601406..4d1bdd562 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -189,13 +189,13 @@ namespace { return false; } - UINT32 length; + UINT32 length = 0; auto err = GetCurrentPackageFullName(&length, NULL); if (err != ERROR_INSUFFICIENT_BUFFER) { if (err == APPMODEL_ERROR_NO_PACKAGE) return false; - return false; + return std::nullopt; } PWSTR fullName = (PWSTR)malloc(length * sizeof(*fullName)); From 58d987d5cfcd34aec441e3a64135471ca1d24301 Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Tue, 27 Sep 2022 19:44:35 +0800 Subject: [PATCH 023/112] feat: support raw xml in windows --- .../example/lib/main.dart | 42 +++++++++++++++ .../lib/flutter_local_notifications.dart | 1 + .../flutter_local_notifications_plugin.dart | 4 +- .../lib/src/notification_details.dart | 5 ++ .../platform_flutter_local_notifications.dart | 9 ++-- .../windows/method_channel_mappers.dart | 10 ++++ .../windows/notification_details.dart | 9 ++++ .../flutter_local_notifications_plugin.cpp | 54 +++++++++++-------- .../flutter_local_notifications/methods.h | 2 +- .../windows/methods.cpp | 2 +- .../windows/registration.cpp | 21 ++++++-- .../windows/utils/utils.h | 2 +- 12 files changed, 129 insertions(+), 32 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index a3787cb81..e89a13d12 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -262,6 +262,9 @@ class _HomePageState extends State { final TextEditingController _linuxIconPathController = TextEditingController(); + final TextEditingController _windowsRawXmlController = + TextEditingController(); + bool _notificationsEnabled = false; @override @@ -1036,6 +1039,35 @@ class _HomePageState extends State { }, ), ], + if (!kIsWeb && Platform.isWindows) ...[ + const Text( + 'Windows-specific examples', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), + child: TextField( + maxLines: 20, + style: const TextStyle(fontFamily: 'RobotoMono'), + controller: _windowsRawXmlController, + decoration: InputDecoration( + hintText: 'Enter the raw xml', + constraints: const BoxConstraints.tightFor( + width: 600, height: 480), + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () => _windowsRawXmlController.clear(), + ), + ), + ), + ), + PaddedElevatedButton( + buttonText: 'Show notification with raw XML', + onPressed: () async { + await _showWindowsNotificationWithRawXml(); + }, + ), + ], ], ), ), @@ -2671,6 +2703,16 @@ class _HomePageState extends State { platformChannelSpecifics, ); } + + Future _showWindowsNotificationWithRawXml() async { + final WindowsNotificationDetails windowsPlatformChannelSpecifics = + WindowsNotificationDetails(rawXml: _windowsRawXmlController.text); + + final NotificationDetails platformChannelSpecifics = + NotificationDetails(windows: windowsPlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show( + id++, 'plain title', 'plain body', platformChannelSpecifics); + } } Future _showLinuxNotificationWithBodyMarkup() async { diff --git a/flutter_local_notifications/lib/flutter_local_notifications.dart b/flutter_local_notifications/lib/flutter_local_notifications.dart index 6766f6db3..d504d10c7 100644 --- a/flutter_local_notifications/lib/flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/flutter_local_notifications.dart @@ -44,5 +44,6 @@ export 'src/platform_specifics/darwin/notification_category_option.dart'; export 'src/platform_specifics/darwin/notification_details.dart'; export 'src/platform_specifics/ios/enums.dart'; export 'src/platform_specifics/windows/initialization_settings.dart'; +export 'src/platform_specifics/windows/notification_details.dart'; export 'src/typedefs.dart'; export 'src/types.dart'; diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index 3897d8d39..34e16f345 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -274,7 +274,9 @@ class FlutterLocalNotificationsPlugin { } else if (defaultTargetPlatform == TargetPlatform.windows) { await resolvePlatformSpecificImplementation< WindowsFlutterLocalNotificationsPlugin>() - ?.show(id, title, body, payload: payload); + ?.show(id, title, body, + notificationDetails: notificationDetails?.windows, + payload: payload); } else { await FlutterLocalNotificationsPlatform.instance.show(id, title, body); } diff --git a/flutter_local_notifications/lib/src/notification_details.dart b/flutter_local_notifications/lib/src/notification_details.dart index feb9b1a79..266196260 100644 --- a/flutter_local_notifications/lib/src/notification_details.dart +++ b/flutter_local_notifications/lib/src/notification_details.dart @@ -2,6 +2,7 @@ import 'package:flutter_local_notifications_linux/flutter_local_notifications_li import 'platform_specifics/android/notification_details.dart'; import 'platform_specifics/darwin/notification_details.dart'; +import 'platform_specifics/windows/notification_details.dart'; /// Contains notification details specific to each platform. class NotificationDetails { @@ -11,6 +12,7 @@ class NotificationDetails { this.iOS, this.macOS, this.linux, + this.windows, }); /// Notification details for Android. @@ -24,4 +26,7 @@ class NotificationDetails { /// Notification details for Linux. final LinuxNotificationDetails? linux; + + /// Notification details for Windows. + final WindowsNotificationDetails? windows; } diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index b78efe8e8..b4d881f21 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -25,6 +25,7 @@ import 'platform_specifics/darwin/notification_details.dart'; import 'platform_specifics/ios/enums.dart'; import 'platform_specifics/windows/initialization_settings.dart'; import 'platform_specifics/windows/method_channel_mappers.dart'; +import 'platform_specifics/windows/notification_details.dart'; import 'type_mappers.dart'; import 'typedefs.dart'; import 'types.dart'; @@ -1035,6 +1036,7 @@ class WindowsFlutterLocalNotificationsPlugin String? body, { String? payload, String? group, + WindowsNotificationDetails? notificationDetails, }) => _channel.invokeMethod('show', { 'id': id, @@ -1042,6 +1044,7 @@ class WindowsFlutterLocalNotificationsPlugin 'body': body, 'group': group, 'payload': payload, + 'platformSpecifics': notificationDetails?.toMap(), }); @override @@ -1053,12 +1056,12 @@ class WindowsFlutterLocalNotificationsPlugin Future _handleMethod(MethodCall call) async { switch (call.method) { - case 'selectNotification': - if (call.arguments is String) { + case 'didReceiveNotificationResponse': + if (call.arguments is Map) { _onDidReceiveNotificationResponse?.call(NotificationResponse( notificationResponseType: NotificationResponseType.selectedNotification, - payload: call.arguments)); + payload: call.arguments['payload'])); } break; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart index d52980b3c..f8ff7990f 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart @@ -1,4 +1,5 @@ import 'initialization_settings.dart'; +import 'notification_details.dart'; /// An extension on [WindowsInitializationSettings] that provides mapping /// to method channel serializable values. @@ -12,3 +13,12 @@ extension WindowsInitializationSettingsMapper on WindowsInitializationSettings { 'iconBgColor': iconBackgroundColor, }; } + +/// An extension on [WindowsNotificationDetails] that provides mapping +/// to method channel serializable values. +extension WindowsNotificationDetailsMapper on WindowsNotificationDetails { + /// Maps [WindowsNotificationDetails] to a [Map]. + Map toMap() => { + 'rawXml': rawXml, + }; +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart index e69de29bb..9bdb4d40e 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart @@ -0,0 +1,9 @@ +/// Contains notification details specific to Windows +class WindowsNotificationDetails { + /// Constructs an instance of [WindowsNotificationDetails]. + const WindowsNotificationDetails({this.rawXml}); + + /// Pass raw XML text to windows api. It will override title and body options. + /// Reference: https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml + final String? rawXml; +} diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 4d1bdd562..ab9fbb57d 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -74,7 +74,8 @@ namespace { const std::optional& title, const std::optional& body, const std::optional& payload, - const std::optional& group); + const std::optional& group, + const std::optional& platformSpecifics); /// /// Dismisses the notification that has the given ID. @@ -144,8 +145,6 @@ namespace { } } else if (method_name == Method::SHOW) { - channel->InvokeMethod("test", nullptr); - const auto args = std::get_if(method_call.arguments()); if (args != nullptr && toastNotifier.has_value()) { const auto id = Utils::GetMapValue("id", args).value(); @@ -153,8 +152,9 @@ namespace { const auto body = Utils::GetMapValue("body", args); const auto payload = Utils::GetMapValue("payload", args); const auto group = Utils::GetMapValue("group", args); + const auto platformSpecifics = Utils::GetMapValue("platformSpecifics", args); - ShowNotification(id, title, body, payload, group); + ShowNotification(id, title, body, payload, group, platformSpecifics); result->Success(); } else { @@ -238,15 +238,21 @@ namespace { const std::optional& title, const std::optional& body, const std::optional& payload, - const std::optional& group + const std::optional& group, + const std::optional& platformSpecifics ) { // obtain a notification template with a title and a body //const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); // find all tags //const auto nodes = doc.GetElementsByTagName(L"text"); + auto rawXml = platformSpecifics.has_value() ? + Utils::GetMapValue("rawXml", &platformSpecifics.value()) : + std::nullopt; + XmlDocument doc; - doc.LoadXml(L"\ + if (!rawXml.has_value()) { + doc.LoadXml(L"\ \ \ \ @@ -254,24 +260,28 @@ namespace { \ "); - const auto bindingNode = doc.SelectSingleNode(L"//binding[1]"); + const auto bindingNode = doc.SelectSingleNode(L"//binding[1]"); - if (title.has_value()) { - // change the text of the first , which will be the title - const auto textNode = doc.CreateElement(L"text"); - textNode.InnerText(winrt::to_hstring(*title)); - bindingNode.AppendChild(textNode); - } - if (body.has_value()) { - // change the text of the second , which will be the body - //nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body.value()))); - const auto textNode = doc.CreateElement(L"text"); - textNode.InnerText(winrt::to_hstring(*body)); - bindingNode.AppendChild(textNode); + if (title.has_value()) { + // change the text of the first , which will be the title + const auto textNode = doc.CreateElement(L"text"); + textNode.InnerText(winrt::to_hstring(*title)); + bindingNode.AppendChild(textNode); + } + if (body.has_value()) { + // change the text of the second , which will be the body + //nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body.value()))); + const auto textNode = doc.CreateElement(L"text"); + textNode.InnerText(winrt::to_hstring(*body)); + bindingNode.AppendChild(textNode); + } + if (payload.has_value()) { + std::cout << "payload: " << *payload << std::endl; + doc.DocumentElement().SetAttribute(L"launch", winrt::to_hstring(*payload)); + } } - if (payload.has_value()) { - std::cout << "payload: " << *payload << std::endl; - doc.DocumentElement().SetAttribute(L"launch", winrt::to_hstring(*payload)); + else { + doc.LoadXml(winrt::to_hstring(rawXml.value())); } std::cout << winrt::to_string(doc.GetXml()) << std::endl; diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h index 0286ff96d..2b0e884e8 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h @@ -11,5 +11,5 @@ namespace Method extern const std::string SHOW; extern const std::string CANCEL; extern const std::string CANCEL_ALL; - extern const std::string SELECT_NOTIFICATION; + extern const std::string DID_RECEIVE_NOTIFICATION_RESPONSE; } diff --git a/flutter_local_notifications/windows/methods.cpp b/flutter_local_notifications/windows/methods.cpp index 295d46885..3749727f2 100644 --- a/flutter_local_notifications/windows/methods.cpp +++ b/flutter_local_notifications/windows/methods.cpp @@ -7,4 +7,4 @@ const std::string Method::SHOW = "show"; const std::string Method::INITIALIZE = "initialize"; const std::string Method::CANCEL = "cancel"; const std::string Method::CANCEL_ALL = "cancelAll"; -const std::string Method::SELECT_NOTIFICATION = "selectNotification"; +const std::string Method::DID_RECEIVE_NOTIFICATION_RESPONSE = "didReceiveNotificationResponse"; diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index 9e9dfc83f..43dd53147 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -28,15 +28,30 @@ struct NotificationActivationCallback : winrt::implementsInvokeMethod(Method::SELECT_NOTIFICATION, std::make_unique(payload), nullptr); + flutter::EncodableMap response; + response[std::string("payload")] = flutter::EncodableValue(payload); + response[std::string("data")] = flutter::EncodableValue(inputData); + channel->InvokeMethod( + Method::DID_RECEIVE_NOTIFICATION_RESPONSE, + std::make_unique(response), + nullptr + ); return S_OK; } catch (...) { diff --git a/flutter_local_notifications/windows/utils/utils.h b/flutter_local_notifications/windows/utils/utils.h index 08766348c..4dadd8ef3 100644 --- a/flutter_local_notifications/windows/utils/utils.h +++ b/flutter_local_notifications/windows/utils/utils.h @@ -17,7 +17,7 @@ namespace Utils { if (pair == m->end()) { return std::nullopt; } - const auto val = pair->second; + const auto &val = pair->second; if (std::holds_alternative(val)) { return std::get(val); } From 585266970c52bf5af7d56b51f41b03055e64b6b0 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 25 Jun 2024 18:36:06 -0400 Subject: [PATCH 024/112] Init error --- .../lib/src/flutter_local_notifications_plugin.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index f6b83dc16..336ba8460 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -193,6 +193,12 @@ class FlutterLocalNotificationsPlugin { onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, ); } else if (defaultTargetPlatform == TargetPlatform.windows) { + if (initializationSettings.windows == null) { + throw ArgumentError( + 'Windows settings must be set when targeting Windows platform.' + ); + } + return await resolvePlatformSpecificImplementation< WindowsFlutterLocalNotificationsPlugin >()?.initialize( From b7be7e952f40bb6d7106150e982c394a6c9b8d5e Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 25 Jun 2024 19:43:18 -0400 Subject: [PATCH 025/112] Catch issues with GUIDs --- .../flutter_local_notifications_plugin.cpp | 6 ++---- .../windows/registration.cpp | 18 ++++++++++++------ .../windows/registration.h | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index ab9fbb57d..e0c39d67c 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -219,7 +219,8 @@ namespace { const std::optional& iconBgColor ) { _aumid = winrt::to_hstring(aumid); - PluginRegistration::RegisterApp(aumid, appName, guid, iconPath, iconBgColor, channel); + auto didRegister = PluginRegistration::RegisterApp(aumid, appName, guid, iconPath, iconBgColor, channel); + if (!didRegister) return false; const auto hasIdentity = HasIdentity(); if (!hasIdentity.has_value()) @@ -276,7 +277,6 @@ namespace { bindingNode.AppendChild(textNode); } if (payload.has_value()) { - std::cout << "payload: " << *payload << std::endl; doc.DocumentElement().SetAttribute(L"launch", winrt::to_hstring(*payload)); } } @@ -284,8 +284,6 @@ namespace { doc.LoadXml(winrt::to_hstring(rawXml.value())); } - std::cout << winrt::to_string(doc.GetXml()) << std::endl; - winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; notif.Tag(winrt::to_hstring(id)); if (group.has_value()) { diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index 43dd53147..d9fb4fe30 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -111,7 +111,7 @@ using RegistryKey = winrt::handle_type; /// /// Updates the Registry to enable notifications. -/// +/// /// Related resources: ///
    ///
  • https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast-other-apps
  • @@ -148,7 +148,7 @@ void UpdateRegistry( // put the following key values under the key // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\PushNotifications\Backup\ - // + // // appType = app:desktop // Setting = s:banner,s:toast,s:audio,c:toast,c:ringing // wnsId = NonImmersivePackage @@ -249,11 +249,17 @@ void UpdateRegistry( /// Register the notificatio activation callback factory /// and the guid of the callback. ///
-void RegisterCallback(std::shared_ptr channel, const std::string& guid) { +bool RegisterCallback(std::shared_ptr channel, const std::string& guid) { DWORD registration{}; const auto factory_ref = winrt::make_self(); const auto factory = factory_ref.get(); + + // The WinRT GUID constructor terminates the app if there's an invalid GUID, so check it here first. + if (guid.size() != 36 || guid[8] != '-' || guid[13] != '-' || guid[18] != '-' || guid[23] != '-') { + return false; + } + winrt::guid rclsid(guid); factory->channel = channel; @@ -263,9 +269,10 @@ void RegisterCallback(std::shared_ptr channel, const std::s CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, ®istration)); + return true; } -void PluginRegistration::RegisterApp( +bool PluginRegistration::RegisterApp( const std::string& aumid, const std::string& appName, const std::string& guid, @@ -273,7 +280,6 @@ void PluginRegistration::RegisterApp( const std::optional& iconBgColor, std::shared_ptr plugin ) { - std::cout << "register app" << std::endl; UpdateRegistry(aumid, appName, guid, iconPath, iconBgColor); - RegisterCallback(plugin, guid); + return RegisterCallback(plugin, guid); } diff --git a/flutter_local_notifications/windows/registration.h b/flutter_local_notifications/windows/registration.h index f2ea4d5fd..f82a0fd2b 100644 --- a/flutter_local_notifications/windows/registration.h +++ b/flutter_local_notifications/windows/registration.h @@ -20,7 +20,7 @@ namespace PluginRegistration { /// An optional path to the icon of the app. /// An optional background color of the icon, in AARRGGBB format. /// The instance of the plugin calling this function - void RegisterApp( + bool RegisterApp( const std::string& aumid, const std::string& appName, const std::string& guid, From 610b01076effa55a1a2e367ecea04e9c18e3324f Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 26 Jun 2024 15:16:10 -0400 Subject: [PATCH 026/112] Support all of the Windows Toast Notification API --- .../example/lib/main.dart | 2 +- .../example/pubspec.yaml | 2 +- .../lib/flutter_local_notifications.dart | 8 + .../platform_flutter_local_notifications.dart | 42 ++++- .../windows/method_channel_mappers.dart | 10 -- .../windows/notification_action.dart | 134 ++++++++++++++ .../windows/notification_audio.dart | 123 +++++++++++++ .../windows/notification_details.dart | 163 +++++++++++++++++- .../windows/notification_group.dart | 37 ++++ .../windows/notification_header.dart | 45 +++++ .../windows/notification_image.dart | 54 ++++++ .../windows/notification_input.dart | 110 ++++++++++++ .../windows/notification_part.dart | 13 ++ .../windows/notification_progress.dart | 39 +++++ .../windows/notification_text.dart | 45 +++++ flutter_local_notifications/pubspec.yaml | 1 + .../windows/registration.cpp | 2 +- 17 files changed, 804 insertions(+), 26 deletions(-) create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/notification_group.dart create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/notification_header.dart create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/notification_part.dart create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart create mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/notification_text.dart diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 3fa35365e..1b8d8b92d 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -2909,7 +2909,7 @@ class _HomePageState extends State { Future _showWindowsNotificationWithRawXml() async { final WindowsNotificationDetails windowsPlatformChannelSpecifics = - WindowsNotificationDetails(rawXml: _windowsRawXmlController.text); + WindowsNotificationDetails.fromXml(_windowsRawXmlController.text); final NotificationDetails platformChannelSpecifics = NotificationDetails(windows: windowsPlatformChannelSpecifics); diff --git a/flutter_local_notifications/example/pubspec.yaml b/flutter_local_notifications/example/pubspec.yaml index 027e68d68..d71a364a1 100644 --- a/flutter_local_notifications/example/pubspec.yaml +++ b/flutter_local_notifications/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: flutter_local_notifications: path: ../ flutter_timezone: ^1.0.4 - http: ^0.13.4 + http: ^1.2.1 image: ^3.0.8 path_provider: ^2.0.0 diff --git a/flutter_local_notifications/lib/flutter_local_notifications.dart b/flutter_local_notifications/lib/flutter_local_notifications.dart index 3871fc406..f1e7df0a2 100644 --- a/flutter_local_notifications/lib/flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/flutter_local_notifications.dart @@ -45,6 +45,14 @@ export 'src/platform_specifics/darwin/notification_details.dart'; export 'src/platform_specifics/darwin/notification_enabled_options.dart'; export 'src/platform_specifics/ios/enums.dart'; export 'src/platform_specifics/windows/initialization_settings.dart'; +export 'src/platform_specifics/windows/notification_action.dart'; +export 'src/platform_specifics/windows/notification_audio.dart'; export 'src/platform_specifics/windows/notification_details.dart'; +export 'src/platform_specifics/windows/notification_group.dart'; +export 'src/platform_specifics/windows/notification_header.dart'; +export 'src/platform_specifics/windows/notification_image.dart'; +export 'src/platform_specifics/windows/notification_input.dart'; +export 'src/platform_specifics/windows/notification_progress.dart'; +export 'src/platform_specifics/windows/notification_text.dart'; export 'src/typedefs.dart'; export 'src/types.dart'; diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 8c120350b..5b7a9e822 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -5,6 +5,7 @@ import 'package:clock/clock.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; import 'package:timezone/timezone.dart'; +import 'package:xml/xml.dart'; import 'callback_dispatcher.dart'; import 'helpers.dart'; @@ -963,15 +964,40 @@ class WindowsFlutterLocalNotificationsPlugin String? payload, String? group, WindowsNotificationDetails? notificationDetails, - }) => - _channel.invokeMethod('show', { - 'id': id, - 'title': title, - 'body': body, - 'group': group, - 'payload': payload, - 'platformSpecifics': notificationDetails?.toMap(), + }) async { + final XmlBuilder builder = XmlBuilder(); + builder.element('toast', + attributes: { + ...notificationDetails?.attributes ?? {}, + if (payload != null) 'launch': payload, + 'useButtonStyle': 'true', + }, + nest: () { + builder.element('visual', nest: () { + builder.element( + 'binding', + attributes: {'template': 'ToastGeneric'}, + nest: () { + builder..element('text', nest: title)..element('text', nest: body); + notificationDetails?.generateBinding(builder); + }, + ); }); + notificationDetails?.toXml(builder); + }); + final String xml = builder + .buildDocument() + .toXmlString(pretty: true, indentAttribute: (_) => true); + // TODO: Remove title, body, and payload from the C++ side + await _channel.invokeMethod('show', { + 'id': id, + 'title': title, + 'body': body, + 'group': group, + 'payload': payload, + 'platformSpecifics': {'rawXml': xml}, + }); + } @override Future cancel(int id, {String? group}) => diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart index f8ff7990f..d52980b3c 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart @@ -1,5 +1,4 @@ import 'initialization_settings.dart'; -import 'notification_details.dart'; /// An extension on [WindowsInitializationSettings] that provides mapping /// to method channel serializable values. @@ -13,12 +12,3 @@ extension WindowsInitializationSettingsMapper on WindowsInitializationSettings { 'iconBgColor': iconBackgroundColor, }; } - -/// An extension on [WindowsNotificationDetails] that provides mapping -/// to method channel serializable values. -extension WindowsNotificationDetailsMapper on WindowsNotificationDetails { - /// Maps [WindowsNotificationDetails] to a [Map]. - Map toMap() => { - 'rawXml': rawXml, - }; -} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart index e69de29bb..ca23d14ac 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart @@ -0,0 +1,134 @@ +import 'package:xml/xml.dart'; + +// NOTE: All enum values in this file have Windows RT-specific names. +// If you change their Dart names, be sure to override [Enum.name]. + +/// Decides how the [WindowsAction] will launch the app. +enum WindowsActivationType { + /// The application will launch in the foreground (the default). + foreground, + /// The application will launch as a background task. + background, + /// Any application can be launched using its protocol. + protocol, +} + +/// Decides how a [WindowsAction] will react to being pressed. +enum WindowsNotificationBehavior { + /// The notification will be dismissed. + dismiss('default'), + /// The notification will remain on screen and show a loading status. + pendingUpdate('pendingUpdate'); + + const WindowsNotificationBehavior(this.name); + /// The Windows API name for this choice. + final String name; +} + +/// Decides how a [WindowsAction] will be styled. +enum WindowsButtonStyle { + /// A green button. + success('Success'), + /// A red button. + critical('Critical'); + + const WindowsButtonStyle(this.name); + /// The Windows API name for this choice. + final String name; +} + +/// Decides how a [WindowsAction] is placed on a notification. +enum WindowsActionPlacement { + /// Instead of a separate button, the action is part of the context menu. + contextMenu, +} + +/// A button in a Windows notification. +/// +/// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action#attributes +class WindowsAction { + /// Constructs a Windows notification button from parameters. + WindowsAction({ + required this.content, + required this.arguments, + this.activationType = WindowsActivationType.foreground, + this.activationBehavior = WindowsNotificationBehavior.dismiss, + this.placement, + this.imageUri, + this.inputId, + this.buttonStyle, + this.tooltip, + }) { + if (imageUri != null && !allowedSchemes.contains(imageUri!.scheme)) { + throw ArgumentError.value( + imageUri.toString(), + 'WindowsNotificationAction.imageUri', + 'URI scheme must be one of the following schemes: $allowedSchemes', + ); + } + } + + /// The set of allowed schemes for [imageUri]. + static const Set allowedSchemes = + {'http', 'https', 'ms-appx', 'ms-appdata', 'file'}; + + /// The body text of the button. + final String content; + + /// An app-defined string that will be passed back if the button is pressed. + final String arguments; + + /// How the application should open if the button is pressed. + /// + /// The default value is [WindowsActivationType.foreground]. + final WindowsActivationType activationType; + + /// How the notification should react when the button is pressed. + /// + /// The default value is [WindowsNotificationBehavior.dismiss]. + final WindowsNotificationBehavior activationBehavior; + + /// How the button should be placed on the notification. + /// + /// Null indicates a regular button. + final WindowsActionPlacement? placement; + + /// A URI of an image to show on the button. + /// + /// Images must be white with a transparent background, and should be + /// 16x16 pixels with no padding. If you provide an image for one button, + /// you must provide images for all your buttons. + /// + /// Supported protocols are: `http`, `https`, `ms-appx`, `ms-appdata:///local`, + /// and `file`. Other protocols will throw an error. + final Uri? imageUri; + + /// The ID of an input box. + /// + /// If provided, this button will be placed next to the specified input. + final String? inputId; + + /// The style of the button. Null indicates a plain button. + final WindowsButtonStyle? buttonStyle; + + /// The tooltip, useful if [content] is empty. + final String? tooltip; + + /// Serializes this notification action as Windows-compatible XML. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action#syntax + void toXml(XmlBuilder builder) => builder.element( + 'action', + attributes: { + 'content': content, + 'arguments': arguments, + 'activationType': activationType.name, + 'afterActivationBehavior': activationBehavior.name, + if (placement != null) 'placement': placement!.name, + if (imageUri != null) 'imageUri': imageUri!.toString(), + if (inputId != null) 'hint-inputId': inputId!, + if (buttonStyle != null) 'hint-buttonStyle': buttonStyle!.name, + if (tooltip != null) 'hint-toolTip': tooltip!, + }, + ); +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart new file mode 100644 index 000000000..b946f3f52 --- /dev/null +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart @@ -0,0 +1,123 @@ +import 'package:xml/xml.dart'; + +extension on Uri { + String get filename => pathSegments.last; + String get extension => pathSegments.last.split('.').last; +} + +/// A preset sound for a Windows notification. +enum WindowsNotificationSound { + /// The default sound. + defaultSound('ms-winsoundevent:Notification.Default'), + /// The IM sound. + im('ms-winsoundevent:Notification.IM'), + /// The Mail sound. + mail('ms-winsoundevent:Notification.Mail'), + /// The Reminder sound. + reminder('ms-winsoundevent:Notification.Reminder'), + /// The SMS sound. + sms('ms-winsoundevent:Notification.SMS'), + /// Alarm sound 1. + alarm1('ms-winsoundevent:Notification.Looping.Alarm1'), + /// Alarm sound 2. + alarm2('ms-winsoundevent:Notification.Looping.Alarm2'), + /// Alarm sound 3. + alarm3('ms-winsoundevent:Notification.Looping.Alarm3'), + /// Alarm sound 4. + alarm4('ms-winsoundevent:Notification.Looping.Alarm4'), + /// Alarm sound 5. + alarm5('ms-winsoundevent:Notification.Looping.Alarm5'), + /// Alarm sound 6. + alarm6('ms-winsoundevent:Notification.Looping.Alarm6'), + /// Alarm sound 7. + alarm7('ms-winsoundevent:Notification.Looping.Alarm7'), + /// Alarm sound 8. + alarm8('ms-winsoundevent:Notification.Looping.Alarm8'), + /// Alarm sound 9. + alarm9('ms-winsoundevent:Notification.Looping.Alarm9'), + /// Alarm sound 10. + alarm10('ms-winsoundevent:Notification.Looping.Alarm10'), + /// Call sound 1. + call1('ms-winsoundevent:Notification.Looping.Call1'), + /// Call sound 2. + call2('ms-winsoundevent:Notification.Looping.Call2'), + /// Call sound 3. + call3('ms-winsoundevent:Notification.Looping.Call3'), + /// Call sound 4. + call4('ms-winsoundevent:Notification.Looping.Call4'), + /// Call sound 5. + call5('ms-winsoundevent:Notification.Looping.Call5'), + /// Call sound 6. + call6('ms-winsoundevent:Notification.Looping.Call6'), + /// Call sound 7. + call7('ms-winsoundevent:Notification.Looping.Call7'), + /// Call sound 8. + call8('ms-winsoundevent:Notification.Looping.Call8'), + /// Call sound 9. + call9('ms-winsoundevent:Notification.Looping.Call9'), + /// Call sound 10. + call10('ms-winsoundevent:Notification.Looping.Call10'); + + const WindowsNotificationSound(this.name); + /// The Windows API name for this sound. + final String name; +} + +/// Specifies custom audio to play during a notification. +class WindowsNotificationAudio { + /// Audio from a Windows preset. See [WindowsNotificationSound] for options. + WindowsNotificationAudio.preset({ + required WindowsNotificationSound sound, + this.shouldLoop = false, + this.isSilent = false, + }) : source = sound.name; + + /// Audio from a file. See [allowedSchemes] and [allowedExtensions]. + WindowsNotificationAudio.fromFile({ + required Uri file, + this.shouldLoop = false, + this.isSilent = false, + }) : source = file.toString() { + if (!allowedSchemes.contains(file.scheme)) { + throw ArgumentError.value( + file.toString(), + 'WindowsNotificationAudio.file', + 'URI scheme must be one of the following schemes: $allowedSchemes', + ); + } + if ( + !file.filename.contains('.') || + !allowedExtensions.contains(file.extension) + ) { + throw ArgumentError.value( + file.toString(), + 'WindowsNotificationAudio.file', + 'File extension must be one of the following: $allowedExtensions', + ); + } + } + + /// Allowed Uri schemes for [WindowsNotificationAudio.fromFile]. + static const Set allowedSchemes = {'ms-appx', 'ms-resource'}; + + /// Allowed file extensions for [WindowsNotificationAudio.fromFile]. + static const Set allowedExtensions = + {'.aac', '.flac', '.m4a', '.mp3', '.wav', '.wma'}; + + /// Whether this audio should loop. + final bool shouldLoop; + /// Whether this notification should be silent. + final bool isSilent; + /// The source of the audio. + final String source; + + /// Serializes this audio to Windows-compatible XML. + void toXml(XmlBuilder builder) => builder.element( + 'audio', + attributes: { + 'src': source, + 'silent': isSilent.toString(), + 'loop': shouldLoop.toString(), + }, + ); +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart index 9bdb4d40e..bce806502 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart @@ -1,9 +1,162 @@ -/// Contains notification details specific to Windows +import 'package:xml/xml.dart'; + +import 'notification_action.dart'; +import 'notification_audio.dart'; +import 'notification_group.dart'; +import 'notification_image.dart'; +import 'notification_input.dart'; +import 'notification_progress.dart'; + +/// The duration for a Windows notification. +enum WindowsNotificationDuration { + /// The notification will stay for a long time. + long, + /// The notification will stay for a short time. + short, +} + +/// The scenario a notification is being used for. +enum WindowsNotificationScenario { + /// Reminders are expanded and remain until manually dismissed. + /// + /// This will be ignored unless the notification also has at least one + /// [WindowsAction] that activates a background task. + reminder, + + /// Alarms are expanded and remain until manually dismissed. + /// + /// By default, alarm notifications loop the standard "alarm" sound. + alarm, + + /// Calls are expanded and show in a special format. + /// + /// By default, call notifications loop the standard "call" sound. + incomingCall, + + /// Urgent notifications can break through Do Not Disturb settings. + urgent, +} + +/// Contains notification details specific to Windows. +/// +/// See: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts class WindowsNotificationDetails { - /// Constructs an instance of [WindowsNotificationDetails]. - const WindowsNotificationDetails({this.rawXml}); + /// Creates a Windows notification from the given options. + WindowsNotificationDetails({ + this.actions = const [], + this.inputs = const [], + this.images = const [], + this.groups = const [], + this.progressBars = const [], + this.audio, + this.duration, + this.scenario, + this.timestamp, + this.subtitle, + }) : rawXml = null { + if (actions.length > 5) { + throw ArgumentError( + 'WindowsNotificationDetails can only have up to 5 actions', + ); + } + if (inputs.length > 5) { + throw ArgumentError( + 'WindowsNotificationDetails can only have up to 5 inputs', + ); + } + } - /// Pass raw XML text to windows api. It will override title and body options. - /// Reference: https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml + /// Passes the raw XML to the Windows API directly. + /// + /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. + /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). + const WindowsNotificationDetails.fromXml(this.rawXml) : + actions = const [], + inputs = const [], + images = const [], + groups = const [], + progressBars = const [], + audio = null, + timestamp = null, + duration = null, + scenario = null, + subtitle = null; + + /// The raw XML passed to the Windows API. + /// + /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. + /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). final String? rawXml; + + /// A list of at most five action buttons. + final List actions; + + /// A list of at most five input elements. + final List inputs; + + /// A custom audio to play during this notification. + final WindowsNotificationAudio? audio; + + /// The duration for this notification. + final WindowsNotificationDuration? duration; + + /// The scenario for this notification. Sets some defaults based on the value. + final WindowsNotificationScenario? scenario; + + /// Overrides the timestamp to show on the notification. + final DateTime? timestamp; + + /// A third line to show under the notification body. + final String? subtitle; + + /// A list of images to show. + final List images; + + /// A list of groups to show. + final List groups; + + /// A list of progress bars to show. + final List progressBars; + + /// XML attributes for the toast notification as a whole. + Map get attributes => { + if (duration != null) 'duration': duration!.name, + if (timestamp != null) 'displayTimestamp': timestamp!.toIso8601String(), + if (scenario != null) 'scenario': scenario!.name, + }; + + /// Builds all relevant XML parts under the root `` element. + void toXml(XmlBuilder builder) { + if (rawXml != null) { + builder.xml(rawXml!); + return; + } + builder.element('actions', nest: () { + for (final WindowsInput input in inputs) { + input.toXml(builder); + } + for (final WindowsAction action in actions) { + action.toXml(builder); + } + }); + audio?.toXml(builder); + } + + /// Generates the `` element of the notification. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-binding + void generateBinding(XmlBuilder builder) { + if (subtitle != null) { + builder.element('text', nest: subtitle); + } + for (final WindowsImage image in images) { + image.toXml(builder); + } + for (final WindowsGroup group in groups) { + group.toXml(builder); + } + for (final WindowsProgressBar progressBar in progressBars) { + progressBar.toXml(builder); + } + } } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_group.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_group.dart new file mode 100644 index 000000000..432b5482b --- /dev/null +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_group.dart @@ -0,0 +1,37 @@ +import 'package:xml/xml.dart'; + +import 'notification_part.dart'; + +/// A group of notification content that must be displayed as a whole. +/// +/// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-group +class WindowsGroup { + /// Makes a group of multiple columns. + const WindowsGroup(this.columns); + + /// The different columns being grouped together. + final List columns; + + /// Serializes this group to XML. + void toXml(XmlBuilder builder) => builder.element( + 'group', + nest: () { + for (final WindowsColumn column in columns) { + builder.element('subgroup', nest: () { + for (final WindowsNotificationPart part in column.parts) { + part.toXml(builder); + } + }); + } + } + ); +} + +/// A vertical column of text and images in a Windows notification. +class WindowsColumn { + /// A const constructor. + const WindowsColumn(this.parts); + + /// A list of text or images in this column. + final List parts; +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_header.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_header.dart new file mode 100644 index 000000000..f61b0cfa7 --- /dev/null +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_header.dart @@ -0,0 +1,45 @@ +import 'package:xml/xml.dart'; + +/// Decides how the application will open when the header is pressed. +enum WindowsHeaderActivation { + /// Opens the app in the foreground. + foreground, + + /// Opens any app using a custom protocol. + protocol, +} + +/// A header that groups multiple Windows notifications. +class WindowsHeader { + /// Creates a Windows header. + const WindowsHeader({ + required this.id, + required this.title, + required this.arguments, + this.activation, + }); + + /// A unique ID for this header. + final String id; + + /// The title of the header. + final String title; + + /// An application-defined payload that will be passed back when pressed. + final String arguments; + + /// Specifies how the application will open. + final WindowsHeaderActivation? activation; + + + /// Serializes this header to XML. + void toXml(XmlBuilder builder) => builder.element( + 'header', + attributes: { + 'id': id, + 'title': title, + 'arguments': arguments, + if (activation != null) 'activationType': activation!.name, + }, + ); +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart new file mode 100644 index 000000000..5263bc5d3 --- /dev/null +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import 'package:xml/xml.dart'; + +import 'notification_part.dart'; + +/// Where a Windows notification image can be placed. +enum WindowsImagePlacement { + /// The image replaces the app logo. + appLogoOverride, + /// The image is shown on top of the notification body. + hero, +} + +/// How a Windows notification image can be cropped. +enum WindowsImageCrop { + /// The image is cropped into a circle. + circle, +} + +/// An image in a Windows notification. +class WindowsImage extends WindowsNotificationPart { + /// Creates a Windows notification image. + const WindowsImage({ + required this.source, + required this.altText, + this.addQueryParams = false, + this.placement, + this.crop, + }); + + /// Whether Windows should add URL query parameters when fetching the image. + final bool addQueryParams; + /// A description of the image to be used by assistive technology. + final String altText; + /// The source of the image. + final File source; + /// Where this image will be placed. Null indicates below the notification. + final WindowsImagePlacement? placement; + /// How the image will be cropped. Null indicates uncropped. + final WindowsImageCrop? crop; + + @override + void toXml(XmlBuilder builder) => builder.element( + 'image', + attributes: { + 'src': Uri.file(source.path, windows: true).toString(), + 'alt': altText, + 'addImageQuery': addQueryParams.toString(), + if (placement != null) 'placement': placement!.name, + if (crop != null) 'hint-crop': crop!.name, + }, + ); +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart new file mode 100644 index 000000000..053fd1408 --- /dev/null +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart @@ -0,0 +1,110 @@ +import 'package:xml/xml.dart'; + +/// The type of a [WindowsInput]. +enum WindowsInputType { + /// A text input. + text, + /// A multiple choice input. + selection, +} + +/// A text or multiple choice input element in a Windows notification. +abstract class WindowsInput { + /// Creates an input field in a notification. + const WindowsInput({ + required this.id, + required this.type, + this.title, + }); + + /// A unique ID for this input. + /// + /// Can be used by buttons to be placed next to this input. + final String id; + + /// The type of this input. + final WindowsInputType type; + + /// The title of this input. + final String? title; + + /// Serializes this input to XML. + void toXml(XmlBuilder builder); +} + +/// A text input. +class WindowsTextInput extends WindowsInput { + /// Creates an input field in a notification. + const WindowsTextInput({ + required super.id, + this.hintText, + super.title, + }) : super(type: WindowsInputType.text); + + /// The hint text. + final String? hintText; + + @override + void toXml(XmlBuilder builder) => builder.element( + 'input', + attributes: { + 'id': id, + 'type': type.name, + if (title != null) 'title': title!, + if (hintText != null) 'placeHolderContent': hintText!, + }, + ); +} + +/// A multiple choice input. +class WindowsSelectionInput extends WindowsInput { + /// Creates a selection input. + const WindowsSelectionInput({ + required super.id, + required this.items, + super.title, + }) : super(type: WindowsInputType.selection); + + /// The items that can be selected. + final List items; + + @override + void toXml(XmlBuilder builder) => builder.element( + 'input', + attributes: { + 'id': id, + 'type': type.name, + if (title != null) 'title': title!, + }, + nest: () { + for (final WindowsSelection item in items) { + item.toXml(builder); + } + } + ); + +} + +/// An option that can be selected by a [WindowsSelectionInput]. +class WindowsSelection { + /// Creates a selectable choice. + const WindowsSelection({ + required this.id, + required this.content, + }); + + /// A unique ID for this item. + final String id; + + /// The content of this item in the UI. + final String content; + + /// Serializes this item to XML. + void toXml(XmlBuilder builder) => builder.element( + 'selection', + attributes: { + 'id': id, + 'content': content, + }, + ); +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_part.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_part.dart new file mode 100644 index 000000000..d53fb035f --- /dev/null +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_part.dart @@ -0,0 +1,13 @@ +import 'package:xml/xml.dart'; + +/// A text or image element in a Windows notification. +/// +/// Note: This should not be used for anything else as notification +/// groups can only contain text and images. +abstract class WindowsNotificationPart { + /// A const constructor. + const WindowsNotificationPart(); + + /// Serializes this part to XML, according to the Windows API. + void toXml(XmlBuilder builder); +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart new file mode 100644 index 000000000..522bc1ddd --- /dev/null +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart @@ -0,0 +1,39 @@ +import 'package:xml/xml.dart'; + +/// A progress bar in a Windows notification. +class WindowsProgressBar { + /// Creates a progress bar for a Windows notification. + WindowsProgressBar({ + required this.status, + required this.value, + this.title, + this.percentageOverride, + }); + + /// An optional title. + final String? title; + + /// Describes what's happening, like `Downloading...` or `Installing...` + final String status; + + /// The value of the progress, from 0.0 to 1.0. + /// + /// Setting this to null indicates a indeterminate progress bar. + final double? value; + + /// Overrides the default reading as a percent with a different text. + /// + /// Useful for indicating discrete progress, like `3/10` instead of `30%`. + final String? percentageOverride; + + /// Serializes this progress bar to XML. + void toXml(XmlBuilder builder) => builder.element( + 'progress', + attributes: { + 'status': status, + 'value': value?.toString() ?? 'indeterminate', + if (title != null) 'title': title!, + if (percentageOverride != null) 'valueStringOverride': percentageOverride! + } + ); +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_text.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_text.dart new file mode 100644 index 000000000..693e8e6a3 --- /dev/null +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_text.dart @@ -0,0 +1,45 @@ +import 'package:xml/xml.dart'; + +import 'notification_part.dart'; + +/// Where text can be placed in a Windows notification. +enum WindowsTextPlacement { + /// Shown at the bottom of the notification body in smaller text. + attribution, +} + +/// Text in a Windows notification. +/// +/// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-text +class WindowsNotificationText extends WindowsNotificationPart { + /// Creates text for a Windows notification. + const WindowsNotificationText({ + required this.text, + this.centerIfCall = false, + this.placement, + this.languageCode, + }); + + /// The text being displayed. + final String text; + + /// Whether to center this text. Only relevant if in an incoming call. + final bool centerIfCall; + + /// The placement of this text. Null indicates default. + final WindowsTextPlacement? placement; + + /// The language of this text. + final String? languageCode; + + @override + void toXml(XmlBuilder builder) => builder.element( + 'text', + attributes: { + if (languageCode != null) 'lang': languageCode!, + if (placement != null) 'placement': placement!.name, + 'hint-callScenarioCenterAlign': centerIfCall.toString(), + }, + nest: text, + ); +} diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index a8b392a44..e202e089d 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: flutter_local_notifications_linux: ^4.0.0 flutter_local_notifications_platform_interface: ^7.1.0 timezone: ^0.9.0 + xml: ^6.5.0 dev_dependencies: flutter_driver: diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index d9fb4fe30..62f00467d 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -257,7 +257,7 @@ bool RegisterCallback(std::shared_ptr channel, const std::s // The WinRT GUID constructor terminates the app if there's an invalid GUID, so check it here first. if (guid.size() != 36 || guid[8] != '-' || guid[13] != '-' || guid[18] != '-' || guid[23] != '-') { - return false; + throw std::invalid_argument("Invalid GUID"); } winrt::guid rclsid(guid); From 6fda1b4685db1918dce5c7f2b616bab5918fdc2c Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 26 Jun 2024 15:52:48 -0400 Subject: [PATCH 027/112] Send user input back to plugin --- .../lib/src/platform_flutter_local_notifications.dart | 9 +++++---- flutter_local_notifications/pubspec.yaml | 4 ++++ flutter_local_notifications/windows/registration.cpp | 8 -------- .../lib/src/types.dart | 7 +++++++ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 5b7a9e822..453803d10 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -988,7 +988,6 @@ class WindowsFlutterLocalNotificationsPlugin final String xml = builder .buildDocument() .toXmlString(pretty: true, indentAttribute: (_) => true); - // TODO: Remove title, body, and payload from the C++ side await _channel.invokeMethod('show', { 'id': id, 'title': title, @@ -1011,9 +1010,11 @@ class WindowsFlutterLocalNotificationsPlugin case 'didReceiveNotificationResponse': if (call.arguments is Map) { _onDidReceiveNotificationResponse?.call(NotificationResponse( - notificationResponseType: - NotificationResponseType.selectedNotification, - payload: call.arguments['payload'])); + notificationResponseType: + NotificationResponseType.selectedNotification, + payload: call.arguments['payload'], + data: Map.from(call.arguments['data']), + )); } break; } diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index e202e089d..0f7f97c61 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -41,3 +41,7 @@ flutter: environment: sdk: ">=2.17.0 <4.0.0" flutter: ">=3.0.0" + +dependency_overrides: + flutter_local_notifications_platform_interface: + path: ../flutter_local_notifications_platform_interface diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index 62f00467d..71f5f957d 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -32,15 +32,9 @@ struct NotificationActivationCallback : winrt::implements{}, }); /// The notification's id. @@ -106,11 +107,17 @@ class NotificationResponse { /// The value of the input field if the notification action had an input /// field. + /// + /// On Windows, this is always null. Instead, [data] holds the values of + /// each input with the input's ID as the key. final String? input; /// The notification's payload. final String? payload; + /// Any other data returned by the platform. + final Map data; + /// The notification response type. final NotificationResponseType notificationResponseType; } From 8692daa863dbcb35fe0fa09b9ce21aa099973f91 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 26 Jun 2024 15:54:19 -0400 Subject: [PATCH 028/112] Fixed typos --- .../platform_flutter_local_notifications.dart | 18 +++++++++--------- flutter_local_notifications/pubspec.yaml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 453803d10..caa9110ef 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -112,7 +112,7 @@ class MethodChannelFlutterLocalNotificationsPlugin /// Android implementation of the local notifications plugin. class AndroidFlutterLocalNotificationsPlugin extends MethodChannelFlutterLocalNotificationsPlugin { - DidReceiveNotificationResponseCallback? _ondidReceiveNotificationResponse; + DidReceiveNotificationResponseCallback? _onDidReceiveNotificationResponse; /// Initializes the plugin. /// @@ -136,7 +136,7 @@ class AndroidFlutterLocalNotificationsPlugin DidReceiveBackgroundNotificationResponseCallback? onDidReceiveBackgroundNotificationResponse, }) async { - _ondidReceiveNotificationResponse = onDidReceiveNotificationResponse; + _onDidReceiveNotificationResponse = onDidReceiveNotificationResponse; _channel.setMethodCallHandler(_handleMethod); final Map arguments = initializationSettings.toMap(); @@ -215,7 +215,7 @@ class AndroidFlutterLocalNotificationsPlugin /// a foreground service with a notification id of 0. /// /// Since not all users of this plugin need such a service, it was not - /// added to this plugins Android manifest. Thie means you have to add + /// added to this plugins Android manifest. This means you have to add /// it if you want to use the foreground service functionality. Add the /// foreground service permission to your apps `AndroidManifest.xml` like /// described in the [official Android documentation](https://developer.android.com/guide/components/foreground-services#request-foreground-service-permissions): @@ -242,11 +242,11 @@ class AndroidFlutterLocalNotificationsPlugin /// The notification of the foreground service can be updated by /// simply calling this method multiple times. /// - /// Information on selecting an appropriate `startType` for your app's usecase - /// should be taken from the official Android documentation, check [`Service.onStartCommand`](https://developer.android.com/reference/android/app/Service#onStartCommand(android.content.Intent,%20int,%20int)). + /// Information on selecting an appropriate `startType` for your app's use + /// case should be taken from the official Android documentation, check [`Service.onStartCommand`](https://developer.android.com/reference/android/app/Service#onStartCommand(android.content.Intent,%20int,%20int)). /// The there mentioned constants can be found in [AndroidServiceStartType]. /// - /// The notification for the foreground service will not be dismissable + /// The notification for the foreground service will not be dismissible /// and automatically removed when using [stopForegroundService]. /// /// `foregroundServiceType` is a set of foreground service types to apply to @@ -541,7 +541,7 @@ class AndroidFlutterLocalNotificationsPlugin Future _handleMethod(MethodCall call) async { switch (call.method) { case 'didReceiveNotificationResponse': - _ondidReceiveNotificationResponse?.call( + _onDidReceiveNotificationResponse?.call( NotificationResponse( id: call.arguments['notificationId'], actionId: call.arguments['actionId'], @@ -568,7 +568,7 @@ class IOSFlutterLocalNotificationsPlugin /// /// Call this method on application before using the plugin further. /// - /// Initialisation may also request notification permissions where users will + /// Initialization may also request notification permissions where users will /// see a permissions prompt. This may be fine in cases where it's acceptable /// to do this when the application runs for the first time. However, if your /// application needs to do this at a later point in time, set the @@ -776,7 +776,7 @@ class MacOSFlutterLocalNotificationsPlugin /// Call this method on application before using the plugin further. /// This should only be done once. /// - /// Initialisation may also request notification permissions where users will + /// Initialization may also request notification permissions where users will /// see a permissions prompt. This may be fine in cases where it's acceptable /// to do this when the application runs for the first time. However, if your /// application needs to do this at a later point in time, set the diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index 0f7f97c61..0af3f347a 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_local_notifications description: A cross platform plugin for displaying and scheduling local - notifications for Flutter applications with the ability to customise for each + notifications for Flutter applications with the ability to customize for each platform. version: 17.1.2 homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications From 4e043fc5741f69ba03762ae395330d717d992452 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 26 Jun 2024 18:18:55 -0400 Subject: [PATCH 029/112] Added getActiveNotifications for Windows --- .../example/lib/main.dart | 1 + .../example/windows/flutter/CMakeLists.txt | 7 +++- .../example/windows/runner/Runner.rc | 10 +++--- .../flutter_local_notifications_plugin.dart | 4 +++ .../windows/notification_audio.dart | 2 +- .../flutter_local_notifications_plugin.cpp | 36 +++++++++++++++++++ .../flutter_local_notifications/methods.h | 1 + .../windows/methods.cpp | 1 + 8 files changed, 55 insertions(+), 7 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 1b8d8b92d..3a268bc82 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -1444,6 +1444,7 @@ class _HomePageState extends State { } Future _zonedScheduleNotification() async { + tz.initializeTimeZones(); await flutterLocalNotificationsPlugin.zonedSchedule( 0, 'scheduled title', diff --git a/flutter_local_notifications/example/windows/flutter/CMakeLists.txt b/flutter_local_notifications/example/windows/flutter/CMakeLists.txt index b2e4bd8d6..4f2af69bb 100644 --- a/flutter_local_notifications/example/windows/flutter/CMakeLists.txt +++ b/flutter_local_notifications/example/windows/flutter/CMakeLists.txt @@ -9,6 +9,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +96,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/flutter_local_notifications/example/windows/runner/Runner.rc b/flutter_local_notifications/example/windows/runner/Runner.rc index 81d27749b..f377c3592 100644 --- a/flutter_local_notifications/example/windows/runner/Runner.rc +++ b/flutter_local_notifications/example/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index 54d53991d..6ab476246 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -492,6 +492,10 @@ class FlutterLocalNotificationsPlugin { /// - macOS: macOS 10.14 or newer /// /// On Linux it will throw an [UnimplementedError]. + /// + /// On Windows, your application must be packaged as an MSIX to be able + /// to use this API. If not, this function will return an empty list. + /// For more details, see: https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/modernize-wpf-tutorial-5 Future> getActiveNotifications() => FlutterLocalNotificationsPlatform.instance.getActiveNotifications(); } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart index b946f3f52..850752580 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart @@ -102,7 +102,7 @@ class WindowsNotificationAudio { /// Allowed file extensions for [WindowsNotificationAudio.fromFile]. static const Set allowedExtensions = - {'.aac', '.flac', '.m4a', '.mp3', '.wav', '.wma'}; + {'aac', 'flac', 'm4a', 'mp3', 'wav', 'wma'}; /// Whether this audio should loop. final bool shouldLoop; diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index e0c39d67c..15402e873 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -10,9 +10,12 @@ #include #include #include +#include #include #include #include +#include +#include // For getPlatformVersion; remove unless needed for your plugin implementation. #include @@ -89,6 +92,8 @@ namespace { ///
void CancelAllNotifications(); + void GetActiveNotifications(std::vector& result); + std::optional HasIdentity(); }; @@ -178,6 +183,22 @@ namespace { CancelAllNotifications(); result->Success(); } + else if (method_name == Method::GET_ACTIVE_NOTIFICATIONS && toastNotifier.has_value()) { + try { + std::vector vec; + GetActiveNotifications(vec); + result->Success(flutter::EncodableValue(vec)); + } catch (std::exception error) { + result->Error("INTERNAL", error.what()); + } catch (winrt::hresult_error error) { + // Windows apps need to be in an MSIX to use this API. + // Return an empty list if that's the case + std::vector vec; + result->Success(vec); + } catch (...) { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + } + } else { result->NotImplemented(); } @@ -315,6 +336,21 @@ namespace { } toastNotificationHistory.value().Clear(_aumid); } + + void FlutterLocalNotificationsPlugin::GetActiveNotifications(std::vector& result) { + if (!toastNotificationHistory.has_value()) { + toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); + } + const auto history = toastNotificationHistory.value().GetHistory(); + const uint32_t size = history.Size(); + for (uint32_t i = 0; i < size; i++) { + flutter::EncodableMap data; + const auto notif = history.GetAt(i); + const auto tag = notif.Tag(); + data["id"] = flutter::EncodableValue(tag.c_str()); + result.emplace_back(flutter::EncodableValue(data)); + } + } } void FlutterLocalNotificationsPluginRegisterWithRegistrar( diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h index 2b0e884e8..945f72bb5 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h @@ -12,4 +12,5 @@ namespace Method extern const std::string CANCEL; extern const std::string CANCEL_ALL; extern const std::string DID_RECEIVE_NOTIFICATION_RESPONSE; + extern const std::string GET_ACTIVE_NOTIFICATIONS; } diff --git a/flutter_local_notifications/windows/methods.cpp b/flutter_local_notifications/windows/methods.cpp index 3749727f2..1675392ab 100644 --- a/flutter_local_notifications/windows/methods.cpp +++ b/flutter_local_notifications/windows/methods.cpp @@ -8,3 +8,4 @@ const std::string Method::INITIALIZE = "initialize"; const std::string Method::CANCEL = "cancel"; const std::string Method::CANCEL_ALL = "cancelAll"; const std::string Method::DID_RECEIVE_NOTIFICATION_RESPONSE = "didReceiveNotificationResponse"; +const std::string Method::GET_ACTIVE_NOTIFICATIONS = "getActiveNotifications"; From 8674b685218fa1df260b62b296f3490a92f193e0 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 27 Jun 2024 15:04:04 -0400 Subject: [PATCH 030/112] All examples working! --- .../example/lib/main.dart | 406 ++++++++++++++++-- .../flutter_local_notifications_plugin.dart | 12 + .../platform_flutter_local_notifications.dart | 105 +++-- .../windows/notification_audio.dart | 6 + .../windows/notification_details.dart | 44 +- .../windows/notification_group.dart | 14 +- .../windows/notification_header.dart | 1 - .../windows/notification_image.dart | 6 +- .../windows/notification_input.dart | 4 + .../windows/notification_text.dart | 6 + .../flutter_local_notifications_plugin.cpp | 158 ++++--- .../flutter_local_notifications/methods.h | 2 + .../windows/methods.cpp | 2 + 13 files changed, 613 insertions(+), 153 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 3a268bc82..1243558c7 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -27,8 +27,8 @@ final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = final StreamController didReceiveLocalNotificationStream = StreamController.broadcast(); -final StreamController selectNotificationStream = - StreamController.broadcast(); +final StreamController selectNotificationStream = + StreamController.broadcast(); const MethodChannel platform = MethodChannel('dexterx.dev/flutter_local_notifications_example'); @@ -41,12 +41,14 @@ class ReceivedNotification { required this.title, required this.body, required this.payload, + this.data, }); final int id; final String? title; final String? body; final String? payload; + final Map? data; } String? selectedNotificationPayload; @@ -155,14 +157,12 @@ Future main() async { requestSoundPermission: false, onDidReceiveLocalNotification: (int id, String? title, String? body, String? payload) async { - didReceiveLocalNotificationStream.add( - ReceivedNotification( + didReceiveLocalNotificationStream.add(ReceivedNotification( id: id, title: title, body: body, payload: payload, - ), - ); + )); }, notificationCategories: darwinNotificationCategories, ); @@ -190,11 +190,11 @@ Future main() async { (NotificationResponse notificationResponse) { switch (notificationResponse.notificationResponseType) { case NotificationResponseType.selectedNotification: - selectNotificationStream.add(notificationResponse.payload); + selectNotificationStream.add(notificationResponse); break; case NotificationResponseType.selectedNotificationAction: if (notificationResponse.actionId == navigationActionId) { - selectNotificationStream.add(notificationResponse.payload); + selectNotificationStream.add(notificationResponse); } break; } @@ -213,10 +213,13 @@ Future main() async { } Future _configureLocalTimeZone() async { - if (kIsWeb || Platform.isLinux || Platform.isWindows) { + if (kIsWeb || Platform.isLinux) { return; } tz.initializeTimeZones(); + if (Platform.isWindows) { + return; + } final String? timeZoneName = await FlutterTimezone.getLocalTimezone(); tz.setLocalLocation(tz.getLocation(timeZoneName!)); } @@ -341,7 +344,7 @@ class _HomePageState extends State { await Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) => - SecondPage(receivedNotification.payload), + SecondPage(receivedNotification.payload, data: receivedNotification.data), ), ); }, @@ -354,9 +357,9 @@ class _HomePageState extends State { } void _configureSelectNotificationSubject() { - selectNotificationStream.stream.listen((String? payload) async { + selectNotificationStream.stream.listen((NotificationResponse? response) async { await Navigator.of(context).push(MaterialPageRoute( - builder: (BuildContext context) => SecondPage(payload), + builder: (BuildContext context) => SecondPage(response?.payload, data: response?.data), )); }); } @@ -1093,23 +1096,81 @@ class _HomePageState extends State { 'Windows-specific examples', style: TextStyle(fontWeight: FontWeight.bold), ), - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), - child: TextField( - maxLines: 20, - style: const TextStyle(fontFamily: 'RobotoMono'), - controller: _windowsRawXmlController, - decoration: InputDecoration( - hintText: 'Enter the raw xml', - constraints: const BoxConstraints.tightFor( - width: 600, height: 480), - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () => _windowsRawXmlController.clear(), + PaddedElevatedButton( + buttonText: 'Show short and long notifications notification', + onPressed: () async { + await _showWindowsNotificationWithDuration(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show different scenarios', + onPressed: () async { + await _showWindowsNotificationWithScenarios(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with some detail', + onPressed: () async { + await _showWindowsNotificationWithDetails(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with image', + onPressed: () async { + await _showWindowsNotificationWithImages(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with groups', + onPressed: () async { + await _showWindowsNotificationWithGroups(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with progress bar', + onPressed: () async { + await _showWindowsNotificationWithProgress(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notitification with activation', + onPressed: () async { + await _showWindowsNotificationWithActivation(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notitification with button styles', + onPressed: () async { + await _showWindowsNotificationWithButtonStyle(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notitifications in a group', + onPressed: () async { + await _showWindowsNotificationWithHeader(); + }, + ), + SizedBox( + width: 500, + child: ExpansionTile( + title: const Text('Click to expand raw XML'), + children: [TextField( + maxLines: 20, + style: const TextStyle(fontFamily: 'RobotoMono'), + controller: _windowsRawXmlController, + decoration: InputDecoration( + hintText: 'Enter the raw xml', + constraints: const BoxConstraints.tightFor( + width: 600, height: 480), + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () => _windowsRawXmlController.clear(), + ), ), - ), + ),] ), ), + const SizedBox(height: 8), PaddedElevatedButton( buttonText: 'Show notification with raw XML', onPressed: () async { @@ -1196,11 +1257,33 @@ class _HomePageState extends State { ], ); - const NotificationDetails notificationDetails = NotificationDetails( + final WindowsNotificationDetails windowsNotificationsDetails = + WindowsNotificationDetails( + subtitle: 'Click the three dots for another button', + actions: [ + WindowsAction( + content: 'Text', + arguments: 'text', + ), + WindowsAction( + content: 'Image', + arguments: 'image', + imageUri: Uri.file(File('icons/coworker.png').absolute.path, windows: true), + ), + WindowsAction( + content: 'Context', + arguments: 'context', + placement: WindowsActionPlacement.contextMenu, + ), + ], + ); + + final NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: iosNotificationDetails, macOS: macOSNotificationDetails, linux: linuxNotificationDetails, + windows: windowsNotificationsDetails, ); await flutterLocalNotificationsPlugin.show( id++, 'plain title', 'plain body', notificationDetails, @@ -1235,10 +1318,21 @@ class _HomePageState extends State { categoryIdentifier: darwinNotificationCategoryText, ); - const NotificationDetails notificationDetails = NotificationDetails( + final WindowsNotificationDetails windowsNotificationDetails = + WindowsNotificationDetails( + actions: [ + WindowsAction(content: 'Send', arguments: 'send-reply', inputId: 'text'), + ], + inputs: [ + const WindowsTextInput(id: 'text', title: 'Send a reply?', hintText: 'Message'), + ], + ); + + final NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, macOS: darwinNotificationDetails, + windows: windowsNotificationDetails, ); await flutterLocalNotificationsPlugin.show(id++, 'Text Input Notification', @@ -1295,10 +1389,28 @@ class _HomePageState extends State { categoryIdentifier: darwinNotificationCategoryText, ); - const NotificationDetails notificationDetails = NotificationDetails( + final WindowsNotificationDetails windowsNotificationDetails = + WindowsNotificationDetails( + actions: [ + WindowsAction(content: 'Submit', arguments: 'submit', inputId: 'choice'), + ], + inputs: const [ + WindowsSelectionInput( + id: 'choice', + defaultItem: 'abc', + items: [ + WindowsSelection(id: 'abc', content: 'abc'), + WindowsSelection(id: 'def', content: 'def'), + ], + ), + ], + ); + + final NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, macOS: darwinNotificationDetails, + windows: windowsNotificationDetails, ); await flutterLocalNotificationsPlugin.show( id++, 'plain title', 'plain body', notificationDetails, @@ -1400,11 +1512,16 @@ class _HomePageState extends State { LinuxNotificationDetails( sound: AssetsLinuxSound('sound/slow_spring_board.mp3'), ); + final WindowsNotificationDetails windowsNotificationDetails = + WindowsNotificationDetails( + audio: WindowsNotificationAudio.preset(sound: WindowsNotificationSound.alarm5), + ); final NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, macOS: darwinNotificationDetails, linux: linuxPlatformChannelSpecifics, + windows: windowsNotificationDetails, ); await flutterLocalNotificationsPlugin.show( id++, @@ -1444,7 +1561,6 @@ class _HomePageState extends State { } Future _zonedScheduleNotification() async { - tz.initializeTimeZones(); await flutterLocalNotificationsPlugin.zonedSchedule( 0, 'scheduled title', @@ -1484,7 +1600,10 @@ class _HomePageState extends State { DarwinNotificationDetails( presentSound: false, ); - const NotificationDetails notificationDetails = NotificationDetails( + final WindowsNotificationDetails windowsDetails = + WindowsNotificationDetails(audio: WindowsNotificationAudio.silent()); + final NotificationDetails notificationDetails = NotificationDetails( + windows: windowsDetails, android: androidNotificationDetails, iOS: darwinNotificationDetails, macOS: darwinNotificationDetails); @@ -1504,7 +1623,10 @@ class _HomePageState extends State { DarwinNotificationDetails( presentSound: false, ); - const NotificationDetails notificationDetails = NotificationDetails( + final WindowsNotificationDetails windowsDetails = + WindowsNotificationDetails(audio: WindowsNotificationAudio.silent()); + final NotificationDetails notificationDetails = NotificationDetails( + windows: windowsDetails, android: androidNotificationDetails, iOS: darwinNotificationDetails, macOS: darwinNotificationDetails); @@ -2908,15 +3030,9 @@ class _HomePageState extends State { ); } - Future _showWindowsNotificationWithRawXml() async { - final WindowsNotificationDetails windowsPlatformChannelSpecifics = - WindowsNotificationDetails.fromXml(_windowsRawXmlController.text); - - final NotificationDetails platformChannelSpecifics = - NotificationDetails(windows: windowsPlatformChannelSpecifics); - await flutterLocalNotificationsPlugin.show( - id++, 'plain title', 'plain body', platformChannelSpecifics); - } + Future? _showWindowsNotificationWithRawXml() => flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation() + ?.showRawXml(id: id++, xml: _windowsRawXmlController.text); } Future _showLinuxNotificationWithBodyMarkup() async { @@ -3128,15 +3244,220 @@ Future getLinuxCapabilities() => LinuxFlutterLocalNotificationsPlugin>()! .getCapabilities(); +Future _showWindowsNotificationWithDuration() async { + await flutterLocalNotificationsPlugin.show( + id++, + 'This is a short notification', + 'This will last about 7 seconds', + NotificationDetails( + windows: WindowsNotificationDetails(duration: WindowsNotificationDuration.short), + ), + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is a long notification', + 'This will last about 25 seconds', + NotificationDetails( + windows: WindowsNotificationDetails(duration: WindowsNotificationDuration.long), + ), + ); +} + +Future _showWindowsNotificationWithScenarios() async { + await flutterLocalNotificationsPlugin.show( + id++, + 'This is an alarm', + null, + NotificationDetails( + windows: WindowsNotificationDetails(scenario: WindowsNotificationScenario.alarm), + ), + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is an incoming call', + null, + NotificationDetails( + windows: WindowsNotificationDetails(scenario: WindowsNotificationScenario.incomingCall), + ), + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is a reminder', + null, + NotificationDetails( + windows: WindowsNotificationDetails(scenario: WindowsNotificationScenario.reminder), + ), + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is an urgent notification', + null, + NotificationDetails( + windows: WindowsNotificationDetails(scenario: WindowsNotificationScenario.urgent), + ), + ); +} + +Future _showWindowsNotificationWithDetails() => flutterLocalNotificationsPlugin.show( + id++, + 'This one has more details', + 'And a different timestamp!', + NotificationDetails( + windows: WindowsNotificationDetails( + subtitle: 'This is the subtitle', + timestamp: DateTime.now().subtract(const Duration(hours: 2, minutes: 5)), + ), + ), +); + +Future _showWindowsNotificationWithImages() => flutterLocalNotificationsPlugin.show( + id++, + 'This notification has an image', + 'You can only show images from files', + NotificationDetails( + windows: WindowsNotificationDetails( + images: [ + WindowsImage( + source: File('./icons/coworker.png'), + altText: 'A beautiful image', + ), + ], + ), + ), +); + +Future _showWindowsNotificationWithGroups() => flutterLocalNotificationsPlugin.show( + id++, + 'This notification has many groups', + 'Each group stays together', + NotificationDetails( + windows: WindowsNotificationDetails( + subtitle: 'Caption text is fainter', + groups: [ + WindowsGroup([ + WindowsColumn([ + WindowsImage(source: File('icons/coworker.png'), altText: 'A coworker'), + const WindowsNotificationText(text: 'A coworker', isCaption: true), + ]), + WindowsColumn([ + WindowsImage(source: File('icons/4.0x/app_icon_density.png'), altText: 'The icon'), + const WindowsNotificationText(text: 'The icon'), + ]), + ]), + ], + ), + ), +); + +Future _showWindowsNotificationWithProgress() => flutterLocalNotificationsPlugin.show( + id++, + 'This notification has progress bars', + 'You can have precise or indeterminate', + NotificationDetails( + windows: WindowsNotificationDetails( + progressBars: [ + WindowsProgressBar( + title: 'This has indeterminate progress', + status: 'Downloading...', + value: null, + ), + WindowsProgressBar( + title: 'This has continuous progress', + status: 'Uploading...', + value: 0.75, + ), + WindowsProgressBar( + title: 'This has discrete progress', + status: 'Syncing...', + value: 0.75, + percentageOverride: '9/12 complete' + ), + ], + ), + ), +); + +Future _showWindowsNotificationWithActivation() => flutterLocalNotificationsPlugin.show( + id++, + 'These buttons do different things', + 'Click on each one!', + NotificationDetails( + windows: WindowsNotificationDetails( + actions: [ + WindowsAction( + content: 'Loading', + arguments: 'loading', + activationType: WindowsActivationType.background, + activationBehavior: WindowsNotificationBehavior.pendingUpdate, + ), + WindowsAction( + content: 'Google', + arguments: 'https://google.com', + activationType: WindowsActivationType.protocol, + activationBehavior: WindowsNotificationBehavior.pendingUpdate, + ), + ], + ), + ), +); + +Future _showWindowsNotificationWithButtonStyle() => flutterLocalNotificationsPlugin.show( + id++, + 'Incoming call', + 'Your best friend', + NotificationDetails( + windows: WindowsNotificationDetails( + actions: [ + WindowsAction( + content: 'Accept', + arguments: 'accept', + buttonStyle: WindowsButtonStyle.success, + ), + WindowsAction( + content: 'Reject', + arguments: 'reject', + buttonStyle: WindowsButtonStyle.critical, + ), + ], + ), + ), +); + +Future _showWindowsNotificationWithHeader() async { + const WindowsHeader header = WindowsHeader( + id: 'header', + title: 'Cool notifications', + arguments: 'header-clicked', + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is the first notification', + null, + NotificationDetails( + windows: WindowsNotificationDetails(header: header), + ), + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is the second notification', + null, + NotificationDetails( + windows: WindowsNotificationDetails(header: header), + ), + ); +} + class SecondPage extends StatefulWidget { const SecondPage( this.payload, { + this.data, Key? key, }) : super(key: key); static const String routeName = '/secondPage'; final String? payload; + final Map? data; @override State createState() => SecondPageState(); @@ -3144,11 +3465,13 @@ class SecondPage extends StatefulWidget { class SecondPageState extends State { String? _payload; + Map? _data; @override void initState() { super.initState(); _payload = widget.payload; + _data = widget.data; } @override @@ -3161,6 +3484,7 @@ class SecondPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Text('payload ${_payload ?? ''}'), + Text('data ${_data ?? ''}'), ElevatedButton( onPressed: () { Navigator.pop(context); diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index 6ab476246..c3390a33c 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -362,6 +362,9 @@ class FlutterLocalNotificationsPlugin { /// On Android, this will also require additional setup for the app, /// especially in the app's `AndroidManifest.xml` file. Please see check the /// readme for further details. + /// + /// On Windows, this will only set a notification on the [scheduledDate], and + /// not repeat, regardless of the value for [matchDateTimeComponents]. Future zonedSchedule( int id, String? title, @@ -408,6 +411,13 @@ class FlutterLocalNotificationsPlugin { id, title, body, scheduledDate, notificationDetails.macOS, payload: payload, matchDateTimeComponents: matchDateTimeComponents); + } else if (defaultTargetPlatform == TargetPlatform.windows) { + await resolvePlatformSpecificImplementation< + WindowsFlutterLocalNotificationsPlugin + >()?.zonedSchedule( + id, title, body, scheduledDate, notificationDetails.windows, + payload: payload, + ); } else { throw UnimplementedError('zonedSchedule() has not been implemented'); } @@ -466,6 +476,8 @@ class FlutterLocalNotificationsPlugin { MacOSFlutterLocalNotificationsPlugin>() ?.periodicallyShow(id, title, body, repeatInterval, notificationDetails: notificationDetails.macOS, payload: payload); + } else if (defaultTargetPlatform == TargetPlatform.windows) { + throw UnsupportedError('Notifications do not repeat on Windows'); } else { await FlutterLocalNotificationsPlatform.instance .periodicallyShow(id, title, body, repeatInterval); diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index caa9110ef..36ba8615b 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -956,6 +956,54 @@ class WindowsFlutterLocalNotificationsPlugin return _channel.invokeMethod('initialize', settings.toMap()); } + /// Passes the raw XML to the Windows API directly. + /// + /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. + /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). + Future showRawXml({ + required int id, + required String xml, + String? group, + }) => _channel.invokeMethod('show', { + 'id': id, + 'group': group, + 'platformSpecifics': {'rawXml': xml}, + }); + + String _notificationToXml({ + String? title, + String? body, + String? payload, + WindowsNotificationDetails? notificationDetails, + }) { + final XmlBuilder builder = XmlBuilder(); + builder.element( + 'toast', + attributes: { + ...notificationDetails?.attributes ?? {}, + if (payload != null) 'launch': payload, + 'useButtonStyle': 'true', + }, + nest: () { + builder.element('visual', nest: () { + builder.element( + 'binding', + attributes: {'template': 'ToastGeneric'}, + nest: () { + builder + ..element('text', nest: title) + ..element('text', nest: body); + notificationDetails?.generateBinding(builder); + }, + ); + }); + notificationDetails?.toXml(builder); + }, + ); + return builder.buildDocument() + .toXmlString(pretty: true, indentAttribute: (_) => true); + } + @override Future show( int id, @@ -965,35 +1013,15 @@ class WindowsFlutterLocalNotificationsPlugin String? group, WindowsNotificationDetails? notificationDetails, }) async { - final XmlBuilder builder = XmlBuilder(); - builder.element('toast', - attributes: { - ...notificationDetails?.attributes ?? {}, - if (payload != null) 'launch': payload, - 'useButtonStyle': 'true', - }, - nest: () { - builder.element('visual', nest: () { - builder.element( - 'binding', - attributes: {'template': 'ToastGeneric'}, - nest: () { - builder..element('text', nest: title)..element('text', nest: body); - notificationDetails?.generateBinding(builder); - }, - ); - }); - notificationDetails?.toXml(builder); - }); - final String xml = builder - .buildDocument() - .toXmlString(pretty: true, indentAttribute: (_) => true); + final String xml = _notificationToXml( + title: title, + body: body, + payload: payload, + notificationDetails: notificationDetails, + ); await _channel.invokeMethod('show', { 'id': id, - 'title': title, - 'body': body, 'group': group, - 'payload': payload, 'platformSpecifics': {'rawXml': xml}, }); } @@ -1005,6 +1033,31 @@ class WindowsFlutterLocalNotificationsPlugin 'group': group, }); + /// Schedules a notification for the future. + Future zonedSchedule( + int id, + String? title, + String? body, + TZDateTime scheduledDate, + WindowsNotificationDetails? notificationDetails, { + String? payload, + }) async { + final String xml = _notificationToXml( + title: title, + body: body, + payload: payload, + notificationDetails: notificationDetails, + ); + final int secondsSinceEpoch = scheduledDate.millisecondsSinceEpoch ~/ 1000; + await _channel.invokeMethod('zonedSchedule', { + 'id': id, + 'platformSpecifics': { + 'rawXml': xml, + 'time': secondsSinceEpoch, + }, + }); + } + Future _handleMethod(MethodCall call) async { switch (call.method) { case 'didReceiveNotificationResponse': diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart index 850752580..18d41149a 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart @@ -65,6 +65,12 @@ enum WindowsNotificationSound { /// Specifies custom audio to play during a notification. class WindowsNotificationAudio { + /// No sound will play during this notification. + WindowsNotificationAudio.silent() : + source = WindowsNotificationSound.defaultSound.name, + shouldLoop = false, + isSilent = true; + /// Audio from a Windows preset. See [WindowsNotificationSound] for options. WindowsNotificationAudio.preset({ required WindowsNotificationSound sound, diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart index bce806502..692a92ee3 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart @@ -3,10 +3,14 @@ import 'package:xml/xml.dart'; import 'notification_action.dart'; import 'notification_audio.dart'; import 'notification_group.dart'; +import 'notification_header.dart'; import 'notification_image.dart'; import 'notification_input.dart'; import 'notification_progress.dart'; +export 'notification_part.dart'; +export 'notification_text.dart'; + /// The duration for a Windows notification. enum WindowsNotificationDuration { /// The notification will stay for a long time. @@ -37,6 +41,23 @@ enum WindowsNotificationScenario { urgent, } +extension on DateTime { + String toIso8601StringTz() { + // Get offset + final Duration offset = timeZoneOffset; + final String sign = offset.isNegative ? '-' : '+'; + final String hours = offset.inHours.abs().toString().padLeft(2, '0'); + final String minutes = offset.inMinutes.abs().remainder(60) + .toString().padLeft(2, '0'); + final String offsetString = '$sign$hours:$minutes'; + + // Get first part of properly formatted ISO 8601 date + final String formattedDate = toIso8601String().split('.').first; + + return '$formattedDate$offsetString'; + } +} + /// Contains notification details specific to Windows. /// /// See: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts @@ -48,6 +69,7 @@ class WindowsNotificationDetails { this.images = const [], this.groups = const [], this.progressBars = const [], + this.header, this.audio, this.duration, this.scenario, @@ -66,22 +88,6 @@ class WindowsNotificationDetails { } } - /// Passes the raw XML to the Windows API directly. - /// - /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. - /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). - const WindowsNotificationDetails.fromXml(this.rawXml) : - actions = const [], - inputs = const [], - images = const [], - groups = const [], - progressBars = const [], - audio = null, - timestamp = null, - duration = null, - scenario = null, - subtitle = null; - /// The raw XML passed to the Windows API. /// /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. @@ -103,6 +109,9 @@ class WindowsNotificationDetails { /// The scenario for this notification. Sets some defaults based on the value. final WindowsNotificationScenario? scenario; + /// The header for this group of notifications. + final WindowsHeader? header; + /// Overrides the timestamp to show on the notification. final DateTime? timestamp; @@ -121,7 +130,7 @@ class WindowsNotificationDetails { /// XML attributes for the toast notification as a whole. Map get attributes => { if (duration != null) 'duration': duration!.name, - if (timestamp != null) 'displayTimestamp': timestamp!.toIso8601String(), + if (timestamp != null) 'displayTimestamp': timestamp!.toIso8601StringTz(), if (scenario != null) 'scenario': scenario!.name, }; @@ -140,6 +149,7 @@ class WindowsNotificationDetails { } }); audio?.toXml(builder); + header?.toXml(builder); } /// Generates the `` element of the notification. diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_group.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_group.dart index 432b5482b..e7be6e89e 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_group.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_group.dart @@ -17,11 +17,15 @@ class WindowsGroup { 'group', nest: () { for (final WindowsColumn column in columns) { - builder.element('subgroup', nest: () { - for (final WindowsNotificationPart part in column.parts) { - part.toXml(builder); - } - }); + builder.element( + 'subgroup', + attributes: {'hint-weight': '1'}, + nest: () { + for (final WindowsNotificationPart part in column.parts) { + part.toXml(builder); + } + }, + ); } } ); diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_header.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_header.dart index f61b0cfa7..4efa913bc 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_header.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_header.dart @@ -31,7 +31,6 @@ class WindowsHeader { /// Specifies how the application will open. final WindowsHeaderActivation? activation; - /// Serializes this header to XML. void toXml(XmlBuilder builder) => builder.element( 'header', diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart index 5263bc5d3..e8b2fbb4f 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart @@ -21,13 +21,13 @@ enum WindowsImageCrop { /// An image in a Windows notification. class WindowsImage extends WindowsNotificationPart { /// Creates a Windows notification image. - const WindowsImage({ - required this.source, + WindowsImage({ + required File source, required this.altText, this.addQueryParams = false, this.placement, this.crop, - }); + }) : source = source.absolute; /// Whether Windows should add URL query parameters when fetching the image. final bool addQueryParams; diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart index 053fd1408..cf600adef 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart @@ -62,11 +62,14 @@ class WindowsSelectionInput extends WindowsInput { const WindowsSelectionInput({ required super.id, required this.items, + this.defaultItem, super.title, }) : super(type: WindowsInputType.selection); /// The items that can be selected. final List items; + /// The default item that is selected. + final String? defaultItem; @override void toXml(XmlBuilder builder) => builder.element( @@ -75,6 +78,7 @@ class WindowsSelectionInput extends WindowsInput { 'id': id, 'type': type.name, if (title != null) 'title': title!, + if (defaultItem != null) 'defaultInput': defaultItem!, }, nest: () { for (final WindowsSelection item in items) { diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_text.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_text.dart index 693e8e6a3..93ddf7ab1 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_text.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_text.dart @@ -16,6 +16,7 @@ class WindowsNotificationText extends WindowsNotificationPart { const WindowsNotificationText({ required this.text, this.centerIfCall = false, + this.isCaption = false, this.placement, this.languageCode, }); @@ -26,6 +27,9 @@ class WindowsNotificationText extends WindowsNotificationPart { /// Whether to center this text. Only relevant if in an incoming call. final bool centerIfCall; + /// Whether the text should be smaller like a caption. + final bool isCaption; + /// The placement of this text. Null indicates default. final WindowsTextPlacement? placement; @@ -39,6 +43,8 @@ class WindowsNotificationText extends WindowsNotificationPart { if (languageCode != null) 'lang': languageCode!, if (placement != null) 'placement': placement!.name, 'hint-callScenarioCenterAlign': centerIfCall.toString(), + 'hint-align': 'center', + if (isCaption) 'hint-style': 'captionsubtle', }, nest: text, ); diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 15402e873..88784d8e8 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -69,14 +69,8 @@ namespace { /// Displays a single notification toast. /// /// A unique ID that identifies this notification. It can be used to cancel/dismiss the notification. - /// An optional title of the notification. - /// An optional body of the notification. - /// void ShowNotification( const int id, - const std::optional& title, - const std::optional& body, - const std::optional& payload, const std::optional& group, const std::optional& platformSpecifics); @@ -92,9 +86,35 @@ namespace { /// void CancelAllNotifications(); + /// + /// Gets all active notifications. Requires an MSIX package to use. + /// & result); + /// + /// Gets all pending or scheduled notifications. + /// & result); + std::optional HasIdentity(); + + /// + /// Schedules a notification to be shown later. + /// + /// A unique ID that identifies this notification. It can be used to cancel/dismiss the notification. + void ScheduleNotification( + const int id, + const flutter::EncodableMap& platformSpecifics); + + /// + /// Updates a progress bar with the given id. + /// + /// A unique ID that identifies this progress bar. + /// A float (0.0 - 1.0) that determines the progress + /// A label to display instead of the percentage + void UpdateProgress(const int id, + const std::optional value, + const std::optional label); }; // static @@ -152,15 +172,18 @@ namespace { else if (method_name == Method::SHOW) { const auto args = std::get_if(method_call.arguments()); if (args != nullptr && toastNotifier.has_value()) { - const auto id = Utils::GetMapValue("id", args).value(); - const auto title = Utils::GetMapValue("title", args); - const auto body = Utils::GetMapValue("body", args); - const auto payload = Utils::GetMapValue("payload", args); - const auto group = Utils::GetMapValue("group", args); - const auto platformSpecifics = Utils::GetMapValue("platformSpecifics", args); - - ShowNotification(id, title, body, payload, group, platformSpecifics); - result->Success(); + try { + const auto id = Utils::GetMapValue("id", args).value(); + const auto group = Utils::GetMapValue("group", args); + const auto platformSpecifics = Utils::GetMapValue("platformSpecifics", args); + + ShowNotification(id, group, platformSpecifics); + result->Success(); + } catch (winrt::hresult_error error) { + result->Error("Invalid XML", "The XML was invalid. If you used raw XML, please verify it. If not, please report this error"); + } catch (...) { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + } } else { result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); @@ -199,6 +222,28 @@ namespace { result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); } } + else if (method_name == Method::GET_PENDING_NOTIFICATIONS && toastNotifier.has_value()) { + try { + std::vector vec; + GetPendingNotifications(vec); + result->Success(vec); + } catch (std::exception error) { + result->Error("INTERNAL", error.what()); + } catch (...) { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + } + } + else if (method_name == Method::SCHEDULE_NOTIFICATION && toastNotifier.has_value()) { + const auto args = std::get_if(method_call.arguments()); + const auto id = Utils::GetMapValue("id", args).value(); + const auto platformSpecifics = Utils::GetMapValue("platformSpecifics", args); + try { + ScheduleNotification(id, platformSpecifics.value()); + result->Success(); + } catch (...) { + result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); + } + } else { result->NotImplemented(); } @@ -257,53 +302,12 @@ namespace { void FlutterLocalNotificationsPlugin::ShowNotification( const int id, - const std::optional& title, - const std::optional& body, - const std::optional& payload, const std::optional& group, const std::optional& platformSpecifics ) { - // obtain a notification template with a title and a body - //const auto doc = winrt::Windows::UI::Notifications::ToastNotificationManager::GetTemplateContent(winrt::Windows::UI::Notifications::ToastTemplateType::ToastText02); - // find all tags - //const auto nodes = doc.GetElementsByTagName(L"text"); - - auto rawXml = platformSpecifics.has_value() ? - Utils::GetMapValue("rawXml", &platformSpecifics.value()) : - std::nullopt; - + auto rawXml = Utils::GetMapValue("rawXml", &platformSpecifics.value()); XmlDocument doc; - if (!rawXml.has_value()) { - doc.LoadXml(L"\ - \ - \ - \ - \ - \ - "); - - const auto bindingNode = doc.SelectSingleNode(L"//binding[1]"); - - if (title.has_value()) { - // change the text of the first , which will be the title - const auto textNode = doc.CreateElement(L"text"); - textNode.InnerText(winrt::to_hstring(*title)); - bindingNode.AppendChild(textNode); - } - if (body.has_value()) { - // change the text of the second , which will be the body - //nodes.Item(1).AppendChild(doc.CreateTextNode(winrt::to_hstring(body.value()))); - const auto textNode = doc.CreateElement(L"text"); - textNode.InnerText(winrt::to_hstring(*body)); - bindingNode.AppendChild(textNode); - } - if (payload.has_value()) { - doc.DocumentElement().SetAttribute(L"launch", winrt::to_hstring(*payload)); - } - } - else { - doc.LoadXml(winrt::to_hstring(rawXml.value())); - } + doc.LoadXml(winrt::to_hstring(rawXml.value())); winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; notif.Tag(winrt::to_hstring(id)); @@ -313,7 +317,6 @@ namespace { else { notif.Group(_aumid); } - toastNotifier.value().Show(notif); } @@ -347,10 +350,45 @@ namespace { flutter::EncodableMap data; const auto notif = history.GetAt(i); const auto tag = notif.Tag(); - data["id"] = flutter::EncodableValue(tag.c_str()); + const auto tagString = winrt::to_string(tag); + const auto tagInt = std::stoi(tagString); + data[std::string("id")] = flutter::EncodableValue(tagInt); + result.emplace_back(flutter::EncodableValue(data)); + } + } + + void FlutterLocalNotificationsPlugin::GetPendingNotifications(std::vector& result) { + const auto scheduled = toastNotifier.value().GetScheduledToastNotifications(); + for (const auto notif : scheduled) { + flutter::EncodableMap data; + const auto tag = notif.Tag(); + const auto tagString = winrt::to_string(tag); + const auto tagInt = std::stoi(tagString); + data[std::string("id")] = flutter::EncodableValue(tagInt); result.emplace_back(flutter::EncodableValue(data)); } } + + void FlutterLocalNotificationsPlugin::ScheduleNotification(const int id, const flutter::EncodableMap& platformSpecifics) { + auto rawXml = Utils::GetMapValue("rawXml", &platformSpecifics); + XmlDocument doc; + doc.LoadXml(winrt::to_hstring(rawXml.value())); + + const auto secondsSinceEpoch = Utils::GetMapValue("time", &platformSpecifics).value(); + time_t time(secondsSinceEpoch); + const auto time2 = winrt::clock::from_time_t(time); + winrt::Windows::UI::Notifications::ScheduledToastNotification notif(doc, time2); + notif.Tag(winrt::to_hstring(id)); + toastNotifier.value().AddToSchedule(notif); + } + + void FlutterLocalNotificationsPlugin::UpdateProgress( + const int id, + const std::optional value, + const std::optional label + ) { + + } } void FlutterLocalNotificationsPluginRegisterWithRegistrar( diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h index 945f72bb5..19febe0c9 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h @@ -13,4 +13,6 @@ namespace Method extern const std::string CANCEL_ALL; extern const std::string DID_RECEIVE_NOTIFICATION_RESPONSE; extern const std::string GET_ACTIVE_NOTIFICATIONS; + extern const std::string GET_PENDING_NOTIFICATIONS; + extern const std::string SCHEDULE_NOTIFICATION; } diff --git a/flutter_local_notifications/windows/methods.cpp b/flutter_local_notifications/windows/methods.cpp index 1675392ab..877f725b5 100644 --- a/flutter_local_notifications/windows/methods.cpp +++ b/flutter_local_notifications/windows/methods.cpp @@ -9,3 +9,5 @@ const std::string Method::CANCEL = "cancel"; const std::string Method::CANCEL_ALL = "cancelAll"; const std::string Method::DID_RECEIVE_NOTIFICATION_RESPONSE = "didReceiveNotificationResponse"; const std::string Method::GET_ACTIVE_NOTIFICATIONS = "getActiveNotifications"; +const std::string Method::GET_PENDING_NOTIFICATIONS = "pendingNotificationRequests"; +const std::string Method::SCHEDULE_NOTIFICATION = "zonedSchedule"; From be5584c7cc15bdbfeddab32cd09503b2fc8a6089 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 27 Jun 2024 17:02:06 -0400 Subject: [PATCH 031/112] Added update, not working on unpackaged --- .../example/lib/main.dart | 64 +++++++++++++++++-- .../platform_flutter_local_notifications.dart | 17 ++++- .../windows/notification_input.dart | 1 + .../windows/notification_progress.dart | 5 ++ .../flutter_local_notifications_plugin.cpp | 31 +++++++-- .../flutter_local_notifications/methods.h | 1 + .../windows/methods.cpp | 1 + 7 files changed, 108 insertions(+), 12 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 1243558c7..100cb6974 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -1121,7 +1121,7 @@ class _HomePageState extends State { }, ), PaddedElevatedButton( - buttonText: 'Show notifications with groups', + buttonText: 'Show notifications with columns', onPressed: () async { await _showWindowsNotificationWithGroups(); }, @@ -1132,6 +1132,12 @@ class _HomePageState extends State { await _showWindowsNotificationWithProgress(); }, ), + PaddedElevatedButton( + buttonText: 'Show notifications with dynamic progress', + onPressed: () async { + await _showWindowsNotificationWithUpdates(); + }, + ), PaddedElevatedButton( buttonText: 'Show notitification with activation', onPressed: () async { @@ -3269,7 +3275,12 @@ Future _showWindowsNotificationWithScenarios() async { 'This is an alarm', null, NotificationDetails( - windows: WindowsNotificationDetails(scenario: WindowsNotificationScenario.alarm), + windows: WindowsNotificationDetails( + scenario: WindowsNotificationScenario.alarm, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ] + ), ), ); await flutterLocalNotificationsPlugin.show( @@ -3277,7 +3288,12 @@ Future _showWindowsNotificationWithScenarios() async { 'This is an incoming call', null, NotificationDetails( - windows: WindowsNotificationDetails(scenario: WindowsNotificationScenario.incomingCall), + windows: WindowsNotificationDetails( + scenario: WindowsNotificationScenario.incomingCall, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ] + ), ), ); await flutterLocalNotificationsPlugin.show( @@ -3285,7 +3301,12 @@ Future _showWindowsNotificationWithScenarios() async { 'This is a reminder', null, NotificationDetails( - windows: WindowsNotificationDetails(scenario: WindowsNotificationScenario.reminder), + windows: WindowsNotificationDetails( + scenario: WindowsNotificationScenario.reminder, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ] + ), ), ); await flutterLocalNotificationsPlugin.show( @@ -3293,7 +3314,12 @@ Future _showWindowsNotificationWithScenarios() async { 'This is an urgent notification', null, NotificationDetails( - windows: WindowsNotificationDetails(scenario: WindowsNotificationScenario.urgent), + windows: WindowsNotificationDetails( + scenario: WindowsNotificationScenario.urgent, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ] + ), ), ); } @@ -3318,7 +3344,7 @@ Future _showWindowsNotificationWithImages() => flutterLocalNotificationsPl windows: WindowsNotificationDetails( images: [ WindowsImage( - source: File('./icons/coworker.png'), + source: File('./icons/4.0x/app_icon_density.png'), altText: 'A beautiful image', ), ], @@ -3377,6 +3403,32 @@ Future _showWindowsNotificationWithProgress() => flutterLocalNotifications ), ); +Future _showWindowsNotificationWithUpdates() async { + final int notifId = id++; + final WindowsFlutterLocalNotificationsPlugin? windows = flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation(); + await flutterLocalNotificationsPlugin.show( + notifId, + 'Dynamic updates', + 'The progress bar should slowly fill up', + NotificationDetails( + windows: WindowsNotificationDetails( + progressBars: [ + WindowsProgressBar(status: 'Updating...', value: 0), + ], + ), + ), + ); + int progress = 0; + Timer.periodic(const Duration(milliseconds: 300), (Timer timer) async { + if (progress++ == 10) { + // await windows?.cancel(notifId); + return timer.cancel(); + } + await windows?.updateProgress(id: notifId, value: progress / 10, label: '$progress/10 completed'); + }); +} + Future _showWindowsNotificationWithActivation() => flutterLocalNotificationsPlugin.show( id++, 'These buttons do different things', diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 36ba8615b..98b7a1e58 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -29,6 +29,7 @@ import 'platform_specifics/ios/enums.dart'; import 'platform_specifics/windows/initialization_settings.dart'; import 'platform_specifics/windows/method_channel_mappers.dart'; import 'platform_specifics/windows/notification_details.dart'; +import 'platform_specifics/windows/notification_progress.dart'; import 'typedefs.dart'; import 'types.dart'; import 'tz_datetime_mapper.dart'; @@ -982,7 +983,7 @@ class WindowsFlutterLocalNotificationsPlugin attributes: { ...notificationDetails?.attributes ?? {}, if (payload != null) 'launch': payload, - 'useButtonStyle': 'true', + if (notificationDetails?.scenario == null) 'useButtonStyle': 'true', }, nest: () { builder.element('visual', nest: () { @@ -1058,6 +1059,20 @@ class WindowsFlutterLocalNotificationsPlugin }); } + /// Updates the progress bar in the notification with the given ID. + /// + /// [value] corresponds to [WindowsProgressBar.value] and [label] with + /// [WindowsProgressBar.percentageOverride]. + Future updateProgress({ + required int id, + double? value, + String? label, + }) => _channel.invokeMethod('update', { + 'id': id, + if (value != null) 'value': value, + if (label != null) 'label': label, + }); + Future _handleMethod(MethodCall call) async { switch (call.method) { case 'didReceiveNotificationResponse': diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart index cf600adef..c7b1a11c1 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart @@ -68,6 +68,7 @@ class WindowsSelectionInput extends WindowsInput { /// The items that can be selected. final List items; + /// The default item that is selected. final String? defaultItem; diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart index 522bc1ddd..a0691fa7b 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart @@ -1,6 +1,11 @@ import 'package:xml/xml.dart'; +import '../../../flutter_local_notifications.dart'; + /// A progress bar in a Windows notification. +/// +/// To update the progress after the notification has been shown, +/// use [WindowsFlutterLocalNotificationsPlugin.updateProgress]. class WindowsProgressBar { /// Creates a progress bar for a Windows notification. WindowsProgressBar({ diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 88784d8e8..bb69cbe52 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include // For getPlatformVersion; remove unless needed for your plugin implementation. @@ -113,7 +114,7 @@ namespace { /// A float (0.0 - 1.0) that determines the progress /// A label to display instead of the percentage void UpdateProgress(const int id, - const std::optional value, + const std::optional value, const std::optional label); }; @@ -244,6 +245,14 @@ namespace { result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); } } + else if (method_name == Method::UPDATE && toastNotifier.has_value()) { + const auto args = std::get_if(method_call.arguments()); + const auto id = Utils::GetMapValue("id", args).value(); + const auto value = Utils::GetMapValue("value", args); + const auto label = Utils::GetMapValue("label", args); + UpdateProgress(id, value, label); + result->Success(); + } else { result->NotImplemented(); } @@ -292,10 +301,11 @@ namespace { if (!hasIdentity.has_value()) return false; + const auto user = winrt::Windows::System::User::GetDefault(); if (hasIdentity.value()) - toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(); + toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::GetForUser(user).CreateToastNotifier(); else - toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::CreateToastNotifier(winrt::to_hstring(aumid)); + toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::GetForUser(user).CreateToastNotifier(winrt::to_hstring(aumid)); return true; } @@ -338,6 +348,9 @@ namespace { toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); } toastNotificationHistory.value().Clear(_aumid); + for (const auto scheduled : toastNotifier.value().GetScheduledToastNotifications()) { + toastNotifier.value().RemoveFromSchedule(scheduled); + } } void FlutterLocalNotificationsPlugin::GetActiveNotifications(std::vector& result) { @@ -384,10 +397,18 @@ namespace { void FlutterLocalNotificationsPlugin::UpdateProgress( const int id, - const std::optional value, + const std::optional value, const std::optional label ) { - + const auto tag = winrt::to_hstring(id); + winrt::Windows::UI::Notifications::NotificationData data; + + if (value.has_value()) { + data.Values().Insert(winrt::to_hstring("progressValue"), winrt::to_hstring(value.value())); + } + if (label.has_value()) { + data.Values().Insert(winrt::to_hstring("progressValueString"), winrt::to_hstring(label.value())); + } } } diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h index 19febe0c9..c6142e5b5 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h @@ -15,4 +15,5 @@ namespace Method extern const std::string GET_ACTIVE_NOTIFICATIONS; extern const std::string GET_PENDING_NOTIFICATIONS; extern const std::string SCHEDULE_NOTIFICATION; + extern const std::string UPDATE; } diff --git a/flutter_local_notifications/windows/methods.cpp b/flutter_local_notifications/windows/methods.cpp index 877f725b5..e143daa9f 100644 --- a/flutter_local_notifications/windows/methods.cpp +++ b/flutter_local_notifications/windows/methods.cpp @@ -11,3 +11,4 @@ const std::string Method::DID_RECEIVE_NOTIFICATION_RESPONSE = "didReceiveNotific const std::string Method::GET_ACTIVE_NOTIFICATIONS = "getActiveNotifications"; const std::string Method::GET_PENDING_NOTIFICATIONS = "pendingNotificationRequests"; const std::string Method::SCHEDULE_NOTIFICATION = "zonedSchedule"; +const std::string Method::UPDATE = "update"; From 6ac515f38dd68729123896d64526dd9bd741b1e8 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 27 Jun 2024 17:10:03 -0400 Subject: [PATCH 032/112] Copied NotificationManager --- .../DesktopNotificationManagerCompat.cpp | 290 ++++++++++++++++++ .../DesktopNotificationManagerCompat.h | 108 +++++++ 2 files changed, 398 insertions(+) create mode 100644 flutter_local_notifications/windows/DesktopNotificationManagerCompat.cpp create mode 100644 flutter_local_notifications/windows/DesktopNotificationManagerCompat.h diff --git a/flutter_local_notifications/windows/DesktopNotificationManagerCompat.cpp b/flutter_local_notifications/windows/DesktopNotificationManagerCompat.cpp new file mode 100644 index 000000000..a7c9401a2 --- /dev/null +++ b/flutter_local_notifications/windows/DesktopNotificationManagerCompat.cpp @@ -0,0 +1,290 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +#include "DesktopNotificationManagerCompat.h" +#include +#include + +#define RETURN_IF_FAILED(hr) do { HRESULT _hrTemp = hr; if (FAILED(_hrTemp)) { return _hrTemp; } } while (false) + +using namespace ABI::Windows::Data::Xml::Dom; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; + +namespace DesktopNotificationManagerCompat +{ + HRESULT RegisterComServer(GUID clsid, const wchar_t exePath[]); + HRESULT EnsureRegistered(); + bool IsRunningAsUwp(); + + bool s_registeredAumidAndComServer = false; + std::wstring s_aumid; + bool s_registeredActivator = false; + bool s_hasCheckedIsRunningAsUwp = false; + bool s_isRunningAsUwp = false; + + HRESULT RegisterAumidAndComServer(const wchar_t *aumid, GUID clsid) + { + // If running as Desktop Bridge + if (IsRunningAsUwp()) + { + // Clear the AUMID since Desktop Bridge doesn't use it, and then we're done. + // Desktop Bridge apps are registered with platform through their manifest. + // Their LocalServer32 key is also registered through their manifest. + s_aumid = L""; + s_registeredAumidAndComServer = true; + return S_OK; + } + + // Copy the aumid + s_aumid = std::wstring(aumid); + + // Get the EXE path + wchar_t exePath[MAX_PATH]; + DWORD charWritten = ::GetModuleFileName(nullptr, exePath, ARRAYSIZE(exePath)); + RETURN_IF_FAILED(charWritten > 0 ? S_OK : HRESULT_FROM_WIN32(::GetLastError())); + + // Register the COM server + RETURN_IF_FAILED(RegisterComServer(clsid, exePath)); + + s_registeredAumidAndComServer = true; + return S_OK; + } + + HRESULT RegisterActivator() + { + // Module needs a callback registered before it can be used. + // Since we don't care about when it shuts down, we'll pass an empty lambda here. + Module::Create([] {}); + + // If a local server process only hosts the COM object then COM expects + // the COM server host to shutdown when the references drop to zero. + // Since the user might still be using the program after activating the notification, + // we don't want to shutdown immediately. Incrementing the object count tells COM that + // we aren't done yet. + Module::GetModule().IncrementObjectCount(); + + RETURN_IF_FAILED(Module::GetModule().RegisterObjects()); + + s_registeredActivator = true; + return S_OK; + } + + HRESULT RegisterComServer(GUID clsid, const wchar_t exePath[]) + { + // Turn the GUID into a string + OLECHAR* clsidOlechar; + StringFromCLSID(clsid, &clsidOlechar); + std::wstring clsidStr(clsidOlechar); + ::CoTaskMemFree(clsidOlechar); + + // Create the subkey + // Something like SOFTWARE\Classes\CLSID\{23A5B06E-20BB-4E7E-A0AC-6982ED6A6041}\LocalServer32 + std::wstring subKey = LR"(SOFTWARE\Classes\CLSID\)" + clsidStr + LR"(\LocalServer32)"; + + // Include -ToastActivated launch args on the exe + std::wstring exePathStr(exePath); + exePathStr = L"\"" + exePathStr + L"\" " + TOAST_ACTIVATED_LAUNCH_ARG; + + // We don't need to worry about overflow here as ::GetModuleFileName won't + // return anything bigger than the max file system path (much fewer than max of DWORD). + DWORD dataSize = static_cast((exePathStr.length() + 1) * sizeof(WCHAR)); + + // Register the EXE for the COM server + return HRESULT_FROM_WIN32(::RegSetKeyValue( + HKEY_CURRENT_USER, + subKey.c_str(), + nullptr, + REG_SZ, + reinterpret_cast(exePathStr.c_str()), + dataSize)); + } + + HRESULT CreateToastNotifier(IToastNotifier **notifier) + { + RETURN_IF_FAILED(EnsureRegistered()); + + ComPtr toastStatics; + RETURN_IF_FAILED(Windows::Foundation::GetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), + &toastStatics)); + + if (s_aumid.empty()) + { + return toastStatics->CreateToastNotifier(notifier); + } + else + { + return toastStatics->CreateToastNotifierWithId(HStringReference(s_aumid.c_str()).Get(), notifier); + } + } + + HRESULT CreateXmlDocumentFromString(const wchar_t *xmlString, IXmlDocument **doc) + { + ComPtr answer; + RETURN_IF_FAILED(Windows::Foundation::ActivateInstance(HStringReference(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument).Get(), &answer)); + + ComPtr docIO; + RETURN_IF_FAILED(answer.As(&docIO)); + + // Load the XML string + RETURN_IF_FAILED(docIO->LoadXml(HStringReference(xmlString).Get())); + + return answer.CopyTo(doc); + } + + HRESULT CreateToastNotification(IXmlDocument *content, IToastNotification **notification) + { + ComPtr factory; + RETURN_IF_FAILED(Windows::Foundation::GetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), + &factory)); + + return factory->CreateToastNotification(content, notification); + } + + HRESULT get_History(std::unique_ptr* history) + { + RETURN_IF_FAILED(EnsureRegistered()); + + ComPtr toastStatics; + RETURN_IF_FAILED(Windows::Foundation::GetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), + &toastStatics)); + + ComPtr toastStatics2; + RETURN_IF_FAILED(toastStatics.As(&toastStatics2)); + + ComPtr nativeHistory; + RETURN_IF_FAILED(toastStatics2->get_History(&nativeHistory)); + + *history = std::unique_ptr(new DesktopNotificationHistoryCompat(s_aumid.c_str(), nativeHistory)); + return S_OK; + } + + bool CanUseHttpImages() + { + return IsRunningAsUwp(); + } + + HRESULT EnsureRegistered() + { + // If not registered AUMID yet + if (!s_registeredAumidAndComServer) + { + // Check if Desktop Bridge + if (IsRunningAsUwp()) + { + // Implicitly registered, all good! + s_registeredAumidAndComServer = true; + } + else + { + // Otherwise, incorrect usage, must call RegisterAumidAndComServer first + return E_ILLEGAL_METHOD_CALL; + } + } + + // If not registered activator yet + if (!s_registeredActivator) + { + // Incorrect usage, must call RegisterActivator first + return E_ILLEGAL_METHOD_CALL; + } + + return S_OK; + } + + bool IsRunningAsUwp() + { + if (!s_hasCheckedIsRunningAsUwp) + { + // https://stackoverflow.com/questions/39609643/determine-if-c-application-is-running-as-a-uwp-app-in-desktop-bridge-project + UINT32 length; + wchar_t packageFamilyName[PACKAGE_FAMILY_NAME_MAX_LENGTH + 1]; + LONG result = GetPackageFamilyName(GetCurrentProcess(), &length, packageFamilyName); + s_isRunningAsUwp = result == ERROR_SUCCESS; + s_hasCheckedIsRunningAsUwp = true; + } + + return s_isRunningAsUwp; + } +} + +DesktopNotificationHistoryCompat::DesktopNotificationHistoryCompat(const wchar_t *aumid, ComPtr history) +{ + m_aumid = std::wstring(aumid); + m_history = history; +} + +HRESULT DesktopNotificationHistoryCompat::Clear() +{ + if (m_aumid.empty()) + { + return m_history->Clear(); + } + else + { + return m_history->ClearWithId(HStringReference(m_aumid.c_str()).Get()); + } +} + +HRESULT DesktopNotificationHistoryCompat::GetHistory(ABI::Windows::Foundation::Collections::IVectorView **toasts) +{ + ComPtr history2; + RETURN_IF_FAILED(m_history.As(&history2)); + + if (m_aumid.empty()) + { + return history2->GetHistory(toasts); + } + else + { + return history2->GetHistoryWithId(HStringReference(m_aumid.c_str()).Get(), toasts); + } +} + +HRESULT DesktopNotificationHistoryCompat::Remove(const wchar_t *tag) +{ + if (m_aumid.empty()) + { + return m_history->Remove(HStringReference(tag).Get()); + } + else + { + return m_history->RemoveGroupedTagWithId(HStringReference(tag).Get(), HStringReference(L"").Get(), HStringReference(m_aumid.c_str()).Get()); + } +} + +HRESULT DesktopNotificationHistoryCompat::RemoveGroupedTag(const wchar_t *tag, const wchar_t *group) +{ + if (m_aumid.empty()) + { + return m_history->RemoveGroupedTag(HStringReference(tag).Get(), HStringReference(group).Get()); + } + else + { + return m_history->RemoveGroupedTagWithId(HStringReference(tag).Get(), HStringReference(group).Get(), HStringReference(m_aumid.c_str()).Get()); + } +} + +HRESULT DesktopNotificationHistoryCompat::RemoveGroup(const wchar_t *group) +{ + if (m_aumid.empty()) + { + return m_history->RemoveGroup(HStringReference(group).Get()); + } + else + { + return m_history->RemoveGroupWithId(HStringReference(group).Get(), HStringReference(m_aumid.c_str()).Get()); + } +} \ No newline at end of file diff --git a/flutter_local_notifications/windows/DesktopNotificationManagerCompat.h b/flutter_local_notifications/windows/DesktopNotificationManagerCompat.h new file mode 100644 index 000000000..2d63fe86d --- /dev/null +++ b/flutter_local_notifications/windows/DesktopNotificationManagerCompat.h @@ -0,0 +1,108 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +#pragma once +#include +#include +#include +#include +#include +#define TOAST_ACTIVATED_LAUNCH_ARG L"-ToastActivated" + +using namespace ABI::Windows::UI::Notifications; + +class DesktopNotificationHistoryCompat; + +namespace DesktopNotificationManagerCompat +{ + /// + /// If not running under the Desktop Bridge, you must call this method to register your AUMID with the Compat library and to + /// register your COM CLSID and EXE in LocalServer32 registry. Feel free to call this regardless, and we will no-op if running + /// under Desktop Bridge. Call this upon application startup, before calling any other APIs. + /// + /// An AUMID that uniquely identifies your application. + /// The CLSID of your NotificationActivator class. + HRESULT RegisterAumidAndComServer(const wchar_t *aumid, GUID clsid); + + /// + /// Registers your module to handle COM activations. Call this upon application startup. + /// + HRESULT RegisterActivator(); + + /// + /// Creates a toast notifier. You must have called RegisterActivator first (and also RegisterAumidAndComServer if you're a classic Win32 app), or this will throw an exception. + /// + HRESULT CreateToastNotifier(IToastNotifier** notifier); + + /// + /// Creates an XmlDocument initialized with the specified string. This is simply a convenience helper method. + /// + HRESULT CreateXmlDocumentFromString(const wchar_t *xmlString, ABI::Windows::Data::Xml::Dom::IXmlDocument** doc); + + /// + /// Creates a toast notification. This is simply a convenience helper method. + /// + HRESULT CreateToastNotification(ABI::Windows::Data::Xml::Dom::IXmlDocument* content, IToastNotification** notification); + + /// + /// Gets the DesktopNotificationHistoryCompat object. You must have called RegisterActivator first (and also RegisterAumidAndComServer if you're a classic Win32 app), or this will throw an exception. + /// + HRESULT get_History(std::unique_ptr* history); + + /// + /// Gets a boolean representing whether http images can be used within toasts. This is true if running under Desktop Bridge. + /// + bool CanUseHttpImages(); +} + +class DesktopNotificationHistoryCompat +{ +public: + + /// + /// Removes all notifications sent by this app from action center. + /// + HRESULT Clear(); + + /// + /// Gets all notifications sent by this app that are currently still in Action Center. + /// + HRESULT GetHistory(ABI::Windows::Foundation::Collections::IVectorView** history); + + /// + /// Removes an individual toast, with the specified tag label, from action center. + /// + /// The tag label of the toast notification to be removed. + HRESULT Remove(const wchar_t *tag); + + /// + /// Removes a toast notification from the action using the notification's tag and group labels. + /// + /// The tag label of the toast notification to be removed. + /// The group label of the toast notification to be removed. + HRESULT RemoveGroupedTag(const wchar_t *tag, const wchar_t *group); + + /// + /// Removes a group of toast notifications, identified by the specified group label, from action center. + /// + /// The group label of the toast notifications to be removed. + HRESULT RemoveGroup(const wchar_t *group); + + /// + /// Do not call this. Instead, call DesktopNotificationManagerCompat.get_History() to obtain an instance. + /// + DesktopNotificationHistoryCompat(const wchar_t *aumid, Microsoft::WRL::ComPtr history); + +private: + std::wstring m_aumid; + Microsoft::WRL::ComPtr m_history = nullptr; +}; \ No newline at end of file From 014c3b72de502b2c9265ea456d83215448a0b32c Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 27 Jun 2024 17:13:11 -0400 Subject: [PATCH 033/112] Hiding old code for now --- ...cations_plugin.cpp => _flutter_local_notifications_plugin.cpp} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename flutter_local_notifications/windows/{flutter_local_notifications_plugin.cpp => _flutter_local_notifications_plugin.cpp} (100%) diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/_flutter_local_notifications_plugin.cpp similarity index 100% rename from flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp rename to flutter_local_notifications/windows/_flutter_local_notifications_plugin.cpp From 3568ad0b744e883b732b7c97d77f06925bdf0e3b Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 27 Jun 2024 18:58:40 -0400 Subject: [PATCH 034/112] one more try at progress --- .../platform_specifics/windows/notification_progress.dart | 3 ++- .../windows/__flutter_local_notifications_plugin.cpp | 5 +++++ ...ons_plugin.cpp => flutter_local_notifications_plugin.cpp} | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 flutter_local_notifications/windows/__flutter_local_notifications_plugin.cpp rename flutter_local_notifications/windows/{_flutter_local_notifications_plugin.cpp => flutter_local_notifications_plugin.cpp} (99%) diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart index a0691fa7b..dd5e368aa 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart @@ -36,7 +36,8 @@ class WindowsProgressBar { 'progress', attributes: { 'status': status, - 'value': value?.toString() ?? 'indeterminate', + // 'value': value?.toString() ?? 'indeterminate', + 'value': '{progressValue}', if (title != null) 'title': title!, if (percentageOverride != null) 'valueStringOverride': percentageOverride! } diff --git a/flutter_local_notifications/windows/__flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/__flutter_local_notifications_plugin.cpp new file mode 100644 index 000000000..61dc6fb36 --- /dev/null +++ b/flutter_local_notifications/windows/__flutter_local_notifications_plugin.cpp @@ -0,0 +1,5 @@ +#include "DesktopNotificationManagerCompat.h" +#include +#include + +// class NotificationActivator WrlSealed WrlFinal : public RuntimeClass, diff --git a/flutter_local_notifications/windows/_flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp similarity index 99% rename from flutter_local_notifications/windows/_flutter_local_notifications_plugin.cpp rename to flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index bb69cbe52..f51129375 100644 --- a/flutter_local_notifications/windows/_flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -18,6 +18,8 @@ #include #include +#include + // For getPlatformVersion; remove unless needed for your plugin implementation. #include @@ -30,6 +32,8 @@ #include using namespace winrt::Windows::Data::Xml::Dom; + + namespace { class FlutterLocalNotificationsPlugin : public flutter::Plugin { From 5089d4b06fd37e8513b808bc3f29492df0668d3b Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 27 Jun 2024 19:48:58 -0400 Subject: [PATCH 035/112] Experimental update support --- .../platform_flutter_local_notifications.dart | 24 ++++++++++-- .../windows/notification_progress.dart | 4 +- .../flutter_local_notifications_plugin.cpp | 39 +++++++++++++++---- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 98b7a1e58..92cd9b920 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -37,6 +37,10 @@ import 'tz_datetime_mapper.dart'; const MethodChannel _channel = MethodChannel('dexterous.com/flutter/local_notifications'); +extension on Iterable { + E? get firstOrNull => isEmpty ? null : first; +} + /// An implementation of a local notifications platform using method channels. class MethodChannelFlutterLocalNotificationsPlugin extends FlutterLocalNotificationsPlatform { @@ -1020,11 +1024,23 @@ class WindowsFlutterLocalNotificationsPlugin payload: payload, notificationDetails: notificationDetails, ); - await _channel.invokeMethod('show', { + final WindowsProgressBar? progressBar = + notificationDetails?.progressBars.firstOrNull; + print(xml); + final opts = { 'id': id, 'group': group, - 'platformSpecifics': {'rawXml': xml}, - }); + 'platformSpecifics': { + 'rawXml': xml, + if (progressBar != null) ...{ + 'progressValue': progressBar.value?.toString() ?? 'indeterminate', + if (progressBar.percentageOverride != null) + 'progressString': progressBar.percentageOverride!, + } + }, + }; + print("opts: $opts"); + await _channel.invokeMethod('show', opts); } @override @@ -1069,7 +1085,7 @@ class WindowsFlutterLocalNotificationsPlugin String? label, }) => _channel.invokeMethod('update', { 'id': id, - if (value != null) 'value': value, + 'value': value?.toString() ?? 'indeterminate', if (label != null) 'label': label, }); diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart index dd5e368aa..a5df6fe92 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart @@ -36,10 +36,10 @@ class WindowsProgressBar { 'progress', attributes: { 'status': status, - // 'value': value?.toString() ?? 'indeterminate', 'value': '{progressValue}', if (title != null) 'title': title!, - if (percentageOverride != null) 'valueStringOverride': percentageOverride! + if (percentageOverride != null) + 'valueStringOverride': '{progressString}', } ); } diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index f51129375..581483024 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -118,7 +118,7 @@ namespace { /// A float (0.0 - 1.0) that determines the progress /// A label to display instead of the percentage void UpdateProgress(const int id, - const std::optional value, + const std::optional value, const std::optional label); }; @@ -252,7 +252,7 @@ namespace { else if (method_name == Method::UPDATE && toastNotifier.has_value()) { const auto args = std::get_if(method_call.arguments()); const auto id = Utils::GetMapValue("id", args).value(); - const auto value = Utils::GetMapValue("value", args); + const auto value = Utils::GetMapValue("value", args); const auto label = Utils::GetMapValue("label", args); UpdateProgress(id, value, label); result->Success(); @@ -331,6 +331,26 @@ namespace { else { notif.Group(_aumid); } + + const auto progressValue = Utils::GetMapValue("progressValue", &platformSpecifics.value()); + const auto progressString = Utils::GetMapValue("progressString", &platformSpecifics.value()); + winrt::Windows::UI::Notifications::NotificationData data; + std::cout << "Has value? " + << progressValue.has_value() + << std::endl; + if (progressValue.has_value()) { + data.Values().Insert(winrt::to_hstring("progressValue"), winrt::to_hstring(progressValue.value())); + std::cout << "Got value: " + << progressValue.value() + << std::endl; + } + if (progressString.has_value()) { + data.Values().Insert(winrt::to_hstring("progressString"), winrt::to_hstring(progressString.value())); + std::cout << "Got progress: " + << progressString.value() + << std::endl; + } + notif.Data(data); toastNotifier.value().Show(notif); } @@ -362,10 +382,8 @@ namespace { toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); } const auto history = toastNotificationHistory.value().GetHistory(); - const uint32_t size = history.Size(); - for (uint32_t i = 0; i < size; i++) { + for (const auto notif : history) { flutter::EncodableMap data; - const auto notif = history.GetAt(i); const auto tag = notif.Tag(); const auto tagString = winrt::to_string(tag); const auto tagInt = std::stoi(tagString); @@ -401,18 +419,25 @@ namespace { void FlutterLocalNotificationsPlugin::UpdateProgress( const int id, - const std::optional value, + const std::optional value, const std::optional label ) { const auto tag = winrt::to_hstring(id); winrt::Windows::UI::Notifications::NotificationData data; + std::cout << "Has value?" + << value.has_value() + << std::endl; if (value.has_value()) { data.Values().Insert(winrt::to_hstring("progressValue"), winrt::to_hstring(value.value())); + std::cout << "Value: " + << value.value() + << std::endl; } if (label.has_value()) { - data.Values().Insert(winrt::to_hstring("progressValueString"), winrt::to_hstring(label.value())); + data.Values().Insert(winrt::to_hstring("progressString"), winrt::to_hstring(label.value())); } + toastNotifier.value().Update(data, tag); } } From f5f46881f8d18d7a97ef256ff5a07ab6dc627561 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 27 Jun 2024 19:59:50 -0400 Subject: [PATCH 036/112] V2 --- .../lib/src/platform_flutter_local_notifications.dart | 2 +- .../windows/flutter_local_notifications_plugin.cpp | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 92cd9b920..950937831 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -1079,7 +1079,7 @@ class WindowsFlutterLocalNotificationsPlugin /// /// [value] corresponds to [WindowsProgressBar.value] and [label] with /// [WindowsProgressBar.percentageOverride]. - Future updateProgress({ + Future updateProgress({ required int id, double? value, String? label, diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 581483024..1671d2e01 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -117,7 +117,7 @@ namespace { /// A unique ID that identifies this progress bar. /// A float (0.0 - 1.0) that determines the progress /// A label to display instead of the percentage - void UpdateProgress(const int id, + int UpdateProgress(const int id, const std::optional value, const std::optional label); }; @@ -254,8 +254,7 @@ namespace { const auto id = Utils::GetMapValue("id", args).value(); const auto value = Utils::GetMapValue("value", args); const auto label = Utils::GetMapValue("label", args); - UpdateProgress(id, value, label); - result->Success(); + result->Success(UpdateProgress(id, value, label)); } else { result->NotImplemented(); @@ -417,7 +416,7 @@ namespace { toastNotifier.value().AddToSchedule(notif); } - void FlutterLocalNotificationsPlugin::UpdateProgress( + int FlutterLocalNotificationsPlugin::UpdateProgress( const int id, const std::optional value, const std::optional label @@ -437,7 +436,7 @@ namespace { if (label.has_value()) { data.Values().Insert(winrt::to_hstring("progressString"), winrt::to_hstring(label.value())); } - toastNotifier.value().Update(data, tag); + return (int) toastNotifier.value().Update(data, tag); } } From fbe9c03e8da6675af23ea458da93aef22e1b2acd Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Fri, 28 Jun 2024 02:56:41 -0400 Subject: [PATCH 037/112] Working progress updates --- .../DesktopNotificationManagerCompat.cpp | 290 ------------------ .../DesktopNotificationManagerCompat.h | 108 ------- .../__flutter_local_notifications_plugin.cpp | 5 - .../flutter_local_notifications_plugin.cpp | 35 +-- .../flutter_local_notifications_plugin.h | 2 + 5 files changed, 19 insertions(+), 421 deletions(-) delete mode 100644 flutter_local_notifications/windows/DesktopNotificationManagerCompat.cpp delete mode 100644 flutter_local_notifications/windows/DesktopNotificationManagerCompat.h delete mode 100644 flutter_local_notifications/windows/__flutter_local_notifications_plugin.cpp diff --git a/flutter_local_notifications/windows/DesktopNotificationManagerCompat.cpp b/flutter_local_notifications/windows/DesktopNotificationManagerCompat.cpp deleted file mode 100644 index a7c9401a2..000000000 --- a/flutter_local_notifications/windows/DesktopNotificationManagerCompat.cpp +++ /dev/null @@ -1,290 +0,0 @@ -// ****************************************************************** -// Copyright (c) Microsoft. All rights reserved. -// This code is licensed under the MIT License (MIT). -// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH -// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. -// ****************************************************************** - -#include "DesktopNotificationManagerCompat.h" -#include -#include - -#define RETURN_IF_FAILED(hr) do { HRESULT _hrTemp = hr; if (FAILED(_hrTemp)) { return _hrTemp; } } while (false) - -using namespace ABI::Windows::Data::Xml::Dom; -using namespace Microsoft::WRL; -using namespace Microsoft::WRL::Wrappers; - -namespace DesktopNotificationManagerCompat -{ - HRESULT RegisterComServer(GUID clsid, const wchar_t exePath[]); - HRESULT EnsureRegistered(); - bool IsRunningAsUwp(); - - bool s_registeredAumidAndComServer = false; - std::wstring s_aumid; - bool s_registeredActivator = false; - bool s_hasCheckedIsRunningAsUwp = false; - bool s_isRunningAsUwp = false; - - HRESULT RegisterAumidAndComServer(const wchar_t *aumid, GUID clsid) - { - // If running as Desktop Bridge - if (IsRunningAsUwp()) - { - // Clear the AUMID since Desktop Bridge doesn't use it, and then we're done. - // Desktop Bridge apps are registered with platform through their manifest. - // Their LocalServer32 key is also registered through their manifest. - s_aumid = L""; - s_registeredAumidAndComServer = true; - return S_OK; - } - - // Copy the aumid - s_aumid = std::wstring(aumid); - - // Get the EXE path - wchar_t exePath[MAX_PATH]; - DWORD charWritten = ::GetModuleFileName(nullptr, exePath, ARRAYSIZE(exePath)); - RETURN_IF_FAILED(charWritten > 0 ? S_OK : HRESULT_FROM_WIN32(::GetLastError())); - - // Register the COM server - RETURN_IF_FAILED(RegisterComServer(clsid, exePath)); - - s_registeredAumidAndComServer = true; - return S_OK; - } - - HRESULT RegisterActivator() - { - // Module needs a callback registered before it can be used. - // Since we don't care about when it shuts down, we'll pass an empty lambda here. - Module::Create([] {}); - - // If a local server process only hosts the COM object then COM expects - // the COM server host to shutdown when the references drop to zero. - // Since the user might still be using the program after activating the notification, - // we don't want to shutdown immediately. Incrementing the object count tells COM that - // we aren't done yet. - Module::GetModule().IncrementObjectCount(); - - RETURN_IF_FAILED(Module::GetModule().RegisterObjects()); - - s_registeredActivator = true; - return S_OK; - } - - HRESULT RegisterComServer(GUID clsid, const wchar_t exePath[]) - { - // Turn the GUID into a string - OLECHAR* clsidOlechar; - StringFromCLSID(clsid, &clsidOlechar); - std::wstring clsidStr(clsidOlechar); - ::CoTaskMemFree(clsidOlechar); - - // Create the subkey - // Something like SOFTWARE\Classes\CLSID\{23A5B06E-20BB-4E7E-A0AC-6982ED6A6041}\LocalServer32 - std::wstring subKey = LR"(SOFTWARE\Classes\CLSID\)" + clsidStr + LR"(\LocalServer32)"; - - // Include -ToastActivated launch args on the exe - std::wstring exePathStr(exePath); - exePathStr = L"\"" + exePathStr + L"\" " + TOAST_ACTIVATED_LAUNCH_ARG; - - // We don't need to worry about overflow here as ::GetModuleFileName won't - // return anything bigger than the max file system path (much fewer than max of DWORD). - DWORD dataSize = static_cast((exePathStr.length() + 1) * sizeof(WCHAR)); - - // Register the EXE for the COM server - return HRESULT_FROM_WIN32(::RegSetKeyValue( - HKEY_CURRENT_USER, - subKey.c_str(), - nullptr, - REG_SZ, - reinterpret_cast(exePathStr.c_str()), - dataSize)); - } - - HRESULT CreateToastNotifier(IToastNotifier **notifier) - { - RETURN_IF_FAILED(EnsureRegistered()); - - ComPtr toastStatics; - RETURN_IF_FAILED(Windows::Foundation::GetActivationFactory( - HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), - &toastStatics)); - - if (s_aumid.empty()) - { - return toastStatics->CreateToastNotifier(notifier); - } - else - { - return toastStatics->CreateToastNotifierWithId(HStringReference(s_aumid.c_str()).Get(), notifier); - } - } - - HRESULT CreateXmlDocumentFromString(const wchar_t *xmlString, IXmlDocument **doc) - { - ComPtr answer; - RETURN_IF_FAILED(Windows::Foundation::ActivateInstance(HStringReference(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument).Get(), &answer)); - - ComPtr docIO; - RETURN_IF_FAILED(answer.As(&docIO)); - - // Load the XML string - RETURN_IF_FAILED(docIO->LoadXml(HStringReference(xmlString).Get())); - - return answer.CopyTo(doc); - } - - HRESULT CreateToastNotification(IXmlDocument *content, IToastNotification **notification) - { - ComPtr factory; - RETURN_IF_FAILED(Windows::Foundation::GetActivationFactory( - HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), - &factory)); - - return factory->CreateToastNotification(content, notification); - } - - HRESULT get_History(std::unique_ptr* history) - { - RETURN_IF_FAILED(EnsureRegistered()); - - ComPtr toastStatics; - RETURN_IF_FAILED(Windows::Foundation::GetActivationFactory( - HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), - &toastStatics)); - - ComPtr toastStatics2; - RETURN_IF_FAILED(toastStatics.As(&toastStatics2)); - - ComPtr nativeHistory; - RETURN_IF_FAILED(toastStatics2->get_History(&nativeHistory)); - - *history = std::unique_ptr(new DesktopNotificationHistoryCompat(s_aumid.c_str(), nativeHistory)); - return S_OK; - } - - bool CanUseHttpImages() - { - return IsRunningAsUwp(); - } - - HRESULT EnsureRegistered() - { - // If not registered AUMID yet - if (!s_registeredAumidAndComServer) - { - // Check if Desktop Bridge - if (IsRunningAsUwp()) - { - // Implicitly registered, all good! - s_registeredAumidAndComServer = true; - } - else - { - // Otherwise, incorrect usage, must call RegisterAumidAndComServer first - return E_ILLEGAL_METHOD_CALL; - } - } - - // If not registered activator yet - if (!s_registeredActivator) - { - // Incorrect usage, must call RegisterActivator first - return E_ILLEGAL_METHOD_CALL; - } - - return S_OK; - } - - bool IsRunningAsUwp() - { - if (!s_hasCheckedIsRunningAsUwp) - { - // https://stackoverflow.com/questions/39609643/determine-if-c-application-is-running-as-a-uwp-app-in-desktop-bridge-project - UINT32 length; - wchar_t packageFamilyName[PACKAGE_FAMILY_NAME_MAX_LENGTH + 1]; - LONG result = GetPackageFamilyName(GetCurrentProcess(), &length, packageFamilyName); - s_isRunningAsUwp = result == ERROR_SUCCESS; - s_hasCheckedIsRunningAsUwp = true; - } - - return s_isRunningAsUwp; - } -} - -DesktopNotificationHistoryCompat::DesktopNotificationHistoryCompat(const wchar_t *aumid, ComPtr history) -{ - m_aumid = std::wstring(aumid); - m_history = history; -} - -HRESULT DesktopNotificationHistoryCompat::Clear() -{ - if (m_aumid.empty()) - { - return m_history->Clear(); - } - else - { - return m_history->ClearWithId(HStringReference(m_aumid.c_str()).Get()); - } -} - -HRESULT DesktopNotificationHistoryCompat::GetHistory(ABI::Windows::Foundation::Collections::IVectorView **toasts) -{ - ComPtr history2; - RETURN_IF_FAILED(m_history.As(&history2)); - - if (m_aumid.empty()) - { - return history2->GetHistory(toasts); - } - else - { - return history2->GetHistoryWithId(HStringReference(m_aumid.c_str()).Get(), toasts); - } -} - -HRESULT DesktopNotificationHistoryCompat::Remove(const wchar_t *tag) -{ - if (m_aumid.empty()) - { - return m_history->Remove(HStringReference(tag).Get()); - } - else - { - return m_history->RemoveGroupedTagWithId(HStringReference(tag).Get(), HStringReference(L"").Get(), HStringReference(m_aumid.c_str()).Get()); - } -} - -HRESULT DesktopNotificationHistoryCompat::RemoveGroupedTag(const wchar_t *tag, const wchar_t *group) -{ - if (m_aumid.empty()) - { - return m_history->RemoveGroupedTag(HStringReference(tag).Get(), HStringReference(group).Get()); - } - else - { - return m_history->RemoveGroupedTagWithId(HStringReference(tag).Get(), HStringReference(group).Get(), HStringReference(m_aumid.c_str()).Get()); - } -} - -HRESULT DesktopNotificationHistoryCompat::RemoveGroup(const wchar_t *group) -{ - if (m_aumid.empty()) - { - return m_history->RemoveGroup(HStringReference(group).Get()); - } - else - { - return m_history->RemoveGroupWithId(HStringReference(group).Get(), HStringReference(m_aumid.c_str()).Get()); - } -} \ No newline at end of file diff --git a/flutter_local_notifications/windows/DesktopNotificationManagerCompat.h b/flutter_local_notifications/windows/DesktopNotificationManagerCompat.h deleted file mode 100644 index 2d63fe86d..000000000 --- a/flutter_local_notifications/windows/DesktopNotificationManagerCompat.h +++ /dev/null @@ -1,108 +0,0 @@ -// ****************************************************************** -// Copyright (c) Microsoft. All rights reserved. -// This code is licensed under the MIT License (MIT). -// THE CODE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH -// THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE. -// ****************************************************************** - -#pragma once -#include -#include -#include -#include -#include -#define TOAST_ACTIVATED_LAUNCH_ARG L"-ToastActivated" - -using namespace ABI::Windows::UI::Notifications; - -class DesktopNotificationHistoryCompat; - -namespace DesktopNotificationManagerCompat -{ - /// - /// If not running under the Desktop Bridge, you must call this method to register your AUMID with the Compat library and to - /// register your COM CLSID and EXE in LocalServer32 registry. Feel free to call this regardless, and we will no-op if running - /// under Desktop Bridge. Call this upon application startup, before calling any other APIs. - /// - /// An AUMID that uniquely identifies your application. - /// The CLSID of your NotificationActivator class. - HRESULT RegisterAumidAndComServer(const wchar_t *aumid, GUID clsid); - - /// - /// Registers your module to handle COM activations. Call this upon application startup. - /// - HRESULT RegisterActivator(); - - /// - /// Creates a toast notifier. You must have called RegisterActivator first (and also RegisterAumidAndComServer if you're a classic Win32 app), or this will throw an exception. - /// - HRESULT CreateToastNotifier(IToastNotifier** notifier); - - /// - /// Creates an XmlDocument initialized with the specified string. This is simply a convenience helper method. - /// - HRESULT CreateXmlDocumentFromString(const wchar_t *xmlString, ABI::Windows::Data::Xml::Dom::IXmlDocument** doc); - - /// - /// Creates a toast notification. This is simply a convenience helper method. - /// - HRESULT CreateToastNotification(ABI::Windows::Data::Xml::Dom::IXmlDocument* content, IToastNotification** notification); - - /// - /// Gets the DesktopNotificationHistoryCompat object. You must have called RegisterActivator first (and also RegisterAumidAndComServer if you're a classic Win32 app), or this will throw an exception. - /// - HRESULT get_History(std::unique_ptr* history); - - /// - /// Gets a boolean representing whether http images can be used within toasts. This is true if running under Desktop Bridge. - /// - bool CanUseHttpImages(); -} - -class DesktopNotificationHistoryCompat -{ -public: - - /// - /// Removes all notifications sent by this app from action center. - /// - HRESULT Clear(); - - /// - /// Gets all notifications sent by this app that are currently still in Action Center. - /// - HRESULT GetHistory(ABI::Windows::Foundation::Collections::IVectorView** history); - - /// - /// Removes an individual toast, with the specified tag label, from action center. - /// - /// The tag label of the toast notification to be removed. - HRESULT Remove(const wchar_t *tag); - - /// - /// Removes a toast notification from the action using the notification's tag and group labels. - /// - /// The tag label of the toast notification to be removed. - /// The group label of the toast notification to be removed. - HRESULT RemoveGroupedTag(const wchar_t *tag, const wchar_t *group); - - /// - /// Removes a group of toast notifications, identified by the specified group label, from action center. - /// - /// The group label of the toast notifications to be removed. - HRESULT RemoveGroup(const wchar_t *group); - - /// - /// Do not call this. Instead, call DesktopNotificationManagerCompat.get_History() to obtain an instance. - /// - DesktopNotificationHistoryCompat(const wchar_t *aumid, Microsoft::WRL::ComPtr history); - -private: - std::wstring m_aumid; - Microsoft::WRL::ComPtr m_history = nullptr; -}; \ No newline at end of file diff --git a/flutter_local_notifications/windows/__flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/__flutter_local_notifications_plugin.cpp deleted file mode 100644 index 61dc6fb36..000000000 --- a/flutter_local_notifications/windows/__flutter_local_notifications_plugin.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "DesktopNotificationManagerCompat.h" -#include -#include - -// class NotificationActivator WrlSealed WrlFinal : public RuntimeClass, diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index 1671d2e01..b0de59bff 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -32,7 +32,7 @@ #include using namespace winrt::Windows::Data::Xml::Dom; - +using namespace winrt::Windows::UI::Notifications; namespace { @@ -46,8 +46,8 @@ namespace { private: std::wstring _aumid; - std::optional toastNotifier; - std::optional toastNotificationHistory; + std::optional toastNotifier; + std::optional toastNotificationHistory; std::shared_ptr channel; // Called when a method is called on this plugin's channel from Dart. @@ -306,9 +306,9 @@ namespace { const auto user = winrt::Windows::System::User::GetDefault(); if (hasIdentity.value()) - toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::GetForUser(user).CreateToastNotifier(); + toastNotifier = ToastNotificationManager::GetForUser(user).CreateToastNotifier(); else - toastNotifier = winrt::Windows::UI::Notifications::ToastNotificationManager::GetForUser(user).CreateToastNotifier(winrt::to_hstring(aumid)); + toastNotifier = ToastNotificationManager::GetForUser(user).CreateToastNotifier(winrt::to_hstring(aumid)); return true; } @@ -322,18 +322,12 @@ namespace { XmlDocument doc; doc.LoadXml(winrt::to_hstring(rawXml.value())); - winrt::Windows::UI::Notifications::ToastNotification notif{ doc }; + ToastNotification notif{ doc }; notif.Tag(winrt::to_hstring(id)); - if (group.has_value()) { - notif.Group(winrt::to_hstring(*group)); - } - else { - notif.Group(_aumid); - } const auto progressValue = Utils::GetMapValue("progressValue", &platformSpecifics.value()); const auto progressString = Utils::GetMapValue("progressString", &platformSpecifics.value()); - winrt::Windows::UI::Notifications::NotificationData data; + NotificationData data; std::cout << "Has value? " << progressValue.has_value() << std::endl; @@ -355,7 +349,7 @@ namespace { void FlutterLocalNotificationsPlugin::CancelNotification(const int id, const std::optional& group) { if (!toastNotificationHistory.has_value()) { - toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); + toastNotificationHistory = ToastNotificationManager::History(); } if (group.has_value()) { @@ -368,7 +362,7 @@ namespace { void FlutterLocalNotificationsPlugin::CancelAllNotifications() { if (!toastNotificationHistory.has_value()) { - toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); + toastNotificationHistory = ToastNotificationManager::History(); } toastNotificationHistory.value().Clear(_aumid); for (const auto scheduled : toastNotifier.value().GetScheduledToastNotifications()) { @@ -378,7 +372,7 @@ namespace { void FlutterLocalNotificationsPlugin::GetActiveNotifications(std::vector& result) { if (!toastNotificationHistory.has_value()) { - toastNotificationHistory = winrt::Windows::UI::Notifications::ToastNotificationManager::History(); + toastNotificationHistory = ToastNotificationManager::History(); } const auto history = toastNotificationHistory.value().GetHistory(); for (const auto notif : history) { @@ -388,6 +382,11 @@ namespace { const auto tagInt = std::stoi(tagString); data[std::string("id")] = flutter::EncodableValue(tagInt); result.emplace_back(flutter::EncodableValue(data)); + + NotificationData notifData; + notifData.Values().Insert(winrt::to_hstring("progressValue"), winrt::to_hstring("0.5")); + notifData.Values().Insert(winrt::to_hstring("progressString"), winrt::to_hstring("5/10")); + toastNotifier.value().Update(notifData, tag); } } @@ -411,7 +410,7 @@ namespace { const auto secondsSinceEpoch = Utils::GetMapValue("time", &platformSpecifics).value(); time_t time(secondsSinceEpoch); const auto time2 = winrt::clock::from_time_t(time); - winrt::Windows::UI::Notifications::ScheduledToastNotification notif(doc, time2); + ScheduledToastNotification notif(doc, time2); notif.Tag(winrt::to_hstring(id)); toastNotifier.value().AddToSchedule(notif); } @@ -422,7 +421,7 @@ namespace { const std::optional label ) { const auto tag = winrt::to_hstring(id); - winrt::Windows::UI::Notifications::NotificationData data; + NotificationData data; std::cout << "Has value?" << value.has_value() diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h b/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h index d50c367fc..624af9607 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h @@ -27,6 +27,8 @@ extern "C" { FLUTTER_PLUGIN_EXPORT void FlutterLocalNotificationsPluginRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar); + + #if defined(__cplusplus) } // extern "C" #endif From 30e813151ec16a0c8bf12107854cdff4f25173f5 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Fri, 28 Jun 2024 03:19:13 -0400 Subject: [PATCH 038/112] Formatting --- .../lib/src/platform_flutter_local_notifications.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 950937831..4eb1b8e21 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -1026,8 +1026,7 @@ class WindowsFlutterLocalNotificationsPlugin ); final WindowsProgressBar? progressBar = notificationDetails?.progressBars.firstOrNull; - print(xml); - final opts = { + await _channel.invokeMethod('show', { 'id': id, 'group': group, 'platformSpecifics': { @@ -1038,9 +1037,7 @@ class WindowsFlutterLocalNotificationsPlugin 'progressString': progressBar.percentageOverride!, } }, - }; - print("opts: $opts"); - await _channel.invokeMethod('show', opts); + }); } @override From bfab5f704ffae7a2351050dd134d6878e0237caf Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Fri, 28 Jun 2024 16:03:20 -0400 Subject: [PATCH 039/112] All notifications working --- .../example/lib/main.dart | 98 ++-- .../platform_flutter_local_notifications.dart | 57 +-- .../windows/notification_progress.dart | 25 +- .../windows/CMakeLists.txt | 3 +- .../windows/flutter_local_notifications.cpp | 221 +++++++++ .../windows/flutter_local_notifications.h | 68 +++ .../flutter_local_notifications_plugin.cpp | 450 +----------------- .../flutter_local_notifications_plugin.h | 23 +- .../windows/methods.cpp | 2 +- .../methods.h | 8 +- .../windows/registration.cpp | 2 +- .../windows/registration.h | 1 + flutter_local_notifications/windows/utils.h | 27 ++ .../windows/utils/utils.h | 28 -- 14 files changed, 436 insertions(+), 577 deletions(-) create mode 100644 flutter_local_notifications/windows/flutter_local_notifications.cpp create mode 100644 flutter_local_notifications/windows/flutter_local_notifications.h rename flutter_local_notifications/windows/{include/flutter_local_notifications => }/methods.h (77%) create mode 100644 flutter_local_notifications/windows/utils.h delete mode 100644 flutter_local_notifications/windows/utils/utils.h diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 100cb6974..709f93f0c 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -1132,12 +1132,6 @@ class _HomePageState extends State { await _showWindowsNotificationWithProgress(); }, ), - PaddedElevatedButton( - buttonText: 'Show notifications with dynamic progress', - onPressed: () async { - await _showWindowsNotificationWithUpdates(); - }, - ), PaddedElevatedButton( buttonText: 'Show notitification with activation', onPressed: () async { @@ -1166,6 +1160,7 @@ class _HomePageState extends State { controller: _windowsRawXmlController, decoration: InputDecoration( hintText: 'Enter the raw xml', + helperText: 'Bindings: {message} --> Hello, World!', constraints: const BoxConstraints.tightFor( width: 600, height: 480), suffixIcon: IconButton( @@ -3038,7 +3033,11 @@ class _HomePageState extends State { Future? _showWindowsNotificationWithRawXml() => flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation() - ?.showRawXml(id: id++, xml: _windowsRawXmlController.text); + ?.showRawXml( + id: id++, + xml: _windowsRawXmlController.text, + data: {'message': 'Hello, World!'}, + ); } Future _showLinuxNotificationWithBodyMarkup() async { @@ -3375,57 +3374,60 @@ Future _showWindowsNotificationWithGroups() => flutterLocalNotificationsPl ), ); -Future _showWindowsNotificationWithProgress() => flutterLocalNotificationsPlugin.show( - id++, - 'This notification has progress bars', - 'You can have precise or indeterminate', - NotificationDetails( - windows: WindowsNotificationDetails( - progressBars: [ - WindowsProgressBar( - title: 'This has indeterminate progress', - status: 'Downloading...', - value: null, - ), - WindowsProgressBar( - title: 'This has continuous progress', - status: 'Uploading...', - value: 0.75, - ), - WindowsProgressBar( - title: 'This has discrete progress', - status: 'Syncing...', - value: 0.75, - percentageOverride: '9/12 complete' - ), - ], - ), - ), -); - -Future _showWindowsNotificationWithUpdates() async { - final int notifId = id++; - final WindowsFlutterLocalNotificationsPlugin? windows = flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation(); +Future _showWindowsNotificationWithProgress() async { + final WindowsProgressBar fastProgress = + WindowsProgressBar(id: 'fast-progress', status: 'Updating quickly...', value: 0); + final WindowsProgressBar slowProgress = + WindowsProgressBar(id: 'slow-progress', status: 'Updating slowly...', value: 0, label: '0 / 10'); + final int notificationId = id++; + final WindowsFlutterLocalNotificationsPlugin? windows = + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation(); await flutterLocalNotificationsPlugin.show( - notifId, - 'Dynamic updates', - 'The progress bar should slowly fill up', + notificationId, + 'This notification has progress bars', + 'You can have precise or indeterminate', NotificationDetails( windows: WindowsNotificationDetails( progressBars: [ - WindowsProgressBar(status: 'Updating...', value: 0), + WindowsProgressBar( + id: 'indeterminate', + title: 'This has indeterminate progress', + status: 'Downloading...', + value: null, + ), + WindowsProgressBar( + id: 'continuous', + title: 'This has continuous progress', + status: 'Uploading...', + value: 0.75, + ), + WindowsProgressBar( + id: 'discrete', + title: 'This has discrete progress', + status: 'Syncing...', + value: 0.75, + label: '9/12 complete' + ), + fastProgress, + slowProgress, ], ), ), ); - int progress = 0; - Timer.periodic(const Duration(milliseconds: 300), (Timer timer) async { - if (progress++ == 10) { - // await windows?.cancel(notifId); + + int count = 0; + Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async { + fastProgress.value = fastProgress.value! + 0.05; + slowProgress.value = count++ / 50; + fastProgress.value = fastProgress.value!.clamp(0, 1); + slowProgress.value = slowProgress.value!.clamp(0, 1); + if (fastProgress.value == 1 && slowProgress.value == 1) { return timer.cancel(); } - await windows?.updateProgress(id: notifId, value: progress / 10, label: '$progress/10 completed'); + count = count.clamp(0, 50); + slowProgress.label = '$count / 50'; + await windows?.updateProgressBar(notificationId: notificationId, progressBar: fastProgress); + await windows?.updateProgressBar(notificationId: notificationId, progressBar: slowProgress); }); } diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 4eb1b8e21..a69104308 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -37,10 +37,6 @@ import 'tz_datetime_mapper.dart'; const MethodChannel _channel = MethodChannel('dexterous.com/flutter/local_notifications'); -extension on Iterable { - E? get firstOrNull => isEmpty ? null : first; -} - /// An implementation of a local notifications platform using method channels. class MethodChannelFlutterLocalNotificationsPlugin extends FlutterLocalNotificationsPlatform { @@ -963,16 +959,20 @@ class WindowsFlutterLocalNotificationsPlugin /// Passes the raw XML to the Windows API directly. /// + /// You can replace values in the `` element with a `{placeholder}` + /// and set their values in [data] instead. Then, you may update them with + /// [updateBindings]. + /// /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). Future showRawXml({ required int id, required String xml, - String? group, + Map data = const {}, }) => _channel.invokeMethod('show', { 'id': id, - 'group': group, - 'platformSpecifics': {'rawXml': xml}, + 'rawXml': xml, + 'data': data, }); String _notificationToXml({ @@ -1024,18 +1024,13 @@ class WindowsFlutterLocalNotificationsPlugin payload: payload, notificationDetails: notificationDetails, ); - final WindowsProgressBar? progressBar = - notificationDetails?.progressBars.firstOrNull; await _channel.invokeMethod('show', { 'id': id, - 'group': group, - 'platformSpecifics': { - 'rawXml': xml, - if (progressBar != null) ...{ - 'progressValue': progressBar.value?.toString() ?? 'indeterminate', - if (progressBar.percentageOverride != null) - 'progressString': progressBar.percentageOverride!, - } + 'rawXml': xml, + 'data': { + for (final WindowsProgressBar progressBar in notificationDetails + ?.progressBars ?? [] + ) ...progressBar.data, }, }); } @@ -1065,25 +1060,31 @@ class WindowsFlutterLocalNotificationsPlugin final int secondsSinceEpoch = scheduledDate.millisecondsSinceEpoch ~/ 1000; await _channel.invokeMethod('zonedSchedule', { 'id': id, - 'platformSpecifics': { - 'rawXml': xml, - 'time': secondsSinceEpoch, - }, + 'rawXml': xml, + 'time': secondsSinceEpoch, }); } /// Updates the progress bar in the notification with the given ID. /// - /// [value] corresponds to [WindowsProgressBar.value] and [label] with - /// [WindowsProgressBar.percentageOverride]. - Future updateProgress({ + /// Note that in order to update [WindowsProgressBar.label], it must + /// not have been set to null when [show] was called. + Future updateProgressBar({ + required int notificationId, + required WindowsProgressBar progressBar, + }) => updateBindings(id: notificationId, data: progressBar.data); + + /// Updates any data binding in the given notification. + /// + /// If you use [showRawXml], you can replace any value in the `` + /// element with `{name}`, and then use this function to update that value + /// by passing `data: {'name': value}`. + Future updateBindings({ required int id, - double? value, - String? label, + required Map data, }) => _channel.invokeMethod('update', { 'id': id, - 'value': value?.toString() ?? 'indeterminate', - if (label != null) 'label': label, + 'data': data, }); Future _handleMethod(MethodCall call) async { diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart index a5df6fe92..de662c84d 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart @@ -9,12 +9,16 @@ import '../../../flutter_local_notifications.dart'; class WindowsProgressBar { /// Creates a progress bar for a Windows notification. WindowsProgressBar({ + required this.id, required this.status, required this.value, this.title, - this.percentageOverride, + this.label, }); + /// A unique ID for this progress bar. + final String id; + /// An optional title. final String? title; @@ -24,22 +28,31 @@ class WindowsProgressBar { /// The value of the progress, from 0.0 to 1.0. /// /// Setting this to null indicates a indeterminate progress bar. - final double? value; + double? value; /// Overrides the default reading as a percent with a different text. /// /// Useful for indicating discrete progress, like `3/10` instead of `30%`. - final String? percentageOverride; + String? label; /// Serializes this progress bar to XML. void toXml(XmlBuilder builder) => builder.element( 'progress', attributes: { 'status': status, - 'value': '{progressValue}', + 'value': '{$id-progressValue}', if (title != null) 'title': title!, - if (percentageOverride != null) - 'valueStringOverride': '{progressString}', + if (label != null) 'valueStringOverride': '{$id-progressString}', } ); + + /// The data bindings for this progress bar. + /// + /// To support dynamic updates, [toXml] will inject placeholder strings + /// called data bindings instead of actual values. This represents the + /// new data. + Map get data => { + '$id-progressValue': value?.toString() ?? 'indeterminate', + if (label != null) '$id-progressString': label! + }; } diff --git a/flutter_local_notifications/windows/CMakeLists.txt b/flutter_local_notifications/windows/CMakeLists.txt index 16807d156..25d846df9 100644 --- a/flutter_local_notifications/windows/CMakeLists.txt +++ b/flutter_local_notifications/windows/CMakeLists.txt @@ -33,8 +33,9 @@ endfunction() add_library(${PLUGIN_NAME} SHARED "flutter_local_notifications_plugin.cpp" + "flutter_local_notifications.cpp" "methods.cpp" - "utils/utils.h" + "utils.h" "registration.cpp") # setup c++/winrt diff --git a/flutter_local_notifications/windows/flutter_local_notifications.cpp b/flutter_local_notifications/windows/flutter_local_notifications.cpp new file mode 100644 index 000000000..0aba1b88e --- /dev/null +++ b/flutter_local_notifications/windows/flutter_local_notifications.cpp @@ -0,0 +1,221 @@ +#include +#include +#include +#include +#include + +#include "flutter_local_notifications.h" +#include "methods.h" +#include "utils.h" +#include "registration.h" + +using std::string; +using namespace winrt::Windows::Data::Xml::Dom; +using namespace winrt::Windows::UI::Notifications; +using namespace flutter; +using namespace local_notifications; + +void FlutterLocalNotifications::RegisterWithRegistrar(PluginRegistrarWindows* registrar) { + const auto channel = std::make_shared( + registrar->messenger(), + "dexterous.com/flutter/local_notifications", + &flutter::StandardMethodCodec::GetInstance() + ); + auto plugin = std::make_unique(channel); + channel->SetMethodCallHandler( + [pluginPointer = plugin.get()](const auto& call, auto result) { + pluginPointer->HandleMethodCall(call, std::move(result)); + } + ); + registrar->AddPlugin(std::move(plugin)); +} + +FlutterLocalNotifications::FlutterLocalNotifications(std::shared_ptr channel) : + channel(channel) { } + +FlutterLocalNotifications::~FlutterLocalNotifications() { } + +std::optional FlutterLocalNotifications::HasIdentity() { + if (!IsWindows8OrGreater()) return false; + uint32_t length = 0; + auto error = GetCurrentPackageFullName(&length, nullptr); + if (error == APPMODEL_ERROR_NO_PACKAGE) return false; + else if (error != ERROR_INSUFFICIENT_BUFFER) return std::nullopt; + PWSTR fullName = (PWSTR) malloc(length * sizeof(*fullName)); + if (fullName == nullptr) return std::nullopt; + error = GetCurrentPackageFullName(&length, fullName); + if (error != ERROR_SUCCESS) return std::nullopt; + free(fullName); + return true; +} + +void FlutterLocalNotifications::HandleMethodCall( + const FlutterMethodCall& methodCall, + std::unique_ptr result +) { + const auto& methodName = methodCall.method_name(); + const auto args = std::get_if(methodCall.arguments()); + try { + if (methodName == Method::INITIALIZE) { + const auto appName = Utils::GetMapValue("appName", args).value(); + const auto aumid = Utils::GetMapValue("aumid", args).value(); + const auto guid = Utils::GetMapValue("guid", args).value(); + const auto iconPath = Utils::GetMapValue("iconPath", args); + const auto iconColor = Utils::GetMapValue("iconBgColor", args); + const auto value = Initialize(appName, aumid, guid, iconPath, iconColor); + result->Success(value); + } else if (methodName == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { + result->Success(); // TODO: Decide if/how this can be implemented + } else if (methodName == Method::CANCEL_ALL) { + CancelAll(); + result->Success(); + } else if (methodName == Method::CANCEL) { + const auto id = Utils::GetMapValue("id", args).value(); + CancelNotification(id); + result->Success(); + } else if (methodName == Method::SHOW) { + const auto id = Utils::GetMapValue("id", args).value(); + const auto xml = Utils::GetMapValue("rawXml", args).value(); + const auto data = Utils::GetMapValue("data", args).value(); + const auto success = ShowNotification(id, xml, data); + if (success) result->Success(); + else result->Error("invalid-xml", "Invalid XML. If you are passing raw XML yourself, try validating it first in the Notifications Visualizer app. If not, please report this as a bug to flutter_local_notifications."); + } else if (methodName == Method::SCHEDULE_NOTIFICATION) { + const auto id = Utils::GetMapValue("id", args).value(); + const auto xml = Utils::GetMapValue("rawXml", args).value(); + const auto time = Utils::GetMapValue("time", args).value(); + const auto success = ScheduleNotification(id, xml, time); + if (success) result->Success(); + else result->Error("invalid-xml", "Invalid XML. If you are passing raw XML yourself, try validating it first in the Notifications Visualizer app. If not, please report this as a bug to flutter_local_notifications."); + } else if (methodName == Method::GET_ACTIVE_NOTIFICATIONS) { + FlutterList list; + GetActiveNotifications(list); + result->Success(list); + } else if (methodName == Method::GET_PENDING_NOTIFICATIONS) { + FlutterList list; + GetPendingNotifications(list); + result->Success(list); + } else if (methodName == Method::UPDATE) { + const auto id = Utils::GetMapValue("id", args).value(); + const auto data = Utils::GetMapValue("data", args).value(); + result->Success(Update(id, data)); + } else { + result->NotImplemented(); + } + } catch (std::exception error) { + result->Error("internal", error.what()); + } catch (winrt::hresult_error error) { + result->Error(std::to_string(error.code().value), winrt::to_string(error.message())); + } catch (...) { + result->Error("internal", "An internal error occurred"); + } +} + +bool FlutterLocalNotifications::Initialize( + const std::string& appName, + const std::string& aumid, + const std::string& guid, + const std::optional& iconPath, + const std::optional& iconColor +) { + _aumid = winrt::to_hstring(aumid); + const auto didRegister = PluginRegistration::RegisterApp(aumid, appName, guid, iconPath, iconColor, channel); + if (!didRegister) return false; + const auto identityResult = HasIdentity(); + if (!identityResult.has_value()) return false; + hasIdentity = identityResult.value(); + std::cout << "Has identity? " << hasIdentity << std::endl; + toastNotifier = hasIdentity + ? ToastNotificationManager::CreateToastNotifier() + : ToastNotificationManager::CreateToastNotifier(_aumid); + toastHistory = ToastNotificationManager::History(); + return true; +} + +NotificationData dataFromMap(const FlutterMap& map) { + NotificationData data; + for (const auto pair : map) { + const auto key = winrt::to_hstring(std::get(pair.first)); + const auto value = winrt::to_hstring(std::get(pair.second)); + data.Values().Insert(key, value); + } + return data; +} + +bool FlutterLocalNotifications::ShowNotification(const int id, const string& xml, const FlutterMap& args) { + if (!toastNotifier.has_value()) return false; + XmlDocument doc; + try { doc.LoadXml(winrt::to_hstring(xml)); } + catch (winrt::hresult_error error) { return false; } + ToastNotification notification(doc); + const auto data = dataFromMap(args); + notification.Tag(winrt::to_hstring(id)); + notification.Data(data); + toastNotifier.value().Show(notification); + return true; +} + +bool FlutterLocalNotifications::ScheduleNotification(const int id, const std::string xml, const int time) { + if (!toastNotifier.has_value()) return false; + XmlDocument doc; + try { doc.LoadXml(winrt::to_hstring(xml)); } + catch (winrt::hresult_error error) { return false; } + const time_t time2(time); + const auto time3 = winrt::clock::from_time_t(time2); + ScheduledToastNotification notification(doc, time3); + notification.Tag(winrt::to_hstring(id)); + toastNotifier.value().AddToSchedule(notification); + return true; +} + +void FlutterLocalNotifications::CancelAll() { + if (!toastHistory.has_value() || !toastNotifier.has_value()) return; + if (hasIdentity) toastHistory.value().Clear(); + else toastHistory.value().Clear(_aumid); + for (const auto notification : toastNotifier.value().GetScheduledToastNotifications()) { + toastNotifier.value().RemoveFromSchedule(notification); + } +} + +void FlutterLocalNotifications::CancelNotification(int id) { + if (!toastHistory.has_value() || !toastNotifier.has_value()) return; + const auto tag = winrt::to_hstring(id); + if (hasIdentity) toastHistory.value().Remove(tag); + for (const auto notification : toastNotifier.value().GetScheduledToastNotifications()) { + if (notification.Tag() == tag) { + toastNotifier.value().RemoveFromSchedule(notification); + return; + } + } +} + +void FlutterLocalNotifications::GetActiveNotifications(FlutterList& result) { + if (!toastHistory.has_value() || !hasIdentity) return; + for (const auto notification : toastHistory.value().GetHistory()) { + FlutterMap data; + const auto tag = notification.Tag(); + const auto tagString = winrt::to_string(tag); + const auto tagInt = std::stoi(tagString); + data[std::string("id")] = flutter::EncodableValue(tagInt); + result.emplace_back(flutter::EncodableValue(data)); + } +} + +void FlutterLocalNotifications::GetPendingNotifications(FlutterList& result) { + if (!toastNotifier.has_value()) return; + for (const auto notif : toastNotifier.value().GetScheduledToastNotifications()) { + FlutterMap data; + const auto tag = notif.Tag(); + const auto tagString = winrt::to_string(tag); + const auto tagInt = std::stoi(tagString); + data[std::string("id")] = flutter::EncodableValue(tagInt); + result.emplace_back(flutter::EncodableValue(data)); + } +} + +int FlutterLocalNotifications::Update(const int id, const FlutterMap& map) { + if (!toastNotifier.has_value()) return 1; + const auto tag = winrt::to_hstring(id); + const auto data = dataFromMap(map); + return (int) toastNotifier.value().Update(data, tag); +} diff --git a/flutter_local_notifications/windows/flutter_local_notifications.h b/flutter_local_notifications/windows/flutter_local_notifications.h new file mode 100644 index 000000000..9148d14ce --- /dev/null +++ b/flutter_local_notifications/windows/flutter_local_notifications.h @@ -0,0 +1,68 @@ +#ifndef FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_H +#define FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_H + +#include +#include +#include + +#include // <-- This must be the first Windows header +#include + +#include "methods.h" + +namespace local_notifications { + +using FlutterMap = flutter::EncodableMap; +using FlutterList = flutter::EncodableList; +using FlutterMethodCall = flutter::MethodCall; +using FlutterMethodResult = flutter::MethodResult; + +class FlutterLocalNotifications : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + FlutterLocalNotifications(std::shared_ptr channel); + virtual ~FlutterLocalNotifications(); + FlutterLocalNotifications(const FlutterLocalNotifications&) = delete; + FlutterLocalNotifications& operator=(const FlutterLocalNotifications) = delete; + + private: + winrt::hstring _aumid; + std::optional toastNotifier; + std::optional toastHistory; + std::shared_ptr channel; + bool hasIdentity = false; + + std::optional HasIdentity(); + + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const FlutterMethodCall& methodCall, + std::unique_ptr result + ); + + bool Initialize( + const std::string& appName, + const std::string& aumid, + const std::string& guid, + const std::optional& iconPath, + const std::optional& iconColor + ); + + bool ShowNotification(const int id, const std::string& xml, const FlutterMap& args); + + bool ScheduleNotification(const int id, const std::string xml, const int time); + + void CancelAll(); + + void CancelNotification(const int id); + + void GetActiveNotifications(FlutterList& result); + + void GetPendingNotifications(FlutterList& result); + + int Update(const int id, const FlutterMap& map); +}; + +} + +#endif diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp index b0de59bff..0ab61ca73 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp @@ -1,447 +1,13 @@ -#include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" -#include "include/flutter_local_notifications/methods.h" -#include "utils/utils.h" -#include "registration.h" - -// This must be included before many other Windows headers. -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -// For getPlatformVersion; remove unless needed for your plugin implementation. -#include - -#include #include -#include - -#include -#include -#include - -using namespace winrt::Windows::Data::Xml::Dom; -using namespace winrt::Windows::UI::Notifications; - -namespace { - - class FlutterLocalNotificationsPlugin : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); - - FlutterLocalNotificationsPlugin(std::shared_ptr channel); - - virtual ~FlutterLocalNotificationsPlugin(); - - private: - std::wstring _aumid; - std::optional toastNotifier; - std::optional toastNotificationHistory; - std::shared_ptr channel; - - // Called when a method is called on this plugin's channel from Dart. - void HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - - /// - /// Initializes this plugin. - /// - /// The app user model ID that identifies the app. - /// The display name of the app. - /// An optional path to the icon of the app. - /// An optional background color of the icon, in AARRGGBB format. - /// Whether the initialization was successful. - bool Initialize( - const std::string& appName, - const std::string& aumid, - const std::string& guid, - const std::optional& iconPath, - const std::optional& iconBgColor); - - /// - /// Displays a single notification toast. - /// - /// A unique ID that identifies this notification. It can be used to cancel/dismiss the notification. - void ShowNotification( - const int id, - const std::optional& group, - const std::optional& platformSpecifics); - - /// - /// Dismisses the notification that has the given ID. - /// - /// The ID of the notification to be dismissed. - /// The group the notification is in. Default is the aumid of this app. - void CancelNotification(const int id, const std::optional& group); - - /// - /// Dismisses all currently active notifications. - /// - void CancelAllNotifications(); - - /// - /// Gets all active notifications. Requires an MSIX package to use. - /// & result); - - /// - /// Gets all pending or scheduled notifications. - /// & result); - - std::optional HasIdentity(); - - /// - /// Schedules a notification to be shown later. - /// - /// A unique ID that identifies this notification. It can be used to cancel/dismiss the notification. - void ScheduleNotification( - const int id, - const flutter::EncodableMap& platformSpecifics); - - /// - /// Updates a progress bar with the given id. - /// - /// A unique ID that identifies this progress bar. - /// A float (0.0 - 1.0) that determines the progress - /// A label to display instead of the percentage - int UpdateProgress(const int id, - const std::optional value, - const std::optional label); - }; - - // static - void FlutterLocalNotificationsPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarWindows* registrar) { - auto channel = - std::make_shared( - registrar->messenger(), "dexterous.com/flutter/local_notifications", - &flutter::StandardMethodCodec::GetInstance()); - - auto plugin = std::make_unique(channel); - - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto& call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - - registrar->AddPlugin(std::move(plugin)); - } - - FlutterLocalNotificationsPlugin::FlutterLocalNotificationsPlugin(std::shared_ptr channel) : - channel(channel) {} - - FlutterLocalNotificationsPlugin::~FlutterLocalNotificationsPlugin() {} - - void FlutterLocalNotificationsPlugin::HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result - ) { - const auto& method_name = method_call.method_name(); - if (method_name == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { - result->Success(); - } - else if (method_name == Method::INITIALIZE) { - const auto args = std::get_if(method_call.arguments()); - if (args != nullptr) { - try { - const auto appName = Utils::GetMapValue("appName", args).value(); - const auto aumid = Utils::GetMapValue("aumid", args).value(); - const auto guid = Utils::GetMapValue("guid", args).value(); - const auto iconPath = Utils::GetMapValue("iconPath", args); - const auto iconBgColor = Utils::GetMapValue("iconBgColor", args); - result->Success(Initialize(appName, aumid, guid, iconPath, iconBgColor)); - } - // handle exception when user provide a invalid guid. - catch (std::invalid_argument err) { - result->Error("INVALID_ARGUMENT", err.what()); - } - } - else { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); - } - } - else if (method_name == Method::SHOW) { - const auto args = std::get_if(method_call.arguments()); - if (args != nullptr && toastNotifier.has_value()) { - try { - const auto id = Utils::GetMapValue("id", args).value(); - const auto group = Utils::GetMapValue("group", args); - const auto platformSpecifics = Utils::GetMapValue("platformSpecifics", args); - - ShowNotification(id, group, platformSpecifics); - result->Success(); - } catch (winrt::hresult_error error) { - result->Error("Invalid XML", "The XML was invalid. If you used raw XML, please verify it. If not, please report this error"); - } catch (...) { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); - } - } - else { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); - } - } - else if (method_name == Method::CANCEL && toastNotifier.has_value()) { - const auto args = std::get_if(method_call.arguments()); - if (args != nullptr) { - const auto id = Utils::GetMapValue("id", args).value(); - const auto group = Utils::GetMapValue("group", args); - - CancelNotification(id, group); - result->Success(); - } - else { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); - } - } - else if (method_name == Method::CANCEL_ALL && toastNotifier.has_value()) { - CancelAllNotifications(); - result->Success(); - } - else if (method_name == Method::GET_ACTIVE_NOTIFICATIONS && toastNotifier.has_value()) { - try { - std::vector vec; - GetActiveNotifications(vec); - result->Success(flutter::EncodableValue(vec)); - } catch (std::exception error) { - result->Error("INTERNAL", error.what()); - } catch (winrt::hresult_error error) { - // Windows apps need to be in an MSIX to use this API. - // Return an empty list if that's the case - std::vector vec; - result->Success(vec); - } catch (...) { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); - } - } - else if (method_name == Method::GET_PENDING_NOTIFICATIONS && toastNotifier.has_value()) { - try { - std::vector vec; - GetPendingNotifications(vec); - result->Success(vec); - } catch (std::exception error) { - result->Error("INTERNAL", error.what()); - } catch (...) { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); - } - } - else if (method_name == Method::SCHEDULE_NOTIFICATION && toastNotifier.has_value()) { - const auto args = std::get_if(method_call.arguments()); - const auto id = Utils::GetMapValue("id", args).value(); - const auto platformSpecifics = Utils::GetMapValue("platformSpecifics", args); - try { - ScheduleNotification(id, platformSpecifics.value()); - result->Success(); - } catch (...) { - result->Error("INTERNAL", "flutter_local_notifications encountered an internal error."); - } - } - else if (method_name == Method::UPDATE && toastNotifier.has_value()) { - const auto args = std::get_if(method_call.arguments()); - const auto id = Utils::GetMapValue("id", args).value(); - const auto value = Utils::GetMapValue("value", args); - const auto label = Utils::GetMapValue("label", args); - result->Success(UpdateProgress(id, value, label)); - } - else { - result->NotImplemented(); - } - } - - std::optional FlutterLocalNotificationsPlugin::HasIdentity() { - if (!IsWindows8OrGreater()) { - // OS is windows 7 or lower - return false; - } - - UINT32 length = 0; - auto err = GetCurrentPackageFullName(&length, NULL); - if (err != ERROR_INSUFFICIENT_BUFFER) { - if (err == APPMODEL_ERROR_NO_PACKAGE) - return false; - - return std::nullopt; - } - - PWSTR fullName = (PWSTR)malloc(length * sizeof(*fullName)); - if (fullName == nullptr) - return std::nullopt; - - err = GetCurrentPackageFullName(&length, fullName); - if (err != ERROR_SUCCESS) - return std::nullopt; - - free(fullName); - - return true; - } - - bool FlutterLocalNotificationsPlugin::Initialize( - const std::string& appName, - const std::string& aumid, - const std::string& guid, - const std::optional& iconPath, - const std::optional& iconBgColor - ) { - _aumid = winrt::to_hstring(aumid); - auto didRegister = PluginRegistration::RegisterApp(aumid, appName, guid, iconPath, iconBgColor, channel); - if (!didRegister) return false; - - const auto hasIdentity = HasIdentity(); - if (!hasIdentity.has_value()) - return false; - - const auto user = winrt::Windows::System::User::GetDefault(); - if (hasIdentity.value()) - toastNotifier = ToastNotificationManager::GetForUser(user).CreateToastNotifier(); - else - toastNotifier = ToastNotificationManager::GetForUser(user).CreateToastNotifier(winrt::to_hstring(aumid)); - - return true; - } - - void FlutterLocalNotificationsPlugin::ShowNotification( - const int id, - const std::optional& group, - const std::optional& platformSpecifics - ) { - auto rawXml = Utils::GetMapValue("rawXml", &platformSpecifics.value()); - XmlDocument doc; - doc.LoadXml(winrt::to_hstring(rawXml.value())); - - ToastNotification notif{ doc }; - notif.Tag(winrt::to_hstring(id)); - - const auto progressValue = Utils::GetMapValue("progressValue", &platformSpecifics.value()); - const auto progressString = Utils::GetMapValue("progressString", &platformSpecifics.value()); - NotificationData data; - std::cout << "Has value? " - << progressValue.has_value() - << std::endl; - if (progressValue.has_value()) { - data.Values().Insert(winrt::to_hstring("progressValue"), winrt::to_hstring(progressValue.value())); - std::cout << "Got value: " - << progressValue.value() - << std::endl; - } - if (progressString.has_value()) { - data.Values().Insert(winrt::to_hstring("progressString"), winrt::to_hstring(progressString.value())); - std::cout << "Got progress: " - << progressString.value() - << std::endl; - } - notif.Data(data); - toastNotifier.value().Show(notif); - } - - void FlutterLocalNotificationsPlugin::CancelNotification(const int id, const std::optional& group) { - if (!toastNotificationHistory.has_value()) { - toastNotificationHistory = ToastNotificationManager::History(); - } - - if (group.has_value()) { - toastNotificationHistory.value().Remove(winrt::to_hstring(id), winrt::to_hstring(group.value()), _aumid); - } - else { - toastNotificationHistory.value().Remove(winrt::to_hstring(id), _aumid, _aumid); - } - } - - void FlutterLocalNotificationsPlugin::CancelAllNotifications() { - if (!toastNotificationHistory.has_value()) { - toastNotificationHistory = ToastNotificationManager::History(); - } - toastNotificationHistory.value().Clear(_aumid); - for (const auto scheduled : toastNotifier.value().GetScheduledToastNotifications()) { - toastNotifier.value().RemoveFromSchedule(scheduled); - } - } - - void FlutterLocalNotificationsPlugin::GetActiveNotifications(std::vector& result) { - if (!toastNotificationHistory.has_value()) { - toastNotificationHistory = ToastNotificationManager::History(); - } - const auto history = toastNotificationHistory.value().GetHistory(); - for (const auto notif : history) { - flutter::EncodableMap data; - const auto tag = notif.Tag(); - const auto tagString = winrt::to_string(tag); - const auto tagInt = std::stoi(tagString); - data[std::string("id")] = flutter::EncodableValue(tagInt); - result.emplace_back(flutter::EncodableValue(data)); - - NotificationData notifData; - notifData.Values().Insert(winrt::to_hstring("progressValue"), winrt::to_hstring("0.5")); - notifData.Values().Insert(winrt::to_hstring("progressString"), winrt::to_hstring("5/10")); - toastNotifier.value().Update(notifData, tag); - } - } - - void FlutterLocalNotificationsPlugin::GetPendingNotifications(std::vector& result) { - const auto scheduled = toastNotifier.value().GetScheduledToastNotifications(); - for (const auto notif : scheduled) { - flutter::EncodableMap data; - const auto tag = notif.Tag(); - const auto tagString = winrt::to_string(tag); - const auto tagInt = std::stoi(tagString); - data[std::string("id")] = flutter::EncodableValue(tagInt); - result.emplace_back(flutter::EncodableValue(data)); - } - } - - void FlutterLocalNotificationsPlugin::ScheduleNotification(const int id, const flutter::EncodableMap& platformSpecifics) { - auto rawXml = Utils::GetMapValue("rawXml", &platformSpecifics); - XmlDocument doc; - doc.LoadXml(winrt::to_hstring(rawXml.value())); - - const auto secondsSinceEpoch = Utils::GetMapValue("time", &platformSpecifics).value(); - time_t time(secondsSinceEpoch); - const auto time2 = winrt::clock::from_time_t(time); - ScheduledToastNotification notif(doc, time2); - notif.Tag(winrt::to_hstring(id)); - toastNotifier.value().AddToSchedule(notif); - } - - int FlutterLocalNotificationsPlugin::UpdateProgress( - const int id, - const std::optional value, - const std::optional label - ) { - const auto tag = winrt::to_hstring(id); - NotificationData data; - - std::cout << "Has value?" - << value.has_value() - << std::endl; - if (value.has_value()) { - data.Values().Insert(winrt::to_hstring("progressValue"), winrt::to_hstring(value.value())); - std::cout << "Value: " - << value.value() - << std::endl; - } - if (label.has_value()) { - data.Values().Insert(winrt::to_hstring("progressString"), winrt::to_hstring(label.value())); - } - return (int) toastNotifier.value().Update(data, tag); - } -} +#include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" +#include "flutter_local_notifications.h" void FlutterLocalNotificationsPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar) { - FlutterLocalNotificationsPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar)); + FlutterDesktopPluginRegistrar* registrar +) { + local_notifications::FlutterLocalNotifications::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar) + ); } diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h b/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h index 624af9607..42b72b514 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h +++ b/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h @@ -1,18 +1,7 @@ #ifndef FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ #define FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ -#pragma once -#include -#include - #include -#include -#include -#include -#include -#include - -#include #ifdef FLUTTER_PLUGIN_IMPL #define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) @@ -25,17 +14,11 @@ extern "C" { #endif FLUTTER_PLUGIN_EXPORT void FlutterLocalNotificationsPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar); - - + FlutterDesktopPluginRegistrarRef registrar +); #if defined(__cplusplus) } // extern "C" #endif -/// -/// Defines the type of the method channel used by this plugin. -/// -typedef flutter::MethodChannel PluginMethodChannel; - -#endif // FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ +#endif FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ diff --git a/flutter_local_notifications/windows/methods.cpp b/flutter_local_notifications/windows/methods.cpp index e143daa9f..2f0221b83 100644 --- a/flutter_local_notifications/windows/methods.cpp +++ b/flutter_local_notifications/windows/methods.cpp @@ -1,4 +1,4 @@ -#include "include/flutter_local_notifications/methods.h" +#include "methods.h" #include diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h b/flutter_local_notifications/windows/methods.h similarity index 77% rename from flutter_local_notifications/windows/include/flutter_local_notifications/methods.h rename to flutter_local_notifications/windows/methods.h index c6142e5b5..6efbec325 100644 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/methods.h +++ b/flutter_local_notifications/windows/methods.h @@ -1,11 +1,15 @@ #include +#include +#include + +using PluginMethodChannel = flutter::MethodChannel; + /// /// Defines names of methods of this plugin that are callable /// through Flutter's method channel. /// -namespace Method -{ +namespace Method { extern const std::string INITIALIZE; extern const std::string GET_NOTIFICATION_APP_LAUNCH_DETAILS; extern const std::string SHOW; diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index 71f5f957d..7e3360c44 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -4,7 +4,7 @@ #include "registration.h" #include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" -#include "include/flutter_local_notifications/methods.h" +#include "methods.h" #include #include diff --git a/flutter_local_notifications/windows/registration.h b/flutter_local_notifications/windows/registration.h index f82a0fd2b..0c78cc03b 100644 --- a/flutter_local_notifications/windows/registration.h +++ b/flutter_local_notifications/windows/registration.h @@ -2,6 +2,7 @@ #define PLUGIN_REGISTRATRION_H_ #include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" +#include "methods.h" #include #include diff --git a/flutter_local_notifications/windows/utils.h b/flutter_local_notifications/windows/utils.h new file mode 100644 index 000000000..809f2e42d --- /dev/null +++ b/flutter_local_notifications/windows/utils.h @@ -0,0 +1,27 @@ +#ifndef UTILS_H_ +#define UTILS_H_ + +#include +#include +#include + +#include "methods.h" + +namespace Utils { + /// + /// Retrieves the string value stored with the given key in the given EncodableMap. + /// + /// The key that maps to the desired string value. + /// The EncodableMap that stores the key-value pair. + /// The string value that the key maps to, or nullopt if none is found. + template + std::optional GetMapValue(const std::string& key, const flutter::EncodableMap* m) { + const auto pair = m->find(flutter::EncodableValue(key)); + if (pair == m->end()) return std::nullopt; + const auto &val = pair->second; + if (std::holds_alternative(val)) return std::get(val); + return std::nullopt; + } +} + +#endif // !UTILS_H diff --git a/flutter_local_notifications/windows/utils/utils.h b/flutter_local_notifications/windows/utils/utils.h deleted file mode 100644 index 4dadd8ef3..000000000 --- a/flutter_local_notifications/windows/utils/utils.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef UTILS_H_ -#define UTILS_H_ - -#include -#include - -namespace Utils { - /// - /// Retrieves the string value stored with the given key in the given EncodableMap. - /// - /// The key that maps to the desired string value. - /// The EncodabeMap that stores the key-value pair. - /// The string value that the key maps to, or nullopt if none is found. - template - std::optional GetMapValue(const std::string& key, const flutter::EncodableMap* m) { - const auto pair = m->find(flutter::EncodableValue(key)); - if (pair == m->end()) { - return std::nullopt; - } - const auto &val = pair->second; - if (std::holds_alternative(val)) { - return std::get(val); - } - return std::nullopt; - } -} - -#endif // !UTILS_H From d4ac6d2b66ed9a748d8cdcc19e559ee3c47fcf58 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Fri, 28 Jun 2024 17:54:16 -0400 Subject: [PATCH 040/112] Moved Windows examples out of main.dart; --- .../example/lib/main.dart | 379 +----------------- .../example/lib/padded_button.dart | 21 + .../example/lib/plugin.dart | 6 + .../example/lib/windows.dart | 355 ++++++++++++++++ 4 files changed, 391 insertions(+), 370 deletions(-) create mode 100644 flutter_local_notifications/example/lib/padded_button.dart create mode 100644 flutter_local_notifications/example/lib/plugin.dart create mode 100644 flutter_local_notifications/example/lib/windows.dart diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 709f93f0c..281eb5313 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -17,10 +17,9 @@ import 'package:path_provider/path_provider.dart'; import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/timezone.dart' as tz; -int id = 0; - -final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); +import 'padded_button.dart'; +import 'plugin.dart'; +import 'windows.dart' as windows; /// Streams are created so that app can respond to notification-related events /// since the plugin is initialised in the `main` function @@ -171,18 +170,13 @@ Future main() async { defaultActionName: 'Open notification', defaultIcon: AssetsLinuxIcon('icons/app_icon.png'), ); - const WindowsInitializationSettings initializationSettingsWindows = - WindowsInitializationSettings( - appName: 'Flutter Local Notifications Example', - appUserModelId: 'Com.Dexterous.FlutterLocalNotificationsExample', - guid: '68d0c89d-760f-4f79-a067-ae8d4220ccc1', - ); + final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsDarwin, macOS: initializationSettingsDarwin, linux: initializationSettingsLinux, - windows: initializationSettingsWindows, + windows: windows.initSettings, ); await flutterLocalNotificationsPlugin.initialize( initializationSettings, @@ -224,26 +218,6 @@ Future _configureLocalTimeZone() async { tz.setLocalLocation(tz.getLocation(timeZoneName!)); } -class PaddedElevatedButton extends StatelessWidget { - const PaddedElevatedButton({ - required this.buttonText, - required this.onPressed, - Key? key, - }) : super(key: key); - - final String buttonText; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), - child: ElevatedButton( - onPressed: onPressed, - child: Text(buttonText), - ), - ); -} - class HomePage extends StatefulWidget { const HomePage( this.notificationAppLaunchDetails, { @@ -1091,94 +1065,11 @@ class _HomePageState extends State { }, ), ], - if (!kIsWeb && Platform.isWindows) ...[ - const Text( - 'Windows-specific examples', - style: TextStyle(fontWeight: FontWeight.bold), - ), - PaddedElevatedButton( - buttonText: 'Show short and long notifications notification', - onPressed: () async { - await _showWindowsNotificationWithDuration(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show different scenarios', - onPressed: () async { - await _showWindowsNotificationWithScenarios(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notifications with some detail', - onPressed: () async { - await _showWindowsNotificationWithDetails(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notifications with image', - onPressed: () async { - await _showWindowsNotificationWithImages(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notifications with columns', - onPressed: () async { - await _showWindowsNotificationWithGroups(); - }, + if (!kIsWeb && Platform.isWindows) + ...windows.examples( + xmlController: _windowsRawXmlController, + showXmlNotification: _showWindowsNotificationWithRawXml, ), - PaddedElevatedButton( - buttonText: 'Show notifications with progress bar', - onPressed: () async { - await _showWindowsNotificationWithProgress(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notitification with activation', - onPressed: () async { - await _showWindowsNotificationWithActivation(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notitification with button styles', - onPressed: () async { - await _showWindowsNotificationWithButtonStyle(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notitifications in a group', - onPressed: () async { - await _showWindowsNotificationWithHeader(); - }, - ), - SizedBox( - width: 500, - child: ExpansionTile( - title: const Text('Click to expand raw XML'), - children: [TextField( - maxLines: 20, - style: const TextStyle(fontFamily: 'RobotoMono'), - controller: _windowsRawXmlController, - decoration: InputDecoration( - hintText: 'Enter the raw xml', - helperText: 'Bindings: {message} --> Hello, World!', - constraints: const BoxConstraints.tightFor( - width: 600, height: 480), - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () => _windowsRawXmlController.clear(), - ), - ), - ),] - ), - ), - const SizedBox(height: 8), - PaddedElevatedButton( - buttonText: 'Show notification with raw XML', - onPressed: () async { - await _showWindowsNotificationWithRawXml(); - }, - ), - ], ], ), ), @@ -3249,258 +3140,6 @@ Future getLinuxCapabilities() => LinuxFlutterLocalNotificationsPlugin>()! .getCapabilities(); -Future _showWindowsNotificationWithDuration() async { - await flutterLocalNotificationsPlugin.show( - id++, - 'This is a short notification', - 'This will last about 7 seconds', - NotificationDetails( - windows: WindowsNotificationDetails(duration: WindowsNotificationDuration.short), - ), - ); - await flutterLocalNotificationsPlugin.show( - id++, - 'This is a long notification', - 'This will last about 25 seconds', - NotificationDetails( - windows: WindowsNotificationDetails(duration: WindowsNotificationDuration.long), - ), - ); -} - -Future _showWindowsNotificationWithScenarios() async { - await flutterLocalNotificationsPlugin.show( - id++, - 'This is an alarm', - null, - NotificationDetails( - windows: WindowsNotificationDetails( - scenario: WindowsNotificationScenario.alarm, - actions: [ - WindowsAction(content: 'Button', arguments: 'button') - ] - ), - ), - ); - await flutterLocalNotificationsPlugin.show( - id++, - 'This is an incoming call', - null, - NotificationDetails( - windows: WindowsNotificationDetails( - scenario: WindowsNotificationScenario.incomingCall, - actions: [ - WindowsAction(content: 'Button', arguments: 'button') - ] - ), - ), - ); - await flutterLocalNotificationsPlugin.show( - id++, - 'This is a reminder', - null, - NotificationDetails( - windows: WindowsNotificationDetails( - scenario: WindowsNotificationScenario.reminder, - actions: [ - WindowsAction(content: 'Button', arguments: 'button') - ] - ), - ), - ); - await flutterLocalNotificationsPlugin.show( - id++, - 'This is an urgent notification', - null, - NotificationDetails( - windows: WindowsNotificationDetails( - scenario: WindowsNotificationScenario.urgent, - actions: [ - WindowsAction(content: 'Button', arguments: 'button') - ] - ), - ), - ); -} - -Future _showWindowsNotificationWithDetails() => flutterLocalNotificationsPlugin.show( - id++, - 'This one has more details', - 'And a different timestamp!', - NotificationDetails( - windows: WindowsNotificationDetails( - subtitle: 'This is the subtitle', - timestamp: DateTime.now().subtract(const Duration(hours: 2, minutes: 5)), - ), - ), -); - -Future _showWindowsNotificationWithImages() => flutterLocalNotificationsPlugin.show( - id++, - 'This notification has an image', - 'You can only show images from files', - NotificationDetails( - windows: WindowsNotificationDetails( - images: [ - WindowsImage( - source: File('./icons/4.0x/app_icon_density.png'), - altText: 'A beautiful image', - ), - ], - ), - ), -); - -Future _showWindowsNotificationWithGroups() => flutterLocalNotificationsPlugin.show( - id++, - 'This notification has many groups', - 'Each group stays together', - NotificationDetails( - windows: WindowsNotificationDetails( - subtitle: 'Caption text is fainter', - groups: [ - WindowsGroup([ - WindowsColumn([ - WindowsImage(source: File('icons/coworker.png'), altText: 'A coworker'), - const WindowsNotificationText(text: 'A coworker', isCaption: true), - ]), - WindowsColumn([ - WindowsImage(source: File('icons/4.0x/app_icon_density.png'), altText: 'The icon'), - const WindowsNotificationText(text: 'The icon'), - ]), - ]), - ], - ), - ), -); - -Future _showWindowsNotificationWithProgress() async { - final WindowsProgressBar fastProgress = - WindowsProgressBar(id: 'fast-progress', status: 'Updating quickly...', value: 0); - final WindowsProgressBar slowProgress = - WindowsProgressBar(id: 'slow-progress', status: 'Updating slowly...', value: 0, label: '0 / 10'); - final int notificationId = id++; - final WindowsFlutterLocalNotificationsPlugin? windows = - flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation(); - await flutterLocalNotificationsPlugin.show( - notificationId, - 'This notification has progress bars', - 'You can have precise or indeterminate', - NotificationDetails( - windows: WindowsNotificationDetails( - progressBars: [ - WindowsProgressBar( - id: 'indeterminate', - title: 'This has indeterminate progress', - status: 'Downloading...', - value: null, - ), - WindowsProgressBar( - id: 'continuous', - title: 'This has continuous progress', - status: 'Uploading...', - value: 0.75, - ), - WindowsProgressBar( - id: 'discrete', - title: 'This has discrete progress', - status: 'Syncing...', - value: 0.75, - label: '9/12 complete' - ), - fastProgress, - slowProgress, - ], - ), - ), - ); - - int count = 0; - Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async { - fastProgress.value = fastProgress.value! + 0.05; - slowProgress.value = count++ / 50; - fastProgress.value = fastProgress.value!.clamp(0, 1); - slowProgress.value = slowProgress.value!.clamp(0, 1); - if (fastProgress.value == 1 && slowProgress.value == 1) { - return timer.cancel(); - } - count = count.clamp(0, 50); - slowProgress.label = '$count / 50'; - await windows?.updateProgressBar(notificationId: notificationId, progressBar: fastProgress); - await windows?.updateProgressBar(notificationId: notificationId, progressBar: slowProgress); - }); -} - -Future _showWindowsNotificationWithActivation() => flutterLocalNotificationsPlugin.show( - id++, - 'These buttons do different things', - 'Click on each one!', - NotificationDetails( - windows: WindowsNotificationDetails( - actions: [ - WindowsAction( - content: 'Loading', - arguments: 'loading', - activationType: WindowsActivationType.background, - activationBehavior: WindowsNotificationBehavior.pendingUpdate, - ), - WindowsAction( - content: 'Google', - arguments: 'https://google.com', - activationType: WindowsActivationType.protocol, - activationBehavior: WindowsNotificationBehavior.pendingUpdate, - ), - ], - ), - ), -); - -Future _showWindowsNotificationWithButtonStyle() => flutterLocalNotificationsPlugin.show( - id++, - 'Incoming call', - 'Your best friend', - NotificationDetails( - windows: WindowsNotificationDetails( - actions: [ - WindowsAction( - content: 'Accept', - arguments: 'accept', - buttonStyle: WindowsButtonStyle.success, - ), - WindowsAction( - content: 'Reject', - arguments: 'reject', - buttonStyle: WindowsButtonStyle.critical, - ), - ], - ), - ), -); - -Future _showWindowsNotificationWithHeader() async { - const WindowsHeader header = WindowsHeader( - id: 'header', - title: 'Cool notifications', - arguments: 'header-clicked', - ); - await flutterLocalNotificationsPlugin.show( - id++, - 'This is the first notification', - null, - NotificationDetails( - windows: WindowsNotificationDetails(header: header), - ), - ); - await flutterLocalNotificationsPlugin.show( - id++, - 'This is the second notification', - null, - NotificationDetails( - windows: WindowsNotificationDetails(header: header), - ), - ); -} - class SecondPage extends StatefulWidget { const SecondPage( this.payload, { diff --git a/flutter_local_notifications/example/lib/padded_button.dart b/flutter_local_notifications/example/lib/padded_button.dart new file mode 100644 index 000000000..254377061 --- /dev/null +++ b/flutter_local_notifications/example/lib/padded_button.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class PaddedElevatedButton extends StatelessWidget { + const PaddedElevatedButton({ + required this.buttonText, + required this.onPressed, + Key? key, + }) : super(key: key); + + final String buttonText; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), + child: ElevatedButton( + onPressed: onPressed, + child: Text(buttonText), + ), + ); +} diff --git a/flutter_local_notifications/example/lib/plugin.dart b/flutter_local_notifications/example/lib/plugin.dart new file mode 100644 index 000000000..52a6d496a --- /dev/null +++ b/flutter_local_notifications/example/lib/plugin.dart @@ -0,0 +1,6 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + +int id = 0; diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart new file mode 100644 index 000000000..98d44dbf5 --- /dev/null +++ b/flutter_local_notifications/example/lib/windows.dart @@ -0,0 +1,355 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +import 'padded_button.dart'; +import 'plugin.dart'; + +const WindowsInitializationSettings initSettings = WindowsInitializationSettings( + appName: 'Flutter Local Notifications Example', + appUserModelId: 'Com.Dexterous.FlutterLocalNotificationsExample', + guid: '68d0c89d-760f-4f79-a067-ae8d4220ccc1', +); + +List examples({ + required TextEditingController xmlController, + required VoidCallback showXmlNotification, +}) => [ + const Text('Windows-specific examples', + style: TextStyle(fontWeight: FontWeight.bold), + ), + PaddedElevatedButton( + buttonText: 'Show short and long notifications notification', + onPressed: () async { + await _showWindowsNotificationWithDuration(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show different scenarios', + onPressed: () async { + await _showWindowsNotificationWithScenarios(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with some detail', + onPressed: () async { + await _showWindowsNotificationWithDetails(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with image', + onPressed: () async { + await _showWindowsNotificationWithImages(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with columns', + onPressed: () async { + await _showWindowsNotificationWithGroups(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with progress bar', + onPressed: () async { + await _showWindowsNotificationWithProgress(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notitification with activation', + onPressed: () async { + await _showWindowsNotificationWithActivation(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notitification with button styles', + onPressed: () async { + await _showWindowsNotificationWithButtonStyle(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notitifications in a group', + onPressed: () async { + await _showWindowsNotificationWithHeader(); + }, + ), + SizedBox( + width: 500, + child: ExpansionTile( + title: const Text('Click to expand raw XML'), + children: [TextField( + maxLines: 20, + style: const TextStyle(fontFamily: 'RobotoMono'), + controller: xmlController, + decoration: InputDecoration( + hintText: 'Enter the raw xml', + helperText: 'Bindings: {message} --> Hello, World!', + constraints: const BoxConstraints.tightFor( + width: 600, height: 480), + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () => xmlController.clear(), + ), + ), + ),] + ), + ), + const SizedBox(height: 8), + PaddedElevatedButton( + buttonText: 'Show notification with raw XML', + onPressed: showXmlNotification, + ), +]; + +Future _showWindowsNotificationWithDuration() async { + await flutterLocalNotificationsPlugin.show( + id++, + 'This is a short notification', + 'This will last about 7 seconds', + NotificationDetails( + windows: WindowsNotificationDetails(duration: WindowsNotificationDuration.short), + ), + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is a long notification', + 'This will last about 25 seconds', + NotificationDetails( + windows: WindowsNotificationDetails(duration: WindowsNotificationDuration.long), + ), + ); +} + +Future _showWindowsNotificationWithScenarios() async { + await flutterLocalNotificationsPlugin.show( + id++, + 'This is an alarm', + null, + NotificationDetails( + windows: WindowsNotificationDetails( + scenario: WindowsNotificationScenario.alarm, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ] + ), + ), + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is an incoming call', + null, + NotificationDetails( + windows: WindowsNotificationDetails( + scenario: WindowsNotificationScenario.incomingCall, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ] + ), + ), + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is a reminder', + null, + NotificationDetails( + windows: WindowsNotificationDetails( + scenario: WindowsNotificationScenario.reminder, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ] + ), + ), + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is an urgent notification', + null, + NotificationDetails( + windows: WindowsNotificationDetails( + scenario: WindowsNotificationScenario.urgent, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ] + ), + ), + ); +} + +Future _showWindowsNotificationWithDetails() => flutterLocalNotificationsPlugin.show( + id++, + 'This one has more details', + 'And a different timestamp!', + NotificationDetails( + windows: WindowsNotificationDetails( + subtitle: 'This is the subtitle', + timestamp: DateTime.now().subtract(const Duration(hours: 2, minutes: 5)), + ), + ), +); + +Future _showWindowsNotificationWithImages() => flutterLocalNotificationsPlugin.show( + id++, + 'This notification has an image', + 'You can only show images from files', + NotificationDetails( + windows: WindowsNotificationDetails( + images: [ + WindowsImage( + source: File('./icons/4.0x/app_icon_density.png'), + altText: 'A beautiful image', + ), + ], + ), + ), +); + +Future _showWindowsNotificationWithGroups() => flutterLocalNotificationsPlugin.show( + id++, + 'This notification has many groups', + 'Each group stays together', + NotificationDetails( + windows: WindowsNotificationDetails( + subtitle: 'Caption text is fainter', + groups: [ + WindowsGroup([ + WindowsColumn([ + WindowsImage(source: File('icons/coworker.png'), altText: 'A coworker'), + const WindowsNotificationText(text: 'A coworker', isCaption: true), + ]), + WindowsColumn([ + WindowsImage(source: File('icons/4.0x/app_icon_density.png'), altText: 'The icon'), + const WindowsNotificationText(text: 'The icon'), + ]), + ]), + ], + ), + ), +); + +Future _showWindowsNotificationWithProgress() async { + final WindowsProgressBar fastProgress = + WindowsProgressBar(id: 'fast-progress', status: 'Updating quickly...', value: 0); + final WindowsProgressBar slowProgress = + WindowsProgressBar(id: 'slow-progress', status: 'Updating slowly...', value: 0, label: '0 / 10'); + final int notificationId = id++; + final WindowsFlutterLocalNotificationsPlugin? windows = + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation(); + await flutterLocalNotificationsPlugin.show( + notificationId, + 'This notification has progress bars', + 'You can have precise or indeterminate', + NotificationDetails( + windows: WindowsNotificationDetails( + progressBars: [ + WindowsProgressBar( + id: 'indeterminate', + title: 'This has indeterminate progress', + status: 'Downloading...', + value: null, + ), + WindowsProgressBar( + id: 'continuous', + title: 'This has continuous progress', + status: 'Uploading...', + value: 0.75, + ), + WindowsProgressBar( + id: 'discrete', + title: 'This has discrete progress', + status: 'Syncing...', + value: 0.75, + label: '9/12 complete' + ), + fastProgress, + slowProgress, + ], + ), + ), + ); + + int count = 0; + Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async { + fastProgress.value = fastProgress.value! + 0.05; + slowProgress.value = count++ / 50; + fastProgress.value = fastProgress.value!.clamp(0, 1); + slowProgress.value = slowProgress.value!.clamp(0, 1); + if (fastProgress.value == 1 && slowProgress.value == 1) { + return timer.cancel(); + } + count = count.clamp(0, 50); + slowProgress.label = '$count / 50'; + await windows?.updateProgressBar(notificationId: notificationId, progressBar: fastProgress); + await windows?.updateProgressBar(notificationId: notificationId, progressBar: slowProgress); + }); +} + +Future _showWindowsNotificationWithActivation() => flutterLocalNotificationsPlugin.show( + id++, + 'These buttons do different things', + 'Click on each one!', + NotificationDetails( + windows: WindowsNotificationDetails( + actions: [ + WindowsAction( + content: 'Loading', + arguments: 'loading', + activationType: WindowsActivationType.background, + activationBehavior: WindowsNotificationBehavior.pendingUpdate, + ), + WindowsAction( + content: 'Google', + arguments: 'https://google.com', + activationType: WindowsActivationType.protocol, + activationBehavior: WindowsNotificationBehavior.pendingUpdate, + ), + ], + ), + ), +); + +Future _showWindowsNotificationWithButtonStyle() => flutterLocalNotificationsPlugin.show( + id++, + 'Incoming call', + 'Your best friend', + NotificationDetails( + windows: WindowsNotificationDetails( + actions: [ + WindowsAction( + content: 'Accept', + arguments: 'accept', + buttonStyle: WindowsButtonStyle.success, + ), + WindowsAction( + content: 'Reject', + arguments: 'reject', + buttonStyle: WindowsButtonStyle.critical, + ), + ], + ), + ), +); + +Future _showWindowsNotificationWithHeader() async { + const WindowsHeader header = WindowsHeader( + id: 'header', + title: 'Cool notifications', + arguments: 'header-clicked', + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is the first notification', + null, + NotificationDetails( + windows: WindowsNotificationDetails(header: header), + ), + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'This is the second notification', + null, + NotificationDetails( + windows: WindowsNotificationDetails(header: header), + ), + ); +} From 7301018fa6c738f2d0a36fe06615a4f5ac784fa7 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Fri, 28 Jun 2024 18:17:13 -0400 Subject: [PATCH 041/112] cleanup --- flutter_local_notifications/pubspec.yaml | 4 ---- .../windows/flutter_local_notifications.cpp | 1 - 2 files changed, 5 deletions(-) diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index 0af3f347a..81ca11451 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -41,7 +41,3 @@ flutter: environment: sdk: ">=2.17.0 <4.0.0" flutter: ">=3.0.0" - -dependency_overrides: - flutter_local_notifications_platform_interface: - path: ../flutter_local_notifications_platform_interface diff --git a/flutter_local_notifications/windows/flutter_local_notifications.cpp b/flutter_local_notifications/windows/flutter_local_notifications.cpp index 0aba1b88e..a9b26e929 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications.cpp @@ -124,7 +124,6 @@ bool FlutterLocalNotifications::Initialize( const auto identityResult = HasIdentity(); if (!identityResult.has_value()) return false; hasIdentity = identityResult.value(); - std::cout << "Has identity? " << hasIdentity << std::endl; toastNotifier = hasIdentity ? ToastNotificationManager::CreateToastNotifier() : ToastNotificationManager::CreateToastNotifier(_aumid); From 0c0ff8169dda002e4e14da892481ec8d8707f85d Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 1 Jul 2024 09:42:42 -0400 Subject: [PATCH 042/112] Hide repeating examples on Windows --- .../example/lib/main.dart | 200 +---------------- .../example/lib/repeating.dart | 211 ++++++++++++++++++ 2 files changed, 214 insertions(+), 197 deletions(-) create mode 100644 flutter_local_notifications/example/lib/repeating.dart diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 281eb5313..64c0db4d7 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -19,6 +19,7 @@ import 'package:timezone/timezone.dart' as tz; import 'padded_button.dart'; import 'plugin.dart'; +import 'repeating.dart' as repeating; import 'windows.dart' as windows; /// Streams are created so that app can respond to notification-related events @@ -430,44 +431,6 @@ class _HomePageState extends State { await _zonedScheduleAlarmClockNotification(); }, ), - PaddedElevatedButton( - buttonText: 'Repeat notification every minute', - onPressed: () async { - await _repeatNotification(); - }, - ), - PaddedElevatedButton( - buttonText: - 'Schedule daily 10:00:00 am notification in your ' - 'local time zone', - onPressed: () async { - await _scheduleDailyTenAMNotification(); - }, - ), - PaddedElevatedButton( - buttonText: - 'Schedule daily 10:00:00 am notification in your ' - "local time zone using last year's date", - onPressed: () async { - await _scheduleDailyTenAMLastYearNotification(); - }, - ), - PaddedElevatedButton( - buttonText: - 'Schedule weekly 10:00:00 am notification in your ' - 'local time zone', - onPressed: () async { - await _scheduleWeeklyTenAMNotification(); - }, - ), - PaddedElevatedButton( - buttonText: - 'Schedule weekly Monday 10:00:00 am notification ' - 'in your local time zone', - onPressed: () async { - await _scheduleWeeklyMondayTenAMNotification(); - }, - ), PaddedElevatedButton( buttonText: 'Check pending notifications', onPressed: () async { @@ -481,22 +444,6 @@ class _HomePageState extends State { }, ), ], - PaddedElevatedButton( - buttonText: - 'Schedule monthly Monday 10:00:00 am notification in ' - 'your local time zone', - onPressed: () async { - await _scheduleMonthlyMondayTenAMNotification(); - }, - ), - PaddedElevatedButton( - buttonText: - 'Schedule yearly Monday 10:00:00 am notification in ' - 'your local time zone', - onPressed: () async { - await _scheduleYearlyMondayTenAMNotification(); - }, - ), PaddedElevatedButton( buttonText: 'Show notification from silent channel', onPressed: () async { @@ -522,6 +469,8 @@ class _HomePageState extends State { await _cancelAllNotifications(); }, ), + if (!Platform.isWindows) + ...repeating.examples(context), const Divider(), const Text( 'Notifications with actions', @@ -1933,149 +1882,6 @@ class _HomePageState extends State { notificationDetails); } - Future _repeatNotification() async { - const AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'repeating channel id', 'repeating channel name', - channelDescription: 'repeating description'); - const NotificationDetails notificationDetails = - NotificationDetails(android: androidNotificationDetails); - await flutterLocalNotificationsPlugin.periodicallyShow( - id++, - 'repeating title', - 'repeating body', - RepeatInterval.everyMinute, - notificationDetails, - androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, - ); - } - - Future _scheduleDailyTenAMNotification() async { - await flutterLocalNotificationsPlugin.zonedSchedule( - 0, - 'daily scheduled notification title', - 'daily scheduled notification body', - _nextInstanceOfTenAM(), - const NotificationDetails( - android: AndroidNotificationDetails('daily notification channel id', - 'daily notification channel name', - channelDescription: 'daily notification description'), - ), - androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.time); - } - - /// To test we don't validate past dates when using `matchDateTimeComponents` - Future _scheduleDailyTenAMLastYearNotification() async { - await flutterLocalNotificationsPlugin.zonedSchedule( - 0, - 'daily scheduled notification title', - 'daily scheduled notification body', - _nextInstanceOfTenAMLastYear(), - const NotificationDetails( - android: AndroidNotificationDetails('daily notification channel id', - 'daily notification channel name', - channelDescription: 'daily notification description'), - ), - androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.time); - } - - Future _scheduleWeeklyTenAMNotification() async { - await flutterLocalNotificationsPlugin.zonedSchedule( - 0, - 'weekly scheduled notification title', - 'weekly scheduled notification body', - _nextInstanceOfTenAM(), - const NotificationDetails( - android: AndroidNotificationDetails('weekly notification channel id', - 'weekly notification channel name', - channelDescription: 'weekly notificationdescription'), - ), - androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime); - } - - Future _scheduleWeeklyMondayTenAMNotification() async { - await flutterLocalNotificationsPlugin.zonedSchedule( - 0, - 'weekly scheduled notification title', - 'weekly scheduled notification body', - _nextInstanceOfMondayTenAM(), - const NotificationDetails( - android: AndroidNotificationDetails('weekly notification channel id', - 'weekly notification channel name', - channelDescription: 'weekly notificationdescription'), - ), - androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime); - } - - Future _scheduleMonthlyMondayTenAMNotification() async { - await flutterLocalNotificationsPlugin.zonedSchedule( - 0, - 'monthly scheduled notification title', - 'monthly scheduled notification body', - _nextInstanceOfMondayTenAM(), - const NotificationDetails( - android: AndroidNotificationDetails('monthly notification channel id', - 'monthly notification channel name', - channelDescription: 'monthly notificationdescription'), - ), - androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.dayOfMonthAndTime); - } - - Future _scheduleYearlyMondayTenAMNotification() async { - await flutterLocalNotificationsPlugin.zonedSchedule( - 0, - 'yearly scheduled notification title', - 'yearly scheduled notification body', - _nextInstanceOfMondayTenAM(), - const NotificationDetails( - android: AndroidNotificationDetails('yearly notification channel id', - 'yearly notification channel name', - channelDescription: 'yearly notification description'), - ), - androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - matchDateTimeComponents: DateTimeComponents.dateAndTime); - } - - tz.TZDateTime _nextInstanceOfTenAM() { - final tz.TZDateTime now = tz.TZDateTime.now(tz.local); - tz.TZDateTime scheduledDate = - tz.TZDateTime(tz.local, now.year, now.month, now.day, 10); - if (scheduledDate.isBefore(now)) { - scheduledDate = scheduledDate.add(const Duration(days: 1)); - } - return scheduledDate; - } - - tz.TZDateTime _nextInstanceOfTenAMLastYear() { - final tz.TZDateTime now = tz.TZDateTime.now(tz.local); - return tz.TZDateTime(tz.local, now.year - 1, now.month, now.day, 10); - } - - tz.TZDateTime _nextInstanceOfMondayTenAM() { - tz.TZDateTime scheduledDate = _nextInstanceOfTenAM(); - while (scheduledDate.weekday != DateTime.monday) { - scheduledDate = scheduledDate.add(const Duration(days: 1)); - } - return scheduledDate; - } - Future _showNotificationWithNoBadge() async { const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails('no badge channel', 'no badge name', diff --git a/flutter_local_notifications/example/lib/repeating.dart b/flutter_local_notifications/example/lib/repeating.dart new file mode 100644 index 000000000..864ad07c4 --- /dev/null +++ b/flutter_local_notifications/example/lib/repeating.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:timezone/timezone.dart' as tz; + +import 'padded_button.dart'; +import 'plugin.dart'; + +List examples(BuildContext context) => [ + const Divider(), + const Text( + 'Repeating notifications', + style: TextStyle(fontWeight: FontWeight.bold), + ), + PaddedElevatedButton( + buttonText: 'Repeat notification every minute', + onPressed: () async { + await _repeatNotification(); + }, + ), + PaddedElevatedButton( + buttonText: + 'Schedule daily 10:00:00 am notification in your ' + 'local time zone', + onPressed: () async { + await _scheduleDailyTenAMNotification(); + }, + ), + PaddedElevatedButton( + buttonText: + 'Schedule daily 10:00:00 am notification in your ' + "local time zone using last year's date", + onPressed: () async { + await _scheduleDailyTenAMLastYearNotification(); + }, + ), + PaddedElevatedButton( + buttonText: + 'Schedule weekly 10:00:00 am notification in your ' + 'local time zone', + onPressed: () async { + await _scheduleWeeklyTenAMNotification(); + }, + ), + PaddedElevatedButton( + buttonText: + 'Schedule weekly Monday 10:00:00 am notification ' + 'in your local time zone', + onPressed: () async { + await _scheduleWeeklyMondayTenAMNotification(); + }, + ), + PaddedElevatedButton( + buttonText: + 'Schedule monthly Monday 10:00:00 am notification in ' + 'your local time zone', + onPressed: () async { + await _scheduleMonthlyMondayTenAMNotification(); + }, + ), + PaddedElevatedButton( + buttonText: + 'Schedule yearly Monday 10:00:00 am notification in ' + 'your local time zone', + onPressed: () async { + await _scheduleYearlyMondayTenAMNotification(); + }, + ), +]; + +/// To test we don't validate past dates when using `matchDateTimeComponents` +Future _scheduleDailyTenAMLastYearNotification() async { + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + 'daily scheduled notification title', + 'daily scheduled notification body', + _nextInstanceOfTenAMLastYear(), + const NotificationDetails( + android: AndroidNotificationDetails('daily notification channel id', + 'daily notification channel name', + channelDescription: 'daily notification description'), + ), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.time); +} + +Future _scheduleWeeklyTenAMNotification() async { + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + 'weekly scheduled notification title', + 'weekly scheduled notification body', + _nextInstanceOfTenAM(), + const NotificationDetails( + android: AndroidNotificationDetails('weekly notification channel id', + 'weekly notification channel name', + channelDescription: 'weekly notificationdescription'), + ), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime); +} + +Future _scheduleWeeklyMondayTenAMNotification() async { + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + 'weekly scheduled notification title', + 'weekly scheduled notification body', + _nextInstanceOfMondayTenAM(), + const NotificationDetails( + android: AndroidNotificationDetails('weekly notification channel id', + 'weekly notification channel name', + channelDescription: 'weekly notificationdescription'), + ), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime); +} + +Future _scheduleMonthlyMondayTenAMNotification() async { + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + 'monthly scheduled notification title', + 'monthly scheduled notification body', + _nextInstanceOfMondayTenAM(), + const NotificationDetails( + android: AndroidNotificationDetails('monthly notification channel id', + 'monthly notification channel name', + channelDescription: 'monthly notificationdescription'), + ), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.dayOfMonthAndTime); +} + +Future _scheduleYearlyMondayTenAMNotification() async { + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + 'yearly scheduled notification title', + 'yearly scheduled notification body', + _nextInstanceOfMondayTenAM(), + const NotificationDetails( + android: AndroidNotificationDetails('yearly notification channel id', + 'yearly notification channel name', + channelDescription: 'yearly notification description'), + ), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.dateAndTime); +} + +Future _repeatNotification() async { + const AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails( + 'repeating channel id', 'repeating channel name', + channelDescription: 'repeating description'); + const NotificationDetails notificationDetails = + NotificationDetails(android: androidNotificationDetails); + await flutterLocalNotificationsPlugin.periodicallyShow( + id++, + 'repeating title', + 'repeating body', + RepeatInterval.everyMinute, + notificationDetails, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + ); +} + +Future _scheduleDailyTenAMNotification() async { + await flutterLocalNotificationsPlugin.zonedSchedule( + 0, + 'daily scheduled notification title', + 'daily scheduled notification body', + _nextInstanceOfTenAM(), + const NotificationDetails( + android: AndroidNotificationDetails('daily notification channel id', + 'daily notification channel name', + channelDescription: 'daily notification description'), + ), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.time); +} + +tz.TZDateTime _nextInstanceOfTenAM() { + final tz.TZDateTime now = tz.TZDateTime.now(tz.local); + tz.TZDateTime scheduledDate = + tz.TZDateTime(tz.local, now.year, now.month, now.day, 10); + if (scheduledDate.isBefore(now)) { + scheduledDate = scheduledDate.add(const Duration(days: 1)); + } + return scheduledDate; +} + +tz.TZDateTime _nextInstanceOfTenAMLastYear() { + final tz.TZDateTime now = tz.TZDateTime.now(tz.local); + return tz.TZDateTime(tz.local, now.year - 1, now.month, now.day, 10); +} + +tz.TZDateTime _nextInstanceOfMondayTenAM() { + tz.TZDateTime scheduledDate = _nextInstanceOfTenAM(); + while (scheduledDate.weekday != DateTime.monday) { + scheduledDate = scheduledDate.add(const Duration(days: 1)); + } + return scheduledDate; +} From 7f3cccaf770f3d1fbe8fcf05f64c3453da1bb9aa Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 1 Jul 2024 09:50:37 -0400 Subject: [PATCH 043/112] Moved example XML box to bottom --- flutter_local_notifications/example/lib/windows.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index 98d44dbf5..ed9074d77 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -74,6 +74,11 @@ List examples({ await _showWindowsNotificationWithHeader(); }, ), + PaddedElevatedButton( + buttonText: 'Show notification with raw XML', + onPressed: showXmlNotification, + ), + const SizedBox(height: 8), SizedBox( width: 500, child: ExpansionTile( @@ -95,11 +100,6 @@ List examples({ ),] ), ), - const SizedBox(height: 8), - PaddedElevatedButton( - buttonText: 'Show notification with raw XML', - onPressed: showXmlNotification, - ), ]; Future _showWindowsNotificationWithDuration() async { From 3a0699ba495ea8aacd0d700a5909732767b5c890 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 1 Jul 2024 11:10:44 -0400 Subject: [PATCH 044/112] Made WindowsImage.file constructor --- .../example/lib/windows.dart | 8 ++++---- .../windows/notification_image.dart | 14 +++++++++----- .../windows/flutter_local_notifications.h | 3 +++ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index ed9074d77..ebc8617be 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -195,8 +195,8 @@ Future _showWindowsNotificationWithImages() => flutterLocalNotificationsPl NotificationDetails( windows: WindowsNotificationDetails( images: [ - WindowsImage( - source: File('./icons/4.0x/app_icon_density.png'), + WindowsImage.file( + File('./icons/4.0x/app_icon_density.png'), altText: 'A beautiful image', ), ], @@ -214,11 +214,11 @@ Future _showWindowsNotificationWithGroups() => flutterLocalNotificationsPl groups: [ WindowsGroup([ WindowsColumn([ - WindowsImage(source: File('icons/coworker.png'), altText: 'A coworker'), + WindowsImage.file(File('icons/coworker.png'), altText: 'A coworker'), const WindowsNotificationText(text: 'A coworker', isCaption: true), ]), WindowsColumn([ - WindowsImage(source: File('icons/4.0x/app_icon_density.png'), altText: 'The icon'), + WindowsImage.file(File('icons/4.0x/app_icon_density.png'), altText: 'The icon'), const WindowsNotificationText(text: 'The icon'), ]), ]), diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart index e8b2fbb4f..48d1f0755 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart @@ -21,22 +21,26 @@ enum WindowsImageCrop { /// An image in a Windows notification. class WindowsImage extends WindowsNotificationPart { /// Creates a Windows notification image. - WindowsImage({ - required File source, + WindowsImage.file( + this.file, { required this.altText, this.addQueryParams = false, this.placement, this.crop, - }) : source = source.absolute; + }); /// Whether Windows should add URL query parameters when fetching the image. final bool addQueryParams; + /// A description of the image to be used by assistive technology. final String altText; + /// The source of the image. - final File source; + final File file; + /// Where this image will be placed. Null indicates below the notification. final WindowsImagePlacement? placement; + /// How the image will be cropped. Null indicates uncropped. final WindowsImageCrop? crop; @@ -44,7 +48,7 @@ class WindowsImage extends WindowsNotificationPart { void toXml(XmlBuilder builder) => builder.element( 'image', attributes: { - 'src': Uri.file(source.path, windows: true).toString(), + 'src': Uri.file(file.absolute.path, windows: true).toString(), 'alt': altText, 'addImageQuery': addQueryParams.toString(), if (placement != null) 'placement': placement!.name, diff --git a/flutter_local_notifications/windows/flutter_local_notifications.h b/flutter_local_notifications/windows/flutter_local_notifications.h index 9148d14ce..244ccb6f1 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications.h +++ b/flutter_local_notifications/windows/flutter_local_notifications.h @@ -32,6 +32,9 @@ class FlutterLocalNotifications : public flutter::Plugin { std::shared_ptr channel; bool hasIdentity = false; + /// Checks if this app was installed using an MSIX packager. + /// + /// See: https://learn.microsoft.com/en-us/windows/msix/detect-package-identity. std::optional HasIdentity(); // Called when a method is called on this plugin's channel from Dart. From 4abc92724434468b7e700e4322ef822d6aea99f0 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 1 Jul 2024 18:15:44 -0400 Subject: [PATCH 045/112] getLaunchDetails working --- .../platform_flutter_local_notifications.dart | 48 +++++++++---------- .../windows/flutter_local_notifications.cpp | 23 ++++++++- .../windows/flutter_local_notifications.h | 8 ++-- flutter_local_notifications/windows/methods.h | 1 - .../windows/registration.cpp | 33 +++++++------ .../windows/registration.h | 41 ++++++++-------- flutter_local_notifications/windows/types.h | 15 ++++++ 7 files changed, 103 insertions(+), 66 deletions(-) create mode 100644 flutter_local_notifications/windows/types.h diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 5152f9b36..ea8f0301d 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -50,31 +50,31 @@ class MethodChannelFlutterLocalNotificationsPlugin Future cancelAll() => _channel.invokeMethod('cancelAll'); @override - Future - getNotificationAppLaunchDetails() async { + Future getNotificationAppLaunchDetails( + ) async { final Map? result = - await _channel.invokeMethod('getNotificationAppLaunchDetails'); + await _channel.invokeMethod('getNotificationAppLaunchDetails'); final Map? notificationResponse = - result != null && result.containsKey('notificationResponse') - ? result['notificationResponse'] - : null; - return result != null - ? NotificationAppLaunchDetails( - result['notificationLaunchedApp'], - notificationResponse: notificationResponse == null - ? null - : NotificationResponse( - id: notificationResponse['notificationId'], - actionId: notificationResponse['actionId'], - input: notificationResponse['input'], - notificationResponseType: NotificationResponseType.values[ - notificationResponse['notificationResponseType']], - payload: notificationResponse.containsKey('payload') - ? notificationResponse['payload'] - : null, - ), - ) - : null; + result != null && result.containsKey('notificationResponse') + ? result['notificationResponse'] : null; + return result == null ? null : NotificationAppLaunchDetails( + result['notificationLaunchedApp'], + notificationResponse: notificationResponse == null ? null + : NotificationResponse( + id: notificationResponse['notificationId'], + actionId: notificationResponse['actionId'], + input: notificationResponse['input'], + notificationResponseType: NotificationResponseType.values[ + notificationResponse['notificationResponseType']], + payload: notificationResponse.containsKey('payload') + ? notificationResponse['payload'] + : null, + data: Map.from( + notificationResponse['data'] + ?? {}, + ), + ), + ); } @override @@ -1161,7 +1161,7 @@ class WindowsFlutterLocalNotificationsPlugin /// Updates any data binding in the given notification. /// - /// If you use [showRawXml], you can replace any value in the `` + /// Instead of a text value, you can replace any value in the `` /// element with `{name}`, and then use this function to update that value /// by passing `data: {'name': value}`. Future updateBindings({ diff --git a/flutter_local_notifications/windows/flutter_local_notifications.cpp b/flutter_local_notifications/windows/flutter_local_notifications.cpp index a9b26e929..d1392ffb5 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications.cpp @@ -65,7 +65,17 @@ void FlutterLocalNotifications::HandleMethodCall( const auto value = Initialize(appName, aumid, guid, iconPath, iconColor); result->Success(value); } else if (methodName == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { - result->Success(); // TODO: Decide if/how this can be implemented + auto map = utils->launchData; + *utils->didLaunchWithNotification = true; + + const auto didLaunch = *(utils->didLaunchWithNotification); + FlutterMap outerData; + outerData[std::string("notificationLaunchedApp")] = didLaunch; + if (didLaunch) { + auto data = *(utils->launchData); + outerData[std::string("notificationResponse")] = flutter::EncodableValue(data); + } + result->Success(flutter::EncodableValue(outerData)); } else if (methodName == Method::CANCEL_ALL) { CancelAll(); result->Success(); @@ -119,7 +129,16 @@ bool FlutterLocalNotifications::Initialize( const std::optional& iconColor ) { _aumid = winrt::to_hstring(aumid); - const auto didRegister = PluginRegistration::RegisterApp(aumid, appName, guid, iconPath, iconColor, channel); + + FlutterMap launchDetails; + bool didLaunchWithNotifications = false; + RegistrationUtils rawUtils; + rawUtils.channel = channel; + rawUtils.didLaunchWithNotification = std::make_shared(didLaunchWithNotifications); + rawUtils.launchData = std::make_shared(launchDetails); + utils = std::make_shared(rawUtils); + + const auto didRegister = RegisterApp(aumid, appName, guid, iconPath, iconColor, utils); if (!didRegister) return false; const auto identityResult = HasIdentity(); if (!identityResult.has_value()) return false; diff --git a/flutter_local_notifications/windows/flutter_local_notifications.h b/flutter_local_notifications/windows/flutter_local_notifications.h index 244ccb6f1..1e36bdf29 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications.h +++ b/flutter_local_notifications/windows/flutter_local_notifications.h @@ -9,14 +9,11 @@ #include #include "methods.h" +#include "registration.h" +#include "types.h" namespace local_notifications { -using FlutterMap = flutter::EncodableMap; -using FlutterList = flutter::EncodableList; -using FlutterMethodCall = flutter::MethodCall; -using FlutterMethodResult = flutter::MethodResult; - class FlutterLocalNotifications : public flutter::Plugin { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); @@ -30,6 +27,7 @@ class FlutterLocalNotifications : public flutter::Plugin { std::optional toastNotifier; std::optional toastHistory; std::shared_ptr channel; + std::shared_ptr utils; bool hasIdentity = false; /// Checks if this app was installed using an MSIX packager. diff --git a/flutter_local_notifications/windows/methods.h b/flutter_local_notifications/windows/methods.h index 6efbec325..488ff84cb 100644 --- a/flutter_local_notifications/windows/methods.h +++ b/flutter_local_notifications/windows/methods.h @@ -3,7 +3,6 @@ #include #include -using PluginMethodChannel = flutter::MethodChannel; /// /// Defines names of methods of this plugin that are callable diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications/windows/registration.cpp index 7e3360c44..71836993a 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications/windows/registration.cpp @@ -21,9 +21,8 @@ /// /// This callback will be called when a notification sent by this plugin is clicked on. /// -struct NotificationActivationCallback : winrt::implements -{ - std::shared_ptr channel; +struct NotificationActivationCallback : winrt::implements { + std::shared_ptr utils; HRESULT __stdcall Activate( LPCWSTR app, @@ -38,12 +37,16 @@ struct NotificationActivationCallback : winrt::implementsInvokeMethod( + const auto openedWithAction = args != nullptr; + auto map = utils->launchData; + *utils->didLaunchWithNotification = true; + (*map)[std::string("notificationResponseType")] = (int) openedWithAction; + (*map)[std::string("payload")] = flutter::EncodableValue(payload); + (*map)[std::string("data")] = flutter::EncodableValue(inputData); + flutter::EncodableMap copy(*map); + utils->channel->InvokeMethod( Method::DID_RECEIVE_NOTIFICATION_RESPONSE, - std::make_unique(response), + std::make_unique(copy), nullptr ); return S_OK; @@ -59,7 +62,7 @@ struct NotificationActivationCallback : winrt::implements struct NotificationActivationCallbackFactory : winrt::implements { - std::shared_ptr channel; + std::shared_ptr utils; HRESULT __stdcall CreateInstance( IUnknown* outer, @@ -73,7 +76,7 @@ struct NotificationActivationCallbackFactory : winrt::implements(); - cb.get()->channel = channel; + cb.get()->utils = utils; return cb->QueryInterface(iid, result); } @@ -241,7 +244,7 @@ void UpdateRegistry( /// Register the notificatio activation callback factory /// and the guid of the callback. /// -bool RegisterCallback(std::shared_ptr channel, const std::string& guid) { +bool RegisterCallback(const std::string& guid, std::shared_ptr utils) { DWORD registration{}; const auto factory_ref = winrt::make_self(); @@ -253,7 +256,7 @@ bool RegisterCallback(std::shared_ptr channel, const std::s } winrt::guid rclsid(guid); - factory->channel = channel; + factory->utils = utils; winrt::check_hresult(CoRegisterClassObject( rclsid, @@ -264,14 +267,14 @@ bool RegisterCallback(std::shared_ptr channel, const std::s return true; } -bool PluginRegistration::RegisterApp( +bool RegisterApp( const std::string& aumid, const std::string& appName, const std::string& guid, const std::optional& iconPath, const std::optional& iconBgColor, - std::shared_ptr plugin + std::shared_ptr utils ) { UpdateRegistry(aumid, appName, guid, iconPath, iconBgColor); - return RegisterCallback(plugin, guid); + return RegisterCallback(guid, utils); } diff --git a/flutter_local_notifications/windows/registration.h b/flutter_local_notifications/windows/registration.h index 0c78cc03b..dbc54c19d 100644 --- a/flutter_local_notifications/windows/registration.h +++ b/flutter_local_notifications/windows/registration.h @@ -3,31 +3,34 @@ #include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" #include "methods.h" +#include "types.h" #include #include #include +struct RegistrationUtils { + std::shared_ptr launchData; + std::shared_ptr channel; + std::shared_ptr didLaunchWithNotification; +}; + /// -/// Contains logic for handling Registry values. +/// Registers the running app to the Windows Registry. /// -namespace PluginRegistration { - /// - /// Registers the running app to the Windows Registry. - /// - /// The app user model ID that identifies the app. - /// The display name of the app. - /// The display name of the app. - /// An optional path to the icon of the app. - /// An optional background color of the icon, in AARRGGBB format. - /// The instance of the plugin calling this function - bool RegisterApp( - const std::string& aumid, - const std::string& appName, - const std::string& guid, - const std::optional& iconPath, - const std::optional& iconBgColor, - std::shared_ptr plugin); -} +/// The app user model ID that identifies the app. +/// The display name of the app. +/// The display name of the app. +/// An optional path to the icon of the app. +/// An optional background color of the icon, in AARRGGBB format. +/// The instance of the plugin calling this function +bool RegisterApp( + const std::string& aumid, + const std::string& appName, + const std::string& guid, + const std::optional& iconPath, + const std::optional& iconBgColor, + std::shared_ptr utils +); #endif // !PLUGIN_REGISTRATRION_H_ diff --git a/flutter_local_notifications/windows/types.h b/flutter_local_notifications/windows/types.h new file mode 100644 index 000000000..4275318a8 --- /dev/null +++ b/flutter_local_notifications/windows/types.h @@ -0,0 +1,15 @@ +#ifndef FLUTTER_LOCAL_NOTIFICATION_TYPES_H_ +#define FLUTTER_LOCAL_NOTIFICATION_TYPES_H_ + +#include +#include +#include +#include + +using FlutterMap = flutter::EncodableMap; +using FlutterList = flutter::EncodableList; +using FlutterMethodCall = flutter::MethodCall; +using FlutterMethodResult = flutter::MethodResult; +using PluginMethodChannel = flutter::MethodChannel; + +#endif From 56f152a1cc17e31353bc4150936d2ddb60ad0600 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 1 Jul 2024 19:25:19 -0400 Subject: [PATCH 046/112] Cleanup --- .../windows/flutter_local_notifications.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/flutter_local_notifications/windows/flutter_local_notifications.cpp b/flutter_local_notifications/windows/flutter_local_notifications.cpp index d1392ffb5..c29fd19f7 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications.cpp @@ -65,9 +65,6 @@ void FlutterLocalNotifications::HandleMethodCall( const auto value = Initialize(appName, aumid, guid, iconPath, iconColor); result->Success(value); } else if (methodName == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { - auto map = utils->launchData; - *utils->didLaunchWithNotification = true; - const auto didLaunch = *(utils->didLaunchWithNotification); FlutterMap outerData; outerData[std::string("notificationLaunchedApp")] = didLaunch; From 66574c03592c3ca05fc4c803cbc01b721d09a473 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 2 Jul 2024 01:41:23 -0400 Subject: [PATCH 047/112] Made all images msix safe --- .../example/lib/main.dart | 2 +- .../windows/notification_action.dart | 30 +++++++++---------- .../windows/notification_image.dart | 10 ++++++- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 32701f44a..4e95147b8 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -1116,7 +1116,7 @@ class _HomePageState extends State { WindowsAction( content: 'Image', arguments: 'image', - imageUri: Uri.file(File('icons/coworker.png').absolute.path, windows: true), + image: File('icons/coworker.png'), ), WindowsAction( content: 'Context', diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart index ca23d14ac..1e5e25128 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart @@ -1,9 +1,13 @@ +import 'dart:io'; + import 'package:xml/xml.dart'; // NOTE: All enum values in this file have Windows RT-specific names. // If you change their Dart names, be sure to override [Enum.name]. /// Decides how the [WindowsAction] will launch the app. +/// +/// On desktop platforms, [foreground] and [background] are treated the same. enum WindowsActivationType { /// The application will launch in the foreground (the default). foreground, @@ -54,24 +58,20 @@ class WindowsAction { this.activationType = WindowsActivationType.foreground, this.activationBehavior = WindowsNotificationBehavior.dismiss, this.placement, - this.imageUri, + this.image, this.inputId, this.buttonStyle, this.tooltip, }) { - if (imageUri != null && !allowedSchemes.contains(imageUri!.scheme)) { + if (image != null && !image!.isAbsolute) { throw ArgumentError.value( - imageUri.toString(), - 'WindowsNotificationAction.imageUri', - 'URI scheme must be one of the following schemes: $allowedSchemes', + image!.path, + 'WindowsImage.file', + 'File path must be absolute', ); } } - /// The set of allowed schemes for [imageUri]. - static const Set allowedSchemes = - {'http', 'https', 'ms-appx', 'ms-appdata', 'file'}; - /// The body text of the button. final String content; @@ -93,15 +93,12 @@ class WindowsAction { /// Null indicates a regular button. final WindowsActionPlacement? placement; - /// A URI of an image to show on the button. + /// An image to show on the button. /// /// Images must be white with a transparent background, and should be /// 16x16 pixels with no padding. If you provide an image for one button, - /// you must provide images for all your buttons. - /// - /// Supported protocols are: `http`, `https`, `ms-appx`, `ms-appdata:///local`, - /// and `file`. Other protocols will throw an error. - final Uri? imageUri; + /// you should provide images for all your buttons. + final File? image; /// The ID of an input box. /// @@ -125,7 +122,8 @@ class WindowsAction { 'activationType': activationType.name, 'afterActivationBehavior': activationBehavior.name, if (placement != null) 'placement': placement!.name, - if (imageUri != null) 'imageUri': imageUri!.toString(), + if (image != null) 'imageUri': + Uri.file(image!.absolute.path, windows: true).toString(), if (inputId != null) 'hint-inputId': inputId!, if (buttonStyle != null) 'hint-buttonStyle': buttonStyle!.name, if (tooltip != null) 'hint-toolTip': tooltip!, diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart index 48d1f0755..ed102d3f9 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart @@ -27,7 +27,15 @@ class WindowsImage extends WindowsNotificationPart { this.addQueryParams = false, this.placement, this.crop, - }); + }) { + if (!file.isAbsolute) { + throw ArgumentError.value( + file.path, + 'WindowsImage.file', + 'File path must be absolute', + ); + } + } /// Whether Windows should add URL query parameters when fetching the image. final bool addQueryParams; From 29309a9257b7da1a115388f56759890dd27ad04c Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 2 Jul 2024 14:32:00 -0400 Subject: [PATCH 048/112] Catch when getLaunchDetails() is called before init() and add dynamic example --- .../example/lib/main.dart | 113 +++++++++--------- .../example/lib/windows.dart | 36 ++++++ .../windows/flutter_local_notifications.cpp | 19 +-- 3 files changed, 106 insertions(+), 62 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 4e95147b8..d44771f2b 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -89,64 +89,53 @@ Future main() async { await _configureLocalTimeZone(); - final NotificationAppLaunchDetails? notificationAppLaunchDetails = !kIsWeb && - Platform.isLinux - ? null - : await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); - String initialRoute = HomePage.routeName; - if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { - selectedNotificationPayload = - notificationAppLaunchDetails!.notificationResponse?.payload; - initialRoute = SecondPage.routeName; - } - const AndroidInitializationSettings initializationSettingsAndroid = - AndroidInitializationSettings('app_icon'); + AndroidInitializationSettings('app_icon'); final List darwinNotificationCategories = - [ - DarwinNotificationCategory( - darwinNotificationCategoryText, - actions: [ - DarwinNotificationAction.text( - 'text_1', - 'Action 1', - buttonTitle: 'Send', - placeholder: 'Placeholder', - ), - ], - ), - DarwinNotificationCategory( - darwinNotificationCategoryPlain, - actions: [ - DarwinNotificationAction.plain('id_1', 'Action 1'), - DarwinNotificationAction.plain( - 'id_2', - 'Action 2 (destructive)', - options: { - DarwinNotificationActionOption.destructive, - }, - ), - DarwinNotificationAction.plain( - navigationActionId, - 'Action 3 (foreground)', - options: { - DarwinNotificationActionOption.foreground, - }, - ), - DarwinNotificationAction.plain( - 'id_4', - 'Action 4 (auth required)', - options: { - DarwinNotificationActionOption.authenticationRequired, - }, - ), - ], - options: { - DarwinNotificationCategoryOption.hiddenPreviewShowTitle, - }, - ) - ]; + [ + DarwinNotificationCategory( + darwinNotificationCategoryText, + actions: [ + DarwinNotificationAction.text( + 'text_1', + 'Action 1', + buttonTitle: 'Send', + placeholder: 'Placeholder', + ), + ], + ), + DarwinNotificationCategory( + darwinNotificationCategoryPlain, + actions: [ + DarwinNotificationAction.plain('id_1', 'Action 1'), + DarwinNotificationAction.plain( + 'id_2', + 'Action 2 (destructive)', + options: { + DarwinNotificationActionOption.destructive, + }, + ), + DarwinNotificationAction.plain( + navigationActionId, + 'Action 3 (foreground)', + options: { + DarwinNotificationActionOption.foreground, + }, + ), + DarwinNotificationAction.plain( + 'id_4', + 'Action 4 (auth required)', + options: { + DarwinNotificationActionOption.authenticationRequired, + }, + ), + ], + options: { + DarwinNotificationCategoryOption.hiddenPreviewShowTitle, + }, + ) + ]; /// Note: permissions aren't requested here just to demonstrate that can be /// done later @@ -166,6 +155,7 @@ Future main() async { }, notificationCategories: darwinNotificationCategories, ); + final LinuxInitializationSettings initializationSettingsLinux = LinuxInitializationSettings( defaultActionName: 'Open notification', @@ -179,6 +169,7 @@ Future main() async { linux: initializationSettingsLinux, windows: windows.initSettings, ); + await flutterLocalNotificationsPlugin.initialize( initializationSettings, onDidReceiveNotificationResponse: @@ -196,6 +187,18 @@ Future main() async { }, onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); + + final NotificationAppLaunchDetails? notificationAppLaunchDetails = !kIsWeb && + Platform.isLinux + ? null + : await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); + String initialRoute = HomePage.routeName; + if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { + selectedNotificationPayload = + notificationAppLaunchDetails!.notificationResponse?.payload; + initialRoute = SecondPage.routeName; + } + runApp( MaterialApp( initialRoute: initialRoute, diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index ebc8617be..3bc667b6f 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -56,6 +56,12 @@ List examples({ await _showWindowsNotificationWithProgress(); }, ), + PaddedElevatedButton( + buttonText: 'Show notifications with dynamic content', + onPressed: () async { + await _showWindowsNotificationWithDynamic(); + }, + ), PaddedElevatedButton( buttonText: 'Show notitification with activation', onPressed: () async { @@ -284,6 +290,36 @@ Future _showWindowsNotificationWithProgress() async { }); } +Future _showWindowsNotificationWithDynamic() async { + final DateTime start = DateTime.now(); + final int notificationId = id++; + final WindowsFlutterLocalNotificationsPlugin? windows = + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation(); + await flutterLocalNotificationsPlugin.show( + notificationId, + 'Dynamic content', + 'This notification will be updated from Dart code', + NotificationDetails( + windows: WindowsNotificationDetails( + subtitle: '{stopwatch}', + ), + ), + ); + Map getBindings() => { + 'stopwatch': 'Elapsed time: ${DateTime.now().difference(start).inSeconds} seconds', + }; + await windows?.updateBindings(id: notificationId, data: getBindings()); + Timer.periodic(const Duration(seconds: 1), (Timer timer) async { + if (timer.tick > 10) { + timer.cancel(); + await flutterLocalNotificationsPlugin.cancel(notificationId); + return; + } + await windows?.updateBindings(id: notificationId, data: getBindings()); + }); +} + Future _showWindowsNotificationWithActivation() => flutterLocalNotificationsPlugin.show( id++, 'These buttons do different things', diff --git a/flutter_local_notifications/windows/flutter_local_notifications.cpp b/flutter_local_notifications/windows/flutter_local_notifications.cpp index c29fd19f7..89e257657 100644 --- a/flutter_local_notifications/windows/flutter_local_notifications.cpp +++ b/flutter_local_notifications/windows/flutter_local_notifications.cpp @@ -65,14 +65,19 @@ void FlutterLocalNotifications::HandleMethodCall( const auto value = Initialize(appName, aumid, guid, iconPath, iconColor); result->Success(value); } else if (methodName == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { - const auto didLaunch = *(utils->didLaunchWithNotification); - FlutterMap outerData; - outerData[std::string("notificationLaunchedApp")] = didLaunch; - if (didLaunch) { - auto data = *(utils->launchData); - outerData[std::string("notificationResponse")] = flutter::EncodableValue(data); + if (utils == nullptr) { + result->Error("not-initialized", "You must initialize the plugin before calling any methods"); + return; } - result->Success(flutter::EncodableValue(outerData)); + // const auto didLaunch = *(utils->didLaunchWithNotification); + // FlutterMap outerData; + // outerData[std::string("notificationLaunchedApp")] = didLaunch; + // if (didLaunch) { + // auto data = *(utils->launchData); + // outerData[std::string("notificationResponse")] = flutter::EncodableValue(data); + // } + // result->Success(flutter::EncodableValue(outerData)); + result->Success(); } else if (methodName == Method::CANCEL_ALL) { CancelAll(); result->Success(); From 370c19ef286fa288c993a08d85a696e0c7101e7b Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Sat, 6 Jul 2024 22:51:35 -0400 Subject: [PATCH 049/112] Fixed issue with special characters in file --- flutter_local_notifications/example/lib/main.dart | 2 +- flutter_local_notifications/example/lib/windows.dart | 6 +++--- .../src/platform_specifics/windows/notification_action.dart | 2 +- .../src/platform_specifics/windows/notification_audio.dart | 2 +- .../src/platform_specifics/windows/notification_image.dart | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index d44771f2b..ec9aaaa25 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -1119,7 +1119,7 @@ class _HomePageState extends State { WindowsAction( content: 'Image', arguments: 'image', - image: File('icons/coworker.png'), + image: File('icons/coworker.png').absolute, ), WindowsAction( content: 'Context', diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index 3bc667b6f..d892da496 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -202,7 +202,7 @@ Future _showWindowsNotificationWithImages() => flutterLocalNotificationsPl windows: WindowsNotificationDetails( images: [ WindowsImage.file( - File('./icons/4.0x/app_icon_density.png'), + File('./icons/4.0x/app_icon_density.png').absolute, altText: 'A beautiful image', ), ], @@ -220,11 +220,11 @@ Future _showWindowsNotificationWithGroups() => flutterLocalNotificationsPl groups: [ WindowsGroup([ WindowsColumn([ - WindowsImage.file(File('icons/coworker.png'), altText: 'A coworker'), + WindowsImage.file(File('icons/coworker.png').absolute, altText: 'A coworker'), const WindowsNotificationText(text: 'A coworker', isCaption: true), ]), WindowsColumn([ - WindowsImage.file(File('icons/4.0x/app_icon_density.png'), altText: 'The icon'), + WindowsImage.file(File('icons/4.0x/app_icon_density.png').absolute, altText: 'The icon'), const WindowsNotificationText(text: 'The icon'), ]), ]), diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart index 1e5e25128..2cc48d977 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart @@ -123,7 +123,7 @@ class WindowsAction { 'afterActivationBehavior': activationBehavior.name, if (placement != null) 'placement': placement!.name, if (image != null) 'imageUri': - Uri.file(image!.absolute.path, windows: true).toString(), + Uri.file(image!.absolute.path, windows: true).toFilePath(), if (inputId != null) 'hint-inputId': inputId!, if (buttonStyle != null) 'hint-buttonStyle': buttonStyle!.name, if (tooltip != null) 'hint-toolTip': tooltip!, diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart index 18d41149a..3751850c2 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart @@ -83,7 +83,7 @@ class WindowsNotificationAudio { required Uri file, this.shouldLoop = false, this.isSilent = false, - }) : source = file.toString() { + }) : source = file.toFilePath() { if (!allowedSchemes.contains(file.scheme)) { throw ArgumentError.value( file.toString(), diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart index ed102d3f9..26eb1fea9 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart @@ -56,7 +56,7 @@ class WindowsImage extends WindowsNotificationPart { void toXml(XmlBuilder builder) => builder.element( 'image', attributes: { - 'src': Uri.file(file.absolute.path, windows: true).toString(), + 'src': Uri.file(file.absolute.path, windows: true).toFilePath(), 'alt': altText, 'addImageQuery': addQueryParams.toString(), if (placement != null) 'placement': placement!.name, From dd335e67f17a034e7178ce7d00113b368839c5ca Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 10 Jul 2024 19:08:04 -0400 Subject: [PATCH 050/112] Added Windows FFI package --- .../example/pubspec.yaml | 2 + flutter_local_notifications/pubspec.yaml | 8 +- .../.gitignore | 29 ++++ flutter_local_notifications_windows/.metadata | 30 ++++ .../CHANGELOG.md | 3 + flutter_local_notifications_windows/LICENSE | 1 + flutter_local_notifications_windows/README.md | 92 ++++++++++++ .../analysis_options.yaml | 52 +++++++ .../ffigen.yaml | 19 +++ .../lib/ffi_bindings.dart | 69 +++++++++ .../flutter_local_notifications_windows.dart | 131 ++++++++++++++++++ .../pubspec.yaml | 74 ++++++++++ .../src/CMakeLists.txt | 17 +++ .../src/flutter_local_notifications_windows.c | 23 +++ .../src/flutter_local_notifications_windows.h | 30 ++++ .../windows/.gitignore | 17 +++ .../windows/CMakeLists.txt | 23 +++ 17 files changed, 618 insertions(+), 2 deletions(-) create mode 100644 flutter_local_notifications_windows/.gitignore create mode 100644 flutter_local_notifications_windows/.metadata create mode 100644 flutter_local_notifications_windows/CHANGELOG.md create mode 100644 flutter_local_notifications_windows/LICENSE create mode 100644 flutter_local_notifications_windows/README.md create mode 100644 flutter_local_notifications_windows/analysis_options.yaml create mode 100644 flutter_local_notifications_windows/ffigen.yaml create mode 100644 flutter_local_notifications_windows/lib/ffi_bindings.dart create mode 100644 flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart create mode 100644 flutter_local_notifications_windows/pubspec.yaml create mode 100644 flutter_local_notifications_windows/src/CMakeLists.txt create mode 100644 flutter_local_notifications_windows/src/flutter_local_notifications_windows.c create mode 100644 flutter_local_notifications_windows/src/flutter_local_notifications_windows.h create mode 100644 flutter_local_notifications_windows/windows/.gitignore create mode 100644 flutter_local_notifications_windows/windows/CMakeLists.txt diff --git a/flutter_local_notifications/example/pubspec.yaml b/flutter_local_notifications/example/pubspec.yaml index d71a364a1..4c130af22 100644 --- a/flutter_local_notifications/example/pubspec.yaml +++ b/flutter_local_notifications/example/pubspec.yaml @@ -30,6 +30,8 @@ dependency_overrides: path: ../../flutter_local_notifications_linux flutter_local_notifications_platform_interface: path: ../../flutter_local_notifications_platform_interface + flutter_local_notifications_windows: + path: ../../flutter_local_notifications_windows flutter: uses-material-design: true diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index 3c2a69530..d05ec085d 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -11,10 +11,15 @@ dependencies: flutter: sdk: flutter flutter_local_notifications_linux: ^4.0.0 + flutter_local_notifications_windows: ^1.0.0 flutter_local_notifications_platform_interface: ^7.2.0 timezone: ^0.9.0 xml: ^6.5.0 +dependency_overrides: + flutter_local_notifications_windows: + path: ../flutter_local_notifications_windows + dev_dependencies: flutter_driver: sdk: flutter @@ -35,8 +40,7 @@ flutter: pluginClass: FlutterLocalNotificationsPlugin linux: default_package: flutter_local_notifications_linux - windows: - pluginClass: FlutterLocalNotificationsPlugin + environment: sdk: ">=2.17.0 <4.0.0" diff --git a/flutter_local_notifications_windows/.gitignore b/flutter_local_notifications_windows/.gitignore new file mode 100644 index 000000000..ac5aa9893 --- /dev/null +++ b/flutter_local_notifications_windows/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/flutter_local_notifications_windows/.metadata b/flutter_local_notifications_windows/.metadata new file mode 100644 index 000000000..fc2d1c605 --- /dev/null +++ b/flutter_local_notifications_windows/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49" + channel: "stable" + +project_type: plugin_ffi + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + - platform: windows + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/flutter_local_notifications_windows/CHANGELOG.md b/flutter_local_notifications_windows/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/flutter_local_notifications_windows/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/flutter_local_notifications_windows/LICENSE b/flutter_local_notifications_windows/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/flutter_local_notifications_windows/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/flutter_local_notifications_windows/README.md b/flutter_local_notifications_windows/README.md new file mode 100644 index 000000000..02bf2fda4 --- /dev/null +++ b/flutter_local_notifications_windows/README.md @@ -0,0 +1,92 @@ +# flutter_local_notifications_windows + +A new Flutter FFI plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[FFI plugin](https://docs.flutter.dev/development/platform-integration/c-interop), +a specialized package that includes native code directly invoked with Dart FFI. + +## Project structure + +This template uses the following structure: + +* `src`: Contains the native source code, and a CmakeFile.txt file for building + that source code into a dynamic library. + +* `lib`: Contains the Dart code that defines the API of the plugin, and which + calls into the native code using `dart:ffi`. + +* platform folders (`android`, `ios`, `windows`, etc.): Contains the build files + for building and bundling the native code library with the platform application. + +## Building and bundling native code + +The `pubspec.yaml` specifies FFI plugins as follows: + +```yaml + plugin: + platforms: + some_platform: + ffiPlugin: true +``` + +This configuration invokes the native build for the various target platforms +and bundles the binaries in Flutter applications using these FFI plugins. + +This can be combined with dartPluginClass, such as when FFI is used for the +implementation of one platform in a federated plugin: + +```yaml + plugin: + implements: some_other_plugin + platforms: + some_platform: + dartPluginClass: SomeClass + ffiPlugin: true +``` + +A plugin can have both FFI and method channels: + +```yaml + plugin: + platforms: + some_platform: + pluginClass: SomeName + ffiPlugin: true +``` + +The native build systems that are invoked by FFI (and method channel) plugins are: + +* For Android: Gradle, which invokes the Android NDK for native builds. + * See the documentation in android/build.gradle. +* For iOS and MacOS: Xcode, via CocoaPods. + * See the documentation in ios/flutter_local_notifications_windows.podspec. + * See the documentation in macos/flutter_local_notifications_windows.podspec. +* For Linux and Windows: CMake. + * See the documentation in linux/CMakeLists.txt. + * See the documentation in windows/CMakeLists.txt. + +## Binding to native code + +To use the native code, bindings in Dart are needed. +To avoid writing these by hand, they are generated from the header file +(`src/flutter_local_notifications_windows.h`) by `package:ffigen`. +Regenerate the bindings by running `dart run ffigen --config ffigen.yaml`. + +## Invoking native code + +Very short-running native functions can be directly invoked from any isolate. +For example, see `sum` in `lib/flutter_local_notifications_windows.dart`. + +Longer-running functions should be invoked on a helper isolate to avoid +dropping frames in Flutter applications. +For example, see `sumAsync` in `lib/flutter_local_notifications_windows.dart`. + +## Flutter help + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/flutter_local_notifications_windows/analysis_options.yaml b/flutter_local_notifications_windows/analysis_options.yaml new file mode 100644 index 000000000..5595d746c --- /dev/null +++ b/flutter_local_notifications_windows/analysis_options.yaml @@ -0,0 +1,52 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. See the following for docs: +# https://dart.dev/guides/language/analysis-options +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. +include: package:very_good_analysis/analysis_options.yaml # has more lints + +analyzer: + language: + # Strict casts isn't helpful with null safety. It only notifies you on `dynamic`, + # which happens all the time in JSON. + # + # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-casts.md + strict-casts: false + + # Don't let any types be inferred as `dynamic`. + # + # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-inference.md + strict-inference: true + + # Don't let Dart infer the wrong type on the left side of an assignment. + # + # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-raw-types.md + strict-raw-types: true + + # These are only temporary while these files are in development. + # exclude: + +linter: + rules: + # Rules NOT in package:very_good_analysis + prefer_double_quotes: true + prefer_expression_function_bodies: true + avoid_types_on_closure_parameters: true + + # Rules to be disabled from package:very_good_analysis + prefer_single_quotes: false # prefer_double_quotes + lines_longer_than_80_chars: false # lines should be at most 100 chars + sort_pub_dependencies: false # Sort dependencies by function + use_key_in_widget_constructors: false # not in Flutter apps + directives_ordering: false # sort dart, then flutter, then package imports + always_use_package_imports: false # not when importing sibling files + sort_constructors_first: false # final properties, then constructor + avoid_dynamic_calls: false # this lint takes over errors in the IDE + one_member_abstracts: false # abstract classes are good for interfaces + cascade_invocations: false # cascades are often harder to read + + # Temporarily disabled until we are ready to document + public_member_api_docs: false + flutter_style_todos: false diff --git a/flutter_local_notifications_windows/ffigen.yaml b/flutter_local_notifications_windows/ffigen.yaml new file mode 100644 index 000000000..0ed073f79 --- /dev/null +++ b/flutter_local_notifications_windows/ffigen.yaml @@ -0,0 +1,19 @@ +# Run with `dart run ffigen --config ffigen.yaml`. +name: FlutterLocalNotificationsWindowsBindings +description: | + Bindings for `src/flutter_local_notifications_windows.h`. + + Regenerate bindings with `dart run ffigen --config ffigen.yaml`. +output: 'lib/ffi_bindings.dart' +headers: + entry-points: + - 'src/flutter_local_notifications_windows.h' + include-directives: + - 'src/flutter_local_notifications_windows.h' +preamble: | + // ignore_for_file: always_specify_types + // ignore_for_file: camel_case_types + // ignore_for_file: non_constant_identifier_names +comments: + style: any + length: full diff --git a/flutter_local_notifications_windows/lib/ffi_bindings.dart b/flutter_local_notifications_windows/lib/ffi_bindings.dart new file mode 100644 index 000000000..4a1a07d66 --- /dev/null +++ b/flutter_local_notifications_windows/lib/ffi_bindings.dart @@ -0,0 +1,69 @@ +// ignore_for_file: always_specify_types +// ignore_for_file: camel_case_types +// ignore_for_file: non_constant_identifier_names + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; + +/// Bindings for `src/flutter_local_notifications_windows.h`. +/// +/// Regenerate bindings with `dart run ffigen --config ffigen.yaml`. +/// +class FlutterLocalNotificationsWindowsBindings { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + FlutterLocalNotificationsWindowsBindings(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + FlutterLocalNotificationsWindowsBindings.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + /// A very short-lived native function. + /// + /// For very short-lived functions, it is fine to call them on the main isolate. + /// They will block the Dart execution while running the native function, so + /// only do this for native functions which are guaranteed to be short-lived. + int sum( + int a, + int b, + ) { + return _sum( + a, + b, + ); + } + + late final _sumPtr = + _lookup>('sum'); + late final _sum = _sumPtr.asFunction(); + + /// A longer lived native function, which occupies the thread calling it. + /// + /// Do not call these kind of native functions in the main isolate. They will + /// block Dart execution. This will cause dropped frames in Flutter applications. + /// Instead, call these native functions on a separate isolate. + int sum_long_running( + int a, + int b, + ) { + return _sum_long_running( + a, + b, + ); + } + + late final _sum_long_runningPtr = + _lookup>( + 'sum_long_running'); + late final _sum_long_running = + _sum_long_runningPtr.asFunction(); +} diff --git a/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart new file mode 100644 index 000000000..321504809 --- /dev/null +++ b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart @@ -0,0 +1,131 @@ + +import "dart:async"; +import "dart:ffi"; +import "dart:io"; +import "dart:isolate"; + +import "ffi_bindings.dart"; + +/// A very short-lived native function. +/// +/// For very short-lived functions, it is fine to call them on the main isolate. +/// They will block the Dart execution while running the native function, so +/// only do this for native functions which are guaranteed to be short-lived. +int sum(int a, int b) => _bindings.sum(a, b); + +/// A longer lived native function, which occupies the thread calling it. +/// +/// Do not call these kind of native functions in the main isolate. They will +/// block Dart execution. This will cause dropped frames in Flutter applications. +/// Instead, call these native functions on a separate isolate. +/// +/// Modify this to suit your own use case. Example use cases: +/// +/// 1. Reuse a single isolate for various different kinds of requests. +/// 2. Use multiple helper isolates for parallel execution. +Future sumAsync(int a, int b) async { + final helperIsolateSendPort = await _helperIsolateSendPort; + final requestId = _nextSumRequestId++; + final request = _SumRequest(requestId, a, b); + final completer = Completer(); + _sumRequests[requestId] = completer; + helperIsolateSendPort.send(request); + return completer.future; +} + +const String _libName = "flutter_local_notifications_windows"; + +/// The dynamic library in which the symbols for [FlutterLocalNotificationsWindowsBindings] can be found. +final DynamicLibrary _dylib = () { + if (Platform.isMacOS || Platform.isIOS) { + return DynamicLibrary.open("$_libName.framework/$_libName"); + } + if (Platform.isAndroid || Platform.isLinux) { + return DynamicLibrary.open("lib$_libName.so"); + } + if (Platform.isWindows) { + return DynamicLibrary.open("$_libName.dll"); + } + throw UnsupportedError("Unknown platform: ${Platform.operatingSystem}"); +}(); + +/// The bindings to the native functions in [_dylib]. +final FlutterLocalNotificationsWindowsBindings _bindings = FlutterLocalNotificationsWindowsBindings(_dylib); + + +/// A request to compute `sum`. +/// +/// Typically sent from one isolate to another. +class _SumRequest { + final int id; + final int a; + final int b; + + const _SumRequest(this.id, this.a, this.b); +} + +/// A response with the result of `sum`. +/// +/// Typically sent from one isolate to another. +class _SumResponse { + final int id; + final int result; + + const _SumResponse(this.id, this.result); +} + +/// Counter to identify [_SumRequest]s and [_SumResponse]s. +int _nextSumRequestId = 0; + +/// Mapping from [_SumRequest] `id`s to the completers corresponding to the correct future of the pending request. +final Map> _sumRequests = >{}; + +/// The SendPort belonging to the helper isolate. +Future _helperIsolateSendPort = () async { + // The helper isolate is going to send us back a SendPort, which we want to + // wait for. + final completer = Completer(); + + // Receive port on the main isolate to receive messages from the helper. + // We receive two types of messages: + // 1. A port to send messages on. + // 2. Responses to requests we sent. + final receivePort = ReceivePort() + ..listen((dynamic data) { + if (data is SendPort) { + // The helper isolate sent us the port on which we can sent it requests. + completer.complete(data); + return; + } + if (data is _SumResponse) { + // The helper isolate sent us a response to a request we sent. + final completer = _sumRequests[data.id]!; + _sumRequests.remove(data.id); + completer.complete(data.result); + return; + } + throw UnsupportedError("Unsupported message type: ${data.runtimeType}"); + }); + + // Start the helper isolate. + await Isolate.spawn((sendPort) async { + final helperReceivePort = ReceivePort() + ..listen((dynamic data) { + // On the helper isolate listen to requests and respond to them. + if (data is _SumRequest) { + final result = _bindings.sum_long_running(data.a, data.b); + final response = _SumResponse(data.id, result); + sendPort.send(response); + return; + } + throw UnsupportedError("Unsupported message type: ${data.runtimeType}"); + }); + + // Send the port to the main isolate on which we can receive requests. + sendPort.send(helperReceivePort.sendPort); + }, receivePort.sendPort,); + + // Wait until the helper isolate has sent us back the SendPort on which we + // can start sending requests. + return completer.future; +}(); diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml new file mode 100644 index 000000000..8ab4a199a --- /dev/null +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -0,0 +1,74 @@ +name: flutter_local_notifications_windows +description: "A new Flutter FFI plugin project." +version: 1.0.0 +homepage: + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + ffi: ^2.1.0 + ffigen: ^12.0.0 + flutter_test: + sdk: flutter + very_good_analysis: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + # + # Please refer to README.md for a detailed explanation. + plugin: + implements: flutter_local_notifications + platforms: + windows: + ffiPlugin: true + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/flutter_local_notifications_windows/src/CMakeLists.txt b/flutter_local_notifications_windows/src/CMakeLists.txt new file mode 100644 index 000000000..0e24a88a7 --- /dev/null +++ b/flutter_local_notifications_windows/src/CMakeLists.txt @@ -0,0 +1,17 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +project(flutter_local_notifications_windows_library VERSION 0.0.1 LANGUAGES C) + +add_library(flutter_local_notifications_windows SHARED + "flutter_local_notifications_windows.c" +) + +set_target_properties(flutter_local_notifications_windows PROPERTIES + PUBLIC_HEADER flutter_local_notifications_windows.h + OUTPUT_NAME "flutter_local_notifications_windows" +) + +target_compile_definitions(flutter_local_notifications_windows PUBLIC DART_SHARED_LIB) diff --git a/flutter_local_notifications_windows/src/flutter_local_notifications_windows.c b/flutter_local_notifications_windows/src/flutter_local_notifications_windows.c new file mode 100644 index 000000000..e685ff5d8 --- /dev/null +++ b/flutter_local_notifications_windows/src/flutter_local_notifications_windows.c @@ -0,0 +1,23 @@ +#include "flutter_local_notifications_windows.h" + +// A very short-lived native function. +// +// For very short-lived functions, it is fine to call them on the main isolate. +// They will block the Dart execution while running the native function, so +// only do this for native functions which are guaranteed to be short-lived. +FFI_PLUGIN_EXPORT int sum(int a, int b) { return a + b; } + +// A longer-lived native function, which occupies the thread calling it. +// +// Do not call these kind of native functions in the main isolate. They will +// block Dart execution. This will cause dropped frames in Flutter applications. +// Instead, call these native functions on a separate isolate. +FFI_PLUGIN_EXPORT int sum_long_running(int a, int b) { + // Simulate work. +#if _WIN32 + Sleep(5000); +#else + usleep(5000 * 1000); +#endif + return a + b; +} diff --git a/flutter_local_notifications_windows/src/flutter_local_notifications_windows.h b/flutter_local_notifications_windows/src/flutter_local_notifications_windows.h new file mode 100644 index 000000000..79444598b --- /dev/null +++ b/flutter_local_notifications_windows/src/flutter_local_notifications_windows.h @@ -0,0 +1,30 @@ +#include +#include +#include + +#if _WIN32 +#include +#else +#include +#include +#endif + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +// A very short-lived native function. +// +// For very short-lived functions, it is fine to call them on the main isolate. +// They will block the Dart execution while running the native function, so +// only do this for native functions which are guaranteed to be short-lived. +FFI_PLUGIN_EXPORT int sum(int a, int b); + +// A longer lived native function, which occupies the thread calling it. +// +// Do not call these kind of native functions in the main isolate. They will +// block Dart execution. This will cause dropped frames in Flutter applications. +// Instead, call these native functions on a separate isolate. +FFI_PLUGIN_EXPORT int sum_long_running(int a, int b); diff --git a/flutter_local_notifications_windows/windows/.gitignore b/flutter_local_notifications_windows/windows/.gitignore new file mode 100644 index 000000000..b3eb2be16 --- /dev/null +++ b/flutter_local_notifications_windows/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/flutter_local_notifications_windows/windows/CMakeLists.txt b/flutter_local_notifications_windows/windows/CMakeLists.txt new file mode 100644 index 000000000..3be2b5cf4 --- /dev/null +++ b/flutter_local_notifications_windows/windows/CMakeLists.txt @@ -0,0 +1,23 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "flutter_local_notifications_windows") +project(${PROJECT_NAME} LANGUAGES CXX) + +# Invoke the build for native code shared with the other target platforms. +# This can be changed to accommodate different builds. +add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_BINARY_DIR}/shared") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(flutter_local_notifications_windows_bundled_libraries + # Defined in ../src/CMakeLists.txt. + # This can be changed to accommodate different builds. + $ + PARENT_SCOPE +) From 36c170e0e67c1fa47ff8329629de33789958e6b9 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 11 Jul 2024 04:03:13 -0400 Subject: [PATCH 051/112] moved to FFI plugin --- .../example/lib/main.dart | 5 +- .../example/lib/windows.dart | 12 +- .../flutter/generated_plugin_registrant.cc | 3 - .../windows/flutter/generated_plugins.cmake | 2 +- .../lib/flutter_local_notifications.dart | 23 +- .../flutter_local_notifications_plugin.dart | 19 +- .../lib/src/initialization_settings.dart | 1 - .../lib/src/notification_details.dart | 2 +- .../platform_flutter_local_notifications.dart | 176 --------- .../windows/method_channel_mappers.dart | 14 - flutter_local_notifications/pubspec.yaml | 2 + .../flutter_local_notifications_test.dart | 1 - .../windows/.gitignore | 17 - .../windows/CMakeLists.txt | 68 ---- .../windows/flutter_local_notifications.cpp | 241 ------------ .../windows/flutter_local_notifications.h | 69 ---- .../flutter_local_notifications_plugin.cpp | 13 - .../flutter_local_notifications_plugin.h | 24 -- .../windows/methods.cpp | 14 - flutter_local_notifications/windows/methods.h | 22 -- .../windows/registration.h | 36 -- flutter_local_notifications/windows/types.h | 15 - flutter_local_notifications/windows/utils.h | 27 -- .../ffigen.yaml | 20 +- .../lib/ffi_bindings.dart | 69 ---- .../flutter_local_notifications_windows.dart | 134 +------ .../lib/src/details.dart | 18 + .../src/details}/initialization_settings.dart | 6 - .../lib/src/details}/notification_action.dart | 36 +- .../lib/src/details}/notification_audio.dart | 76 ++-- .../src/details}/notification_details.dart | 350 +++++++++--------- .../lib/src/details}/notification_group.dart | 16 +- .../lib/src/details}/notification_header.dart | 12 +- .../lib/src/details}/notification_image.dart | 22 +- .../lib/src/details}/notification_input.dart | 33 +- .../lib/src/details}/notification_part.dart | 2 +- .../src/details}/notification_progress.dart | 22 +- .../lib/src/details}/notification_text.dart | 16 +- .../lib/src/details/notification_to_xml.dart | 36 ++ .../lib/src/ffi/bindings.dart | 318 ++++++++++++++++ .../lib/src/ffi/utils.dart | 61 +++ .../lib/src/plugin/base.dart | 57 +++ .../lib/src/plugin/ffi.dart | 138 +++++++ .../lib/src/plugin/stub.dart | 57 +++ .../pubspec.yaml | 9 +- .../src/CMakeLists.txt | 11 +- .../src/ffi_api.cpp | 87 +++++ .../src/ffi_api.h | 91 +++++ .../src/flutter_local_notifications_windows.c | 23 -- .../src/flutter_local_notifications_windows.h | 30 -- .../src/plugin.cpp | 143 +++++++ .../src/plugin.hpp | 46 +++ .../src}/registration.cpp | 138 +++---- .../src/registration.hpp | 31 ++ .../src/utils.cpp | 52 +++ .../src/utils.hpp | 26 ++ 56 files changed, 1572 insertions(+), 1420 deletions(-) delete mode 100644 flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart delete mode 100644 flutter_local_notifications/windows/.gitignore delete mode 100644 flutter_local_notifications/windows/CMakeLists.txt delete mode 100644 flutter_local_notifications/windows/flutter_local_notifications.cpp delete mode 100644 flutter_local_notifications/windows/flutter_local_notifications.h delete mode 100644 flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp delete mode 100644 flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h delete mode 100644 flutter_local_notifications/windows/methods.cpp delete mode 100644 flutter_local_notifications/windows/methods.h delete mode 100644 flutter_local_notifications/windows/registration.h delete mode 100644 flutter_local_notifications/windows/types.h delete mode 100644 flutter_local_notifications/windows/utils.h delete mode 100644 flutter_local_notifications_windows/lib/ffi_bindings.dart create mode 100644 flutter_local_notifications_windows/lib/src/details.dart rename {flutter_local_notifications/lib/src/platform_specifics/windows => flutter_local_notifications_windows/lib/src/details}/initialization_settings.dart (85%) rename {flutter_local_notifications/lib/src/platform_specifics/windows => flutter_local_notifications_windows/lib/src/details}/notification_action.dart (84%) rename {flutter_local_notifications/lib/src/platform_specifics/windows => flutter_local_notifications_windows/lib/src/details}/notification_audio.dart (53%) rename {flutter_local_notifications/lib/src/platform_specifics/windows => flutter_local_notifications_windows/lib/src/details}/notification_details.dart (69%) rename {flutter_local_notifications/lib/src/platform_specifics/windows => flutter_local_notifications_windows/lib/src/details}/notification_group.dart (74%) rename {flutter_local_notifications/lib/src/platform_specifics/windows => flutter_local_notifications_windows/lib/src/details}/notification_header.dart (83%) rename {flutter_local_notifications/lib/src/platform_specifics/windows => flutter_local_notifications_windows/lib/src/details}/notification_image.dart (76%) rename {flutter_local_notifications/lib/src/platform_specifics/windows => flutter_local_notifications_windows/lib/src/details}/notification_input.dart (82%) rename {flutter_local_notifications/lib/src/platform_specifics/windows => flutter_local_notifications_windows/lib/src/details}/notification_part.dart (92%) rename {flutter_local_notifications/lib/src/platform_specifics/windows => flutter_local_notifications_windows/lib/src/details}/notification_progress.dart (71%) rename {flutter_local_notifications/lib/src/platform_specifics/windows => flutter_local_notifications_windows/lib/src/details}/notification_text.dart (76%) create mode 100644 flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart create mode 100644 flutter_local_notifications_windows/lib/src/ffi/bindings.dart create mode 100644 flutter_local_notifications_windows/lib/src/ffi/utils.dart create mode 100644 flutter_local_notifications_windows/lib/src/plugin/base.dart create mode 100644 flutter_local_notifications_windows/lib/src/plugin/ffi.dart create mode 100644 flutter_local_notifications_windows/lib/src/plugin/stub.dart create mode 100644 flutter_local_notifications_windows/src/ffi_api.cpp create mode 100644 flutter_local_notifications_windows/src/ffi_api.h delete mode 100644 flutter_local_notifications_windows/src/flutter_local_notifications_windows.c delete mode 100644 flutter_local_notifications_windows/src/flutter_local_notifications_windows.h create mode 100644 flutter_local_notifications_windows/src/plugin.cpp create mode 100644 flutter_local_notifications_windows/src/plugin.hpp rename {flutter_local_notifications/windows => flutter_local_notifications_windows/src}/registration.cpp (70%) create mode 100644 flutter_local_notifications_windows/src/registration.hpp create mode 100644 flutter_local_notifications_windows/src/utils.cpp create mode 100644 flutter_local_notifications_windows/src/utils.hpp diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index ec9aaaa25..c68e6f10d 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -174,6 +174,7 @@ Future main() async { initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) { + print(notificationResponse.notificationResponseType); switch (notificationResponse.notificationResponseType) { case NotificationResponseType.selectedNotification: selectNotificationStream.add(notificationResponse); @@ -2762,11 +2763,11 @@ class _HomePageState extends State { } Future? _showWindowsNotificationWithRawXml() => flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation() + .resolvePlatformSpecificImplementation() ?.showRawXml( id: id++, xml: _windowsRawXmlController.text, - data: {'message': 'Hello, World!'}, + bindings: {'message': 'Hello, World!'}, ); } diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index d892da496..2b3780833 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -239,8 +239,8 @@ Future _showWindowsNotificationWithProgress() async { final WindowsProgressBar slowProgress = WindowsProgressBar(id: 'slow-progress', status: 'Updating slowly...', value: 0, label: '0 / 10'); final int notificationId = id++; - final WindowsFlutterLocalNotificationsPlugin? windows = - flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation(); + final FlutterLocalNotificationsWindows? windows = + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation(); await flutterLocalNotificationsPlugin.show( notificationId, 'This notification has progress bars', @@ -293,9 +293,9 @@ Future _showWindowsNotificationWithProgress() async { Future _showWindowsNotificationWithDynamic() async { final DateTime start = DateTime.now(); final int notificationId = id++; - final WindowsFlutterLocalNotificationsPlugin? windows = + final FlutterLocalNotificationsWindows? windows = flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation(); + .resolvePlatformSpecificImplementation(); await flutterLocalNotificationsPlugin.show( notificationId, 'Dynamic content', @@ -309,14 +309,14 @@ Future _showWindowsNotificationWithDynamic() async { Map getBindings() => { 'stopwatch': 'Elapsed time: ${DateTime.now().difference(start).inSeconds} seconds', }; - await windows?.updateBindings(id: notificationId, data: getBindings()); + await windows?.updateBindings(id: notificationId, bindings: getBindings()); Timer.periodic(const Duration(seconds: 1), (Timer timer) async { if (timer.tick > 10) { timer.cancel(); await flutterLocalNotificationsPlugin.cancel(notificationId); return; } - await windows?.updateBindings(id: notificationId, data: getBindings()); + await windows?.updateBindings(id: notificationId, bindings: getBindings()); }); } diff --git a/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc index 3ef053e07..8b6d4680a 100644 --- a/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc +++ b/flutter_local_notifications/example/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,6 @@ #include "generated_plugin_registrant.h" -#include void RegisterPlugins(flutter::PluginRegistry* registry) { - FlutterLocalNotificationsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterLocalNotificationsPlugin")); } diff --git a/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake b/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake index 87bfe6c13..93f25e155 100644 --- a/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake +++ b/flutter_local_notifications/example/windows/flutter/generated_plugins.cmake @@ -3,10 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST - flutter_local_notifications ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_local_notifications_windows ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/flutter_local_notifications/lib/flutter_local_notifications.dart b/flutter_local_notifications/lib/flutter_local_notifications.dart index f1e7df0a2..c97d57460 100644 --- a/flutter_local_notifications/lib/flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/flutter_local_notifications.dart @@ -1,14 +1,6 @@ export 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart'; -export 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart' - show - DidReceiveBackgroundNotificationResponseCallback, - DidReceiveNotificationResponseCallback, - PendingNotificationRequest, - ActiveNotification, - RepeatInterval, - NotificationAppLaunchDetails, - NotificationResponse, - NotificationResponseType; +export 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; +export 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart'; export 'src/flutter_local_notifications_plugin.dart'; export 'src/initialization_settings.dart'; @@ -44,15 +36,6 @@ export 'src/platform_specifics/darwin/notification_category_option.dart'; export 'src/platform_specifics/darwin/notification_details.dart'; export 'src/platform_specifics/darwin/notification_enabled_options.dart'; export 'src/platform_specifics/ios/enums.dart'; -export 'src/platform_specifics/windows/initialization_settings.dart'; -export 'src/platform_specifics/windows/notification_action.dart'; -export 'src/platform_specifics/windows/notification_audio.dart'; -export 'src/platform_specifics/windows/notification_details.dart'; -export 'src/platform_specifics/windows/notification_group.dart'; -export 'src/platform_specifics/windows/notification_header.dart'; -export 'src/platform_specifics/windows/notification_image.dart'; -export 'src/platform_specifics/windows/notification_input.dart'; -export 'src/platform_specifics/windows/notification_progress.dart'; -export 'src/platform_specifics/windows/notification_text.dart'; + export 'src/typedefs.dart'; export 'src/types.dart'; diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index 2bef48c86..5ad5a57f1 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart'; import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; +import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart'; import 'package:timezone/timezone.dart'; import 'initialization_settings.dart'; @@ -43,7 +44,7 @@ class FlutterLocalNotificationsPlugin { LinuxFlutterLocalNotificationsPlugin(); } else if (defaultTargetPlatform == TargetPlatform.windows) { FlutterLocalNotificationsPlatform.instance = - WindowsFlutterLocalNotificationsPlugin(); + FlutterLocalNotificationsWindows(); } } @@ -90,9 +91,9 @@ class FlutterLocalNotificationsPlugin { is LinuxFlutterLocalNotificationsPlugin) { return FlutterLocalNotificationsPlatform.instance as T?; } else if (defaultTargetPlatform == TargetPlatform.windows && - T == WindowsFlutterLocalNotificationsPlugin && + T == FlutterLocalNotificationsWindows && FlutterLocalNotificationsPlatform.instance - is WindowsFlutterLocalNotificationsPlugin) { + is FlutterLocalNotificationsWindows) { return FlutterLocalNotificationsPlatform.instance as T?; } @@ -200,10 +201,10 @@ class FlutterLocalNotificationsPlugin { } return await resolvePlatformSpecificImplementation< - WindowsFlutterLocalNotificationsPlugin + FlutterLocalNotificationsWindows >()?.initialize( initializationSettings.windows!, - onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, + onNotificationReceived: onDidReceiveNotificationResponse, ); } return true; @@ -240,7 +241,7 @@ class FlutterLocalNotificationsPlugin { ?.getNotificationAppLaunchDetails(); } else if (defaultTargetPlatform == TargetPlatform.windows) { return await resolvePlatformSpecificImplementation< - WindowsFlutterLocalNotificationsPlugin>() + FlutterLocalNotificationsWindows>() ?.getNotificationAppLaunchDetails(); } else { return await FlutterLocalNotificationsPlatform.instance @@ -286,9 +287,9 @@ class FlutterLocalNotificationsPlugin { payload: payload); } else if (defaultTargetPlatform == TargetPlatform.windows) { await resolvePlatformSpecificImplementation< - WindowsFlutterLocalNotificationsPlugin>() + FlutterLocalNotificationsWindows>() ?.show(id, title, body, - notificationDetails: notificationDetails?.windows, + details: notificationDetails?.windows, payload: payload); } else { await FlutterLocalNotificationsPlatform.instance.show(id, title, body); @@ -413,7 +414,7 @@ class FlutterLocalNotificationsPlugin { matchDateTimeComponents: matchDateTimeComponents); } else if (defaultTargetPlatform == TargetPlatform.windows) { await resolvePlatformSpecificImplementation< - WindowsFlutterLocalNotificationsPlugin + FlutterLocalNotificationsWindows >()?.zonedSchedule( id, title, body, scheduledDate, notificationDetails.windows, payload: payload, diff --git a/flutter_local_notifications/lib/src/initialization_settings.dart b/flutter_local_notifications/lib/src/initialization_settings.dart index f58358751..ddd7b356d 100644 --- a/flutter_local_notifications/lib/src/initialization_settings.dart +++ b/flutter_local_notifications/lib/src/initialization_settings.dart @@ -1,5 +1,4 @@ import '../flutter_local_notifications.dart'; -export 'platform_specifics/windows/initialization_settings.dart'; /// Settings for initializing the plugin for each platform. class InitializationSettings { diff --git a/flutter_local_notifications/lib/src/notification_details.dart b/flutter_local_notifications/lib/src/notification_details.dart index 266196260..070b03ae1 100644 --- a/flutter_local_notifications/lib/src/notification_details.dart +++ b/flutter_local_notifications/lib/src/notification_details.dart @@ -1,8 +1,8 @@ import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart'; +import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart'; import 'platform_specifics/android/notification_details.dart'; import 'platform_specifics/darwin/notification_details.dart'; -import 'platform_specifics/windows/notification_details.dart'; /// Contains notification details specific to each platform. class NotificationDetails { diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index ea8f0301d..4be8dd44a 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -5,7 +5,6 @@ import 'package:clock/clock.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; import 'package:timezone/timezone.dart'; -import 'package:xml/xml.dart'; import 'callback_dispatcher.dart'; import 'helpers.dart'; @@ -26,10 +25,6 @@ import 'platform_specifics/darwin/mappers.dart'; import 'platform_specifics/darwin/notification_details.dart'; import 'platform_specifics/darwin/notification_enabled_options.dart'; import 'platform_specifics/ios/enums.dart'; -import 'platform_specifics/windows/initialization_settings.dart'; -import 'platform_specifics/windows/method_channel_mappers.dart'; -import 'platform_specifics/windows/notification_details.dart'; -import 'platform_specifics/windows/notification_progress.dart'; import 'typedefs.dart'; import 'types.dart'; import 'tz_datetime_mapper.dart'; @@ -1017,177 +1012,6 @@ class MacOSFlutterLocalNotificationsPlugin } } -/// Windows implementation of the flutter_local_notifications plugin. -class WindowsFlutterLocalNotificationsPlugin - extends MethodChannelFlutterLocalNotificationsPlugin { - DidReceiveNotificationResponseCallback? _onDidReceiveNotificationResponse; - /// Initializes the plugin. - /// - /// Call this method on application before using - /// the plugin further. - /// - /// This should only be done once. When a notification created by this plugin - /// was used to launch the app, calling [initialize] is what will trigger to - /// the [onDidReceiveNotificationResponse] callback to be fire. - /// - /// To handle when a notification launched an application, use - /// [getNotificationAppLaunchDetails]. - Future initialize( - WindowsInitializationSettings settings, { - DidReceiveNotificationResponseCallback? onDidReceiveNotificationResponse - }) { - _onDidReceiveNotificationResponse = onDidReceiveNotificationResponse; - _channel.setMethodCallHandler(_handleMethod); - - return _channel.invokeMethod('initialize', settings.toMap()); - } - - /// Passes the raw XML to the Windows API directly. - /// - /// You can replace values in the `` element with a `{placeholder}` - /// and set their values in [data] instead. Then, you may update them with - /// [updateBindings]. - /// - /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. - /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). - Future showRawXml({ - required int id, - required String xml, - Map data = const {}, - }) => _channel.invokeMethod('show', { - 'id': id, - 'rawXml': xml, - 'data': data, - }); - - String _notificationToXml({ - String? title, - String? body, - String? payload, - WindowsNotificationDetails? notificationDetails, - }) { - final XmlBuilder builder = XmlBuilder(); - builder.element( - 'toast', - attributes: { - ...notificationDetails?.attributes ?? {}, - if (payload != null) 'launch': payload, - if (notificationDetails?.scenario == null) 'useButtonStyle': 'true', - }, - nest: () { - builder.element('visual', nest: () { - builder.element( - 'binding', - attributes: {'template': 'ToastGeneric'}, - nest: () { - builder - ..element('text', nest: title) - ..element('text', nest: body); - notificationDetails?.generateBinding(builder); - }, - ); - }); - notificationDetails?.toXml(builder); - }, - ); - return builder.buildDocument() - .toXmlString(pretty: true, indentAttribute: (_) => true); - } - - @override - Future show( - int id, - String? title, - String? body, { - String? payload, - String? group, - WindowsNotificationDetails? notificationDetails, - }) async { - final String xml = _notificationToXml( - title: title, - body: body, - payload: payload, - notificationDetails: notificationDetails, - ); - await _channel.invokeMethod('show', { - 'id': id, - 'rawXml': xml, - 'data': { - for (final WindowsProgressBar progressBar in notificationDetails - ?.progressBars ?? [] - ) ...progressBar.data, - }, - }); - } - - @override - Future cancel(int id, {String? group}) => - _channel.invokeMethod('cancel', { - 'id': id, - 'group': group, - }); - - /// Schedules a notification for the future. - Future zonedSchedule( - int id, - String? title, - String? body, - TZDateTime scheduledDate, - WindowsNotificationDetails? notificationDetails, { - String? payload, - }) async { - final String xml = _notificationToXml( - title: title, - body: body, - payload: payload, - notificationDetails: notificationDetails, - ); - final int secondsSinceEpoch = scheduledDate.millisecondsSinceEpoch ~/ 1000; - await _channel.invokeMethod('zonedSchedule', { - 'id': id, - 'rawXml': xml, - 'time': secondsSinceEpoch, - }); - } - - /// Updates the progress bar in the notification with the given ID. - /// - /// Note that in order to update [WindowsProgressBar.label], it must - /// not have been set to null when [show] was called. - Future updateProgressBar({ - required int notificationId, - required WindowsProgressBar progressBar, - }) => updateBindings(id: notificationId, data: progressBar.data); - - /// Updates any data binding in the given notification. - /// - /// Instead of a text value, you can replace any value in the `` - /// element with `{name}`, and then use this function to update that value - /// by passing `data: {'name': value}`. - Future updateBindings({ - required int id, - required Map data, - }) => _channel.invokeMethod('update', { - 'id': id, - 'data': data, - }); - - Future _handleMethod(MethodCall call) async { - switch (call.method) { - case 'didReceiveNotificationResponse': - if (call.arguments is Map) { - _onDidReceiveNotificationResponse?.call(NotificationResponse( - notificationResponseType: - NotificationResponseType.selectedNotification, - payload: call.arguments['payload'], - data: Map.from(call.arguments['data']), - )); - } - break; - } - } -} - /// Checks [didReceiveBackgroundNotificationResponseCallback], if not `null`, /// for eligibility to be used as a background callback. /// diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart deleted file mode 100644 index d52980b3c..000000000 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/method_channel_mappers.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'initialization_settings.dart'; - -/// An extension on [WindowsInitializationSettings] that provides mapping -/// to method channel serializable values. -extension WindowsInitializationSettingsMapper on WindowsInitializationSettings { - /// Maps [WindowsInitializationSettings] to a [Map]. - Map toMap() => { - 'appName': appName, - 'aumid': appUserModelId, - 'guid': guid, - 'iconPath': iconPath, - 'iconBgColor': iconBackgroundColor, - }; -} diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index d05ec085d..c75bd9b0e 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -40,6 +40,8 @@ flutter: pluginClass: FlutterLocalNotificationsPlugin linux: default_package: flutter_local_notifications_linux + windows: + default_package: flutter_local_notifications_windows environment: diff --git a/flutter_local_notifications/test/flutter_local_notifications_test.dart b/flutter_local_notifications/test/flutter_local_notifications_test.dart index c9fdfbc84..b47635843 100644 --- a/flutter_local_notifications/test/flutter_local_notifications_test.dart +++ b/flutter_local_notifications/test/flutter_local_notifications_test.dart @@ -1,6 +1,5 @@ import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; diff --git a/flutter_local_notifications/windows/.gitignore b/flutter_local_notifications/windows/.gitignore deleted file mode 100644 index b3eb2be16..000000000 --- a/flutter_local_notifications/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/flutter_local_notifications/windows/CMakeLists.txt b/flutter_local_notifications/windows/CMakeLists.txt deleted file mode 100644 index 25d846df9..000000000 --- a/flutter_local_notifications/windows/CMakeLists.txt +++ /dev/null @@ -1,68 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -set(PROJECT_NAME "flutter_local_notifications") -project(${PROJECT_NAME} LANGUAGES CXX) -include(FetchContent) - -# This value is used when generating builds using this plugin, so it must -# not be changed -set(PLUGIN_NAME "flutter_local_notifications_plugin") - -# nuget configuration -set(NUGET_PACKAGES_PATH "${CMAKE_BINARY_DIR}/packages") - -FetchContent_Declare(nuget - URL "https://dist.nuget.org/win-x86-commandline/v6.0.0/nuget.exe" - URL_HASH SHA256=04eb6c4fe4213907e2773e1be1bbbd730e9a655a3c9c58387ce8d4a714a5b9e1 - DOWNLOAD_NO_EXTRACT true -) -find_program(NUGET nuget) -if (NOT NUGET) - message("Nuget.exe not found, trying to download or use cached version.") - FetchContent_MakeAvailable(nuget) - set(NUGET ${nuget_SOURCE_DIR}/nuget.exe) -endif() - -function(nuget_install pkg ver) - execute_process(COMMAND - ${NUGET} install ${pkg} -Version ${ver} -OutputDirectory ${NUGET_PACKAGES_PATH} - RESULT_VARIABLE result) - if (NOT result EQUAL 0) - message(FATAL_ERROR "Failed to install nuget package ${pkg}, version ${ver}, ${result}") - endif() -endfunction() - -add_library(${PLUGIN_NAME} SHARED - "flutter_local_notifications_plugin.cpp" - "flutter_local_notifications.cpp" - "methods.cpp" - "utils.h" - "registration.cpp") - -# setup c++/winrt -set(CPPWINRT_VERSION "2.0.220131.2") -set(CPPWINRT ${NUGET_PACKAGES_PATH}/Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}/bin/cppwinrt.exe) - -nuget_install("Microsoft.Windows.CppWinRT" ${CPPWINRT_VERSION}) -execute_process(COMMAND - ${CPPWINRT} -input sdk -output include - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - RESULT_VARIABLE ret) -if (NOT ret EQUAL 0) - message(FATAL_ERROR "Result ${ret} ${CPPWINRT} Failed to run cppwinrt.exe") -endif() - -include_directories(BEFORE SYSTEM ${CMAKE_BINARY_DIR}/include) - -apply_standard_settings(${PLUGIN_NAME}) -set_target_properties(${PLUGIN_NAME} PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) -target_include_directories(${PLUGIN_NAME} INTERFACE - "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) - -# List of absolute paths to libraries that should be bundled with the plugin -set(flutter_local_notifications_bundled_libraries - "" - PARENT_SCOPE -) diff --git a/flutter_local_notifications/windows/flutter_local_notifications.cpp b/flutter_local_notifications/windows/flutter_local_notifications.cpp deleted file mode 100644 index 89e257657..000000000 --- a/flutter_local_notifications/windows/flutter_local_notifications.cpp +++ /dev/null @@ -1,241 +0,0 @@ -#include -#include -#include -#include -#include - -#include "flutter_local_notifications.h" -#include "methods.h" -#include "utils.h" -#include "registration.h" - -using std::string; -using namespace winrt::Windows::Data::Xml::Dom; -using namespace winrt::Windows::UI::Notifications; -using namespace flutter; -using namespace local_notifications; - -void FlutterLocalNotifications::RegisterWithRegistrar(PluginRegistrarWindows* registrar) { - const auto channel = std::make_shared( - registrar->messenger(), - "dexterous.com/flutter/local_notifications", - &flutter::StandardMethodCodec::GetInstance() - ); - auto plugin = std::make_unique(channel); - channel->SetMethodCallHandler( - [pluginPointer = plugin.get()](const auto& call, auto result) { - pluginPointer->HandleMethodCall(call, std::move(result)); - } - ); - registrar->AddPlugin(std::move(plugin)); -} - -FlutterLocalNotifications::FlutterLocalNotifications(std::shared_ptr channel) : - channel(channel) { } - -FlutterLocalNotifications::~FlutterLocalNotifications() { } - -std::optional FlutterLocalNotifications::HasIdentity() { - if (!IsWindows8OrGreater()) return false; - uint32_t length = 0; - auto error = GetCurrentPackageFullName(&length, nullptr); - if (error == APPMODEL_ERROR_NO_PACKAGE) return false; - else if (error != ERROR_INSUFFICIENT_BUFFER) return std::nullopt; - PWSTR fullName = (PWSTR) malloc(length * sizeof(*fullName)); - if (fullName == nullptr) return std::nullopt; - error = GetCurrentPackageFullName(&length, fullName); - if (error != ERROR_SUCCESS) return std::nullopt; - free(fullName); - return true; -} - -void FlutterLocalNotifications::HandleMethodCall( - const FlutterMethodCall& methodCall, - std::unique_ptr result -) { - const auto& methodName = methodCall.method_name(); - const auto args = std::get_if(methodCall.arguments()); - try { - if (methodName == Method::INITIALIZE) { - const auto appName = Utils::GetMapValue("appName", args).value(); - const auto aumid = Utils::GetMapValue("aumid", args).value(); - const auto guid = Utils::GetMapValue("guid", args).value(); - const auto iconPath = Utils::GetMapValue("iconPath", args); - const auto iconColor = Utils::GetMapValue("iconBgColor", args); - const auto value = Initialize(appName, aumid, guid, iconPath, iconColor); - result->Success(value); - } else if (methodName == Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS) { - if (utils == nullptr) { - result->Error("not-initialized", "You must initialize the plugin before calling any methods"); - return; - } - // const auto didLaunch = *(utils->didLaunchWithNotification); - // FlutterMap outerData; - // outerData[std::string("notificationLaunchedApp")] = didLaunch; - // if (didLaunch) { - // auto data = *(utils->launchData); - // outerData[std::string("notificationResponse")] = flutter::EncodableValue(data); - // } - // result->Success(flutter::EncodableValue(outerData)); - result->Success(); - } else if (methodName == Method::CANCEL_ALL) { - CancelAll(); - result->Success(); - } else if (methodName == Method::CANCEL) { - const auto id = Utils::GetMapValue("id", args).value(); - CancelNotification(id); - result->Success(); - } else if (methodName == Method::SHOW) { - const auto id = Utils::GetMapValue("id", args).value(); - const auto xml = Utils::GetMapValue("rawXml", args).value(); - const auto data = Utils::GetMapValue("data", args).value(); - const auto success = ShowNotification(id, xml, data); - if (success) result->Success(); - else result->Error("invalid-xml", "Invalid XML. If you are passing raw XML yourself, try validating it first in the Notifications Visualizer app. If not, please report this as a bug to flutter_local_notifications."); - } else if (methodName == Method::SCHEDULE_NOTIFICATION) { - const auto id = Utils::GetMapValue("id", args).value(); - const auto xml = Utils::GetMapValue("rawXml", args).value(); - const auto time = Utils::GetMapValue("time", args).value(); - const auto success = ScheduleNotification(id, xml, time); - if (success) result->Success(); - else result->Error("invalid-xml", "Invalid XML. If you are passing raw XML yourself, try validating it first in the Notifications Visualizer app. If not, please report this as a bug to flutter_local_notifications."); - } else if (methodName == Method::GET_ACTIVE_NOTIFICATIONS) { - FlutterList list; - GetActiveNotifications(list); - result->Success(list); - } else if (methodName == Method::GET_PENDING_NOTIFICATIONS) { - FlutterList list; - GetPendingNotifications(list); - result->Success(list); - } else if (methodName == Method::UPDATE) { - const auto id = Utils::GetMapValue("id", args).value(); - const auto data = Utils::GetMapValue("data", args).value(); - result->Success(Update(id, data)); - } else { - result->NotImplemented(); - } - } catch (std::exception error) { - result->Error("internal", error.what()); - } catch (winrt::hresult_error error) { - result->Error(std::to_string(error.code().value), winrt::to_string(error.message())); - } catch (...) { - result->Error("internal", "An internal error occurred"); - } -} - -bool FlutterLocalNotifications::Initialize( - const std::string& appName, - const std::string& aumid, - const std::string& guid, - const std::optional& iconPath, - const std::optional& iconColor -) { - _aumid = winrt::to_hstring(aumid); - - FlutterMap launchDetails; - bool didLaunchWithNotifications = false; - RegistrationUtils rawUtils; - rawUtils.channel = channel; - rawUtils.didLaunchWithNotification = std::make_shared(didLaunchWithNotifications); - rawUtils.launchData = std::make_shared(launchDetails); - utils = std::make_shared(rawUtils); - - const auto didRegister = RegisterApp(aumid, appName, guid, iconPath, iconColor, utils); - if (!didRegister) return false; - const auto identityResult = HasIdentity(); - if (!identityResult.has_value()) return false; - hasIdentity = identityResult.value(); - toastNotifier = hasIdentity - ? ToastNotificationManager::CreateToastNotifier() - : ToastNotificationManager::CreateToastNotifier(_aumid); - toastHistory = ToastNotificationManager::History(); - return true; -} - -NotificationData dataFromMap(const FlutterMap& map) { - NotificationData data; - for (const auto pair : map) { - const auto key = winrt::to_hstring(std::get(pair.first)); - const auto value = winrt::to_hstring(std::get(pair.second)); - data.Values().Insert(key, value); - } - return data; -} - -bool FlutterLocalNotifications::ShowNotification(const int id, const string& xml, const FlutterMap& args) { - if (!toastNotifier.has_value()) return false; - XmlDocument doc; - try { doc.LoadXml(winrt::to_hstring(xml)); } - catch (winrt::hresult_error error) { return false; } - ToastNotification notification(doc); - const auto data = dataFromMap(args); - notification.Tag(winrt::to_hstring(id)); - notification.Data(data); - toastNotifier.value().Show(notification); - return true; -} - -bool FlutterLocalNotifications::ScheduleNotification(const int id, const std::string xml, const int time) { - if (!toastNotifier.has_value()) return false; - XmlDocument doc; - try { doc.LoadXml(winrt::to_hstring(xml)); } - catch (winrt::hresult_error error) { return false; } - const time_t time2(time); - const auto time3 = winrt::clock::from_time_t(time2); - ScheduledToastNotification notification(doc, time3); - notification.Tag(winrt::to_hstring(id)); - toastNotifier.value().AddToSchedule(notification); - return true; -} - -void FlutterLocalNotifications::CancelAll() { - if (!toastHistory.has_value() || !toastNotifier.has_value()) return; - if (hasIdentity) toastHistory.value().Clear(); - else toastHistory.value().Clear(_aumid); - for (const auto notification : toastNotifier.value().GetScheduledToastNotifications()) { - toastNotifier.value().RemoveFromSchedule(notification); - } -} - -void FlutterLocalNotifications::CancelNotification(int id) { - if (!toastHistory.has_value() || !toastNotifier.has_value()) return; - const auto tag = winrt::to_hstring(id); - if (hasIdentity) toastHistory.value().Remove(tag); - for (const auto notification : toastNotifier.value().GetScheduledToastNotifications()) { - if (notification.Tag() == tag) { - toastNotifier.value().RemoveFromSchedule(notification); - return; - } - } -} - -void FlutterLocalNotifications::GetActiveNotifications(FlutterList& result) { - if (!toastHistory.has_value() || !hasIdentity) return; - for (const auto notification : toastHistory.value().GetHistory()) { - FlutterMap data; - const auto tag = notification.Tag(); - const auto tagString = winrt::to_string(tag); - const auto tagInt = std::stoi(tagString); - data[std::string("id")] = flutter::EncodableValue(tagInt); - result.emplace_back(flutter::EncodableValue(data)); - } -} - -void FlutterLocalNotifications::GetPendingNotifications(FlutterList& result) { - if (!toastNotifier.has_value()) return; - for (const auto notif : toastNotifier.value().GetScheduledToastNotifications()) { - FlutterMap data; - const auto tag = notif.Tag(); - const auto tagString = winrt::to_string(tag); - const auto tagInt = std::stoi(tagString); - data[std::string("id")] = flutter::EncodableValue(tagInt); - result.emplace_back(flutter::EncodableValue(data)); - } -} - -int FlutterLocalNotifications::Update(const int id, const FlutterMap& map) { - if (!toastNotifier.has_value()) return 1; - const auto tag = winrt::to_hstring(id); - const auto data = dataFromMap(map); - return (int) toastNotifier.value().Update(data, tag); -} diff --git a/flutter_local_notifications/windows/flutter_local_notifications.h b/flutter_local_notifications/windows/flutter_local_notifications.h deleted file mode 100644 index 1e36bdf29..000000000 --- a/flutter_local_notifications/windows/flutter_local_notifications.h +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_H -#define FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_H - -#include -#include -#include - -#include // <-- This must be the first Windows header -#include - -#include "methods.h" -#include "registration.h" -#include "types.h" - -namespace local_notifications { - -class FlutterLocalNotifications : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); - FlutterLocalNotifications(std::shared_ptr channel); - virtual ~FlutterLocalNotifications(); - FlutterLocalNotifications(const FlutterLocalNotifications&) = delete; - FlutterLocalNotifications& operator=(const FlutterLocalNotifications) = delete; - - private: - winrt::hstring _aumid; - std::optional toastNotifier; - std::optional toastHistory; - std::shared_ptr channel; - std::shared_ptr utils; - bool hasIdentity = false; - - /// Checks if this app was installed using an MSIX packager. - /// - /// See: https://learn.microsoft.com/en-us/windows/msix/detect-package-identity. - std::optional HasIdentity(); - - // Called when a method is called on this plugin's channel from Dart. - void HandleMethodCall( - const FlutterMethodCall& methodCall, - std::unique_ptr result - ); - - bool Initialize( - const std::string& appName, - const std::string& aumid, - const std::string& guid, - const std::optional& iconPath, - const std::optional& iconColor - ); - - bool ShowNotification(const int id, const std::string& xml, const FlutterMap& args); - - bool ScheduleNotification(const int id, const std::string xml, const int time); - - void CancelAll(); - - void CancelNotification(const int id); - - void GetActiveNotifications(FlutterList& result); - - void GetPendingNotifications(FlutterList& result); - - int Update(const int id, const FlutterMap& map); -}; - -} - -#endif diff --git a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp b/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp deleted file mode 100644 index 0ab61ca73..000000000 --- a/flutter_local_notifications/windows/flutter_local_notifications_plugin.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include - -#include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" -#include "flutter_local_notifications.h" - -void FlutterLocalNotificationsPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrar* registrar -) { - local_notifications::FlutterLocalNotifications::RegisterWithRegistrar( - flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar) - ); -} diff --git a/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h b/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h deleted file mode 100644 index 42b72b514..000000000 --- a/flutter_local_notifications/windows/include/flutter_local_notifications/flutter_local_notifications_plugin.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ -#define FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ - -#include - -#ifdef FLUTTER_PLUGIN_IMPL -#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) -#else -#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) -#endif - -#if defined(__cplusplus) -extern "C" { -#endif - -FLUTTER_PLUGIN_EXPORT void FlutterLocalNotificationsPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar -); - -#if defined(__cplusplus) -} // extern "C" -#endif - -#endif FLUTTER_PLUGIN_FLUTTER_LOCAL_NOTIFICATIONS_PLUGIN_H_ diff --git a/flutter_local_notifications/windows/methods.cpp b/flutter_local_notifications/windows/methods.cpp deleted file mode 100644 index 2f0221b83..000000000 --- a/flutter_local_notifications/windows/methods.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "methods.h" - -#include - -const std::string Method::GET_NOTIFICATION_APP_LAUNCH_DETAILS = "getNotificationAppLaunchDetails"; -const std::string Method::SHOW = "show"; -const std::string Method::INITIALIZE = "initialize"; -const std::string Method::CANCEL = "cancel"; -const std::string Method::CANCEL_ALL = "cancelAll"; -const std::string Method::DID_RECEIVE_NOTIFICATION_RESPONSE = "didReceiveNotificationResponse"; -const std::string Method::GET_ACTIVE_NOTIFICATIONS = "getActiveNotifications"; -const std::string Method::GET_PENDING_NOTIFICATIONS = "pendingNotificationRequests"; -const std::string Method::SCHEDULE_NOTIFICATION = "zonedSchedule"; -const std::string Method::UPDATE = "update"; diff --git a/flutter_local_notifications/windows/methods.h b/flutter_local_notifications/windows/methods.h deleted file mode 100644 index 488ff84cb..000000000 --- a/flutter_local_notifications/windows/methods.h +++ /dev/null @@ -1,22 +0,0 @@ -#include - -#include -#include - - -/// -/// Defines names of methods of this plugin that are callable -/// through Flutter's method channel. -/// -namespace Method { - extern const std::string INITIALIZE; - extern const std::string GET_NOTIFICATION_APP_LAUNCH_DETAILS; - extern const std::string SHOW; - extern const std::string CANCEL; - extern const std::string CANCEL_ALL; - extern const std::string DID_RECEIVE_NOTIFICATION_RESPONSE; - extern const std::string GET_ACTIVE_NOTIFICATIONS; - extern const std::string GET_PENDING_NOTIFICATIONS; - extern const std::string SCHEDULE_NOTIFICATION; - extern const std::string UPDATE; -} diff --git a/flutter_local_notifications/windows/registration.h b/flutter_local_notifications/windows/registration.h deleted file mode 100644 index dbc54c19d..000000000 --- a/flutter_local_notifications/windows/registration.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef PLUGIN_REGISTRATRION_H_ -#define PLUGIN_REGISTRATRION_H_ - -#include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" -#include "methods.h" -#include "types.h" - -#include -#include -#include - -struct RegistrationUtils { - std::shared_ptr launchData; - std::shared_ptr channel; - std::shared_ptr didLaunchWithNotification; -}; - -/// -/// Registers the running app to the Windows Registry. -/// -/// The app user model ID that identifies the app. -/// The display name of the app. -/// The display name of the app. -/// An optional path to the icon of the app. -/// An optional background color of the icon, in AARRGGBB format. -/// The instance of the plugin calling this function -bool RegisterApp( - const std::string& aumid, - const std::string& appName, - const std::string& guid, - const std::optional& iconPath, - const std::optional& iconBgColor, - std::shared_ptr utils -); - -#endif // !PLUGIN_REGISTRATRION_H_ diff --git a/flutter_local_notifications/windows/types.h b/flutter_local_notifications/windows/types.h deleted file mode 100644 index 4275318a8..000000000 --- a/flutter_local_notifications/windows/types.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef FLUTTER_LOCAL_NOTIFICATION_TYPES_H_ -#define FLUTTER_LOCAL_NOTIFICATION_TYPES_H_ - -#include -#include -#include -#include - -using FlutterMap = flutter::EncodableMap; -using FlutterList = flutter::EncodableList; -using FlutterMethodCall = flutter::MethodCall; -using FlutterMethodResult = flutter::MethodResult; -using PluginMethodChannel = flutter::MethodChannel; - -#endif diff --git a/flutter_local_notifications/windows/utils.h b/flutter_local_notifications/windows/utils.h deleted file mode 100644 index 809f2e42d..000000000 --- a/flutter_local_notifications/windows/utils.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef UTILS_H_ -#define UTILS_H_ - -#include -#include -#include - -#include "methods.h" - -namespace Utils { - /// - /// Retrieves the string value stored with the given key in the given EncodableMap. - /// - /// The key that maps to the desired string value. - /// The EncodableMap that stores the key-value pair. - /// The string value that the key maps to, or nullopt if none is found. - template - std::optional GetMapValue(const std::string& key, const flutter::EncodableMap* m) { - const auto pair = m->find(flutter::EncodableValue(key)); - if (pair == m->end()) return std::nullopt; - const auto &val = pair->second; - if (std::holds_alternative(val)) return std::get(val); - return std::nullopt; - } -} - -#endif // !UTILS_H diff --git a/flutter_local_notifications_windows/ffigen.yaml b/flutter_local_notifications_windows/ffigen.yaml index 0ed073f79..82657fdd1 100644 --- a/flutter_local_notifications_windows/ffigen.yaml +++ b/flutter_local_notifications_windows/ffigen.yaml @@ -1,19 +1,29 @@ # Run with `dart run ffigen --config ffigen.yaml`. -name: FlutterLocalNotificationsWindowsBindings +name: NotificationsPluginBindings description: | - Bindings for `src/flutter_local_notifications_windows.h`. + Bindings for `src/ffi_api.h`. Regenerate bindings with `dart run ffigen --config ffigen.yaml`. -output: 'lib/ffi_bindings.dart' +output: 'lib/src/ffi/bindings.dart' + headers: entry-points: - - 'src/flutter_local_notifications_windows.h' + - 'src/ffi_api.h' include-directives: - - 'src/flutter_local_notifications_windows.h' + - 'src/ffi_api.h' + preamble: | // ignore_for_file: always_specify_types // ignore_for_file: camel_case_types // ignore_for_file: non_constant_identifier_names + comments: style: any length: full + +type-map: + native-types: + 'char': # Converts `char` to `Utf8` instead of `Char` + 'lib': 'pkg_ffi' + 'c-type': 'Utf8' + 'dart-type': 'Utf8' diff --git a/flutter_local_notifications_windows/lib/ffi_bindings.dart b/flutter_local_notifications_windows/lib/ffi_bindings.dart deleted file mode 100644 index 4a1a07d66..000000000 --- a/flutter_local_notifications_windows/lib/ffi_bindings.dart +++ /dev/null @@ -1,69 +0,0 @@ -// ignore_for_file: always_specify_types -// ignore_for_file: camel_case_types -// ignore_for_file: non_constant_identifier_names - -// AUTO GENERATED FILE, DO NOT EDIT. -// -// Generated by `package:ffigen`. -// ignore_for_file: type=lint -import 'dart:ffi' as ffi; - -/// Bindings for `src/flutter_local_notifications_windows.h`. -/// -/// Regenerate bindings with `dart run ffigen --config ffigen.yaml`. -/// -class FlutterLocalNotificationsWindowsBindings { - /// Holds the symbol lookup function. - final ffi.Pointer Function(String symbolName) - _lookup; - - /// The symbols are looked up in [dynamicLibrary]. - FlutterLocalNotificationsWindowsBindings(ffi.DynamicLibrary dynamicLibrary) - : _lookup = dynamicLibrary.lookup; - - /// The symbols are looked up with [lookup]. - FlutterLocalNotificationsWindowsBindings.fromLookup( - ffi.Pointer Function(String symbolName) - lookup) - : _lookup = lookup; - - /// A very short-lived native function. - /// - /// For very short-lived functions, it is fine to call them on the main isolate. - /// They will block the Dart execution while running the native function, so - /// only do this for native functions which are guaranteed to be short-lived. - int sum( - int a, - int b, - ) { - return _sum( - a, - b, - ); - } - - late final _sumPtr = - _lookup>('sum'); - late final _sum = _sumPtr.asFunction(); - - /// A longer lived native function, which occupies the thread calling it. - /// - /// Do not call these kind of native functions in the main isolate. They will - /// block Dart execution. This will cause dropped frames in Flutter applications. - /// Instead, call these native functions on a separate isolate. - int sum_long_running( - int a, - int b, - ) { - return _sum_long_running( - a, - b, - ); - } - - late final _sum_long_runningPtr = - _lookup>( - 'sum_long_running'); - late final _sum_long_running = - _sum_long_runningPtr.asFunction(); -} diff --git a/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart index 321504809..0816f1cfd 100644 --- a/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart +++ b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart @@ -1,131 +1,3 @@ - -import "dart:async"; -import "dart:ffi"; -import "dart:io"; -import "dart:isolate"; - -import "ffi_bindings.dart"; - -/// A very short-lived native function. -/// -/// For very short-lived functions, it is fine to call them on the main isolate. -/// They will block the Dart execution while running the native function, so -/// only do this for native functions which are guaranteed to be short-lived. -int sum(int a, int b) => _bindings.sum(a, b); - -/// A longer lived native function, which occupies the thread calling it. -/// -/// Do not call these kind of native functions in the main isolate. They will -/// block Dart execution. This will cause dropped frames in Flutter applications. -/// Instead, call these native functions on a separate isolate. -/// -/// Modify this to suit your own use case. Example use cases: -/// -/// 1. Reuse a single isolate for various different kinds of requests. -/// 2. Use multiple helper isolates for parallel execution. -Future sumAsync(int a, int b) async { - final helperIsolateSendPort = await _helperIsolateSendPort; - final requestId = _nextSumRequestId++; - final request = _SumRequest(requestId, a, b); - final completer = Completer(); - _sumRequests[requestId] = completer; - helperIsolateSendPort.send(request); - return completer.future; -} - -const String _libName = "flutter_local_notifications_windows"; - -/// The dynamic library in which the symbols for [FlutterLocalNotificationsWindowsBindings] can be found. -final DynamicLibrary _dylib = () { - if (Platform.isMacOS || Platform.isIOS) { - return DynamicLibrary.open("$_libName.framework/$_libName"); - } - if (Platform.isAndroid || Platform.isLinux) { - return DynamicLibrary.open("lib$_libName.so"); - } - if (Platform.isWindows) { - return DynamicLibrary.open("$_libName.dll"); - } - throw UnsupportedError("Unknown platform: ${Platform.operatingSystem}"); -}(); - -/// The bindings to the native functions in [_dylib]. -final FlutterLocalNotificationsWindowsBindings _bindings = FlutterLocalNotificationsWindowsBindings(_dylib); - - -/// A request to compute `sum`. -/// -/// Typically sent from one isolate to another. -class _SumRequest { - final int id; - final int a; - final int b; - - const _SumRequest(this.id, this.a, this.b); -} - -/// A response with the result of `sum`. -/// -/// Typically sent from one isolate to another. -class _SumResponse { - final int id; - final int result; - - const _SumResponse(this.id, this.result); -} - -/// Counter to identify [_SumRequest]s and [_SumResponse]s. -int _nextSumRequestId = 0; - -/// Mapping from [_SumRequest] `id`s to the completers corresponding to the correct future of the pending request. -final Map> _sumRequests = >{}; - -/// The SendPort belonging to the helper isolate. -Future _helperIsolateSendPort = () async { - // The helper isolate is going to send us back a SendPort, which we want to - // wait for. - final completer = Completer(); - - // Receive port on the main isolate to receive messages from the helper. - // We receive two types of messages: - // 1. A port to send messages on. - // 2. Responses to requests we sent. - final receivePort = ReceivePort() - ..listen((dynamic data) { - if (data is SendPort) { - // The helper isolate sent us the port on which we can sent it requests. - completer.complete(data); - return; - } - if (data is _SumResponse) { - // The helper isolate sent us a response to a request we sent. - final completer = _sumRequests[data.id]!; - _sumRequests.remove(data.id); - completer.complete(data.result); - return; - } - throw UnsupportedError("Unsupported message type: ${data.runtimeType}"); - }); - - // Start the helper isolate. - await Isolate.spawn((sendPort) async { - final helperReceivePort = ReceivePort() - ..listen((dynamic data) { - // On the helper isolate listen to requests and respond to them. - if (data is _SumRequest) { - final result = _bindings.sum_long_running(data.a, data.b); - final response = _SumResponse(data.id, result); - sendPort.send(response); - return; - } - throw UnsupportedError("Unsupported message type: ${data.runtimeType}"); - }); - - // Send the port to the main isolate on which we can receive requests. - sendPort.send(helperReceivePort.sendPort); - }, receivePort.sendPort,); - - // Wait until the helper isolate has sent us back the SendPort on which we - // can start sending requests. - return completer.future; -}(); +export "src/details.dart"; +export "src/plugin/stub.dart" + if (dart.library.ffi) "src/plugin/ffi.dart"; diff --git a/flutter_local_notifications_windows/lib/src/details.dart b/flutter_local_notifications_windows/lib/src/details.dart new file mode 100644 index 000000000..252be856a --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details.dart @@ -0,0 +1,18 @@ +export "details/initialization_settings.dart"; +export "details/notification_action.dart"; +export "details/notification_audio.dart"; +export "details/notification_details.dart"; +export "details/notification_group.dart"; +export "details/notification_header.dart"; +export "details/notification_image.dart"; +export "details/notification_input.dart"; +export "details/notification_part.dart"; +export "details/notification_progress.dart"; +export "details/notification_text.dart"; +export "details/notification_to_xml.dart"; + +enum NotificationUpdateResult { + success, + error, + notFound, +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart b/flutter_local_notifications_windows/lib/src/details/initialization_settings.dart similarity index 85% rename from flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart rename to flutter_local_notifications_windows/lib/src/details/initialization_settings.dart index b49b3b316..507d85929 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/initialization_settings.dart +++ b/flutter_local_notifications_windows/lib/src/details/initialization_settings.dart @@ -1,5 +1,3 @@ -import 'package:flutter/widgets.dart'; - /// Plugin initialization settings for Windows. class WindowsInitializationSettings { /// Creates a new settings object for initializing this plugin on Windows. @@ -8,7 +6,6 @@ class WindowsInitializationSettings { required this.appUserModelId, required this.guid, this.iconPath, - this.iconBackgroundColor, }); /// The name of the app that should be shown in the notification toast. @@ -26,7 +23,4 @@ class WindowsInitializationSettings { /// The path to the icon of the notification. final String? iconPath; - - /// The background color to the icon. - final Color? iconBackgroundColor; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart b/flutter_local_notifications_windows/lib/src/details/notification_action.dart similarity index 84% rename from flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart rename to flutter_local_notifications_windows/lib/src/details/notification_action.dart index 2cc48d977..09bf912e2 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_action.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_action.dart @@ -1,6 +1,6 @@ -import 'dart:io'; +import "dart:io"; -import 'package:xml/xml.dart'; +import "package:xml/xml.dart"; // NOTE: All enum values in this file have Windows RT-specific names. // If you change their Dart names, be sure to override [Enum.name]. @@ -20,9 +20,9 @@ enum WindowsActivationType { /// Decides how a [WindowsAction] will react to being pressed. enum WindowsNotificationBehavior { /// The notification will be dismissed. - dismiss('default'), + dismiss("default"), /// The notification will remain on screen and show a loading status. - pendingUpdate('pendingUpdate'); + pendingUpdate("pendingUpdate"); const WindowsNotificationBehavior(this.name); /// The Windows API name for this choice. @@ -32,9 +32,9 @@ enum WindowsNotificationBehavior { /// Decides how a [WindowsAction] will be styled. enum WindowsButtonStyle { /// A green button. - success('Success'), + success("Success"), /// A red button. - critical('Critical'); + critical("Critical"); const WindowsButtonStyle(this.name); /// The Windows API name for this choice. @@ -66,8 +66,8 @@ class WindowsAction { if (image != null && !image!.isAbsolute) { throw ArgumentError.value( image!.path, - 'WindowsImage.file', - 'File path must be absolute', + "WindowsImage.file", + "File path must be absolute", ); } } @@ -115,18 +115,18 @@ class WindowsAction { /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action#syntax void toXml(XmlBuilder builder) => builder.element( - 'action', + "action", attributes: { - 'content': content, - 'arguments': arguments, - 'activationType': activationType.name, - 'afterActivationBehavior': activationBehavior.name, - if (placement != null) 'placement': placement!.name, - if (image != null) 'imageUri': + "content": content, + "arguments": arguments, + "activationType": activationType.name, + "afterActivationBehavior": activationBehavior.name, + if (placement != null) "placement": placement!.name, + if (image != null) "imageUri": Uri.file(image!.absolute.path, windows: true).toFilePath(), - if (inputId != null) 'hint-inputId': inputId!, - if (buttonStyle != null) 'hint-buttonStyle': buttonStyle!.name, - if (tooltip != null) 'hint-toolTip': tooltip!, + if (inputId != null) "hint-inputId": inputId!, + if (buttonStyle != null) "hint-buttonStyle": buttonStyle!.name, + if (tooltip != null) "hint-toolTip": tooltip!, }, ); } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart similarity index 53% rename from flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart rename to flutter_local_notifications_windows/lib/src/details/notification_audio.dart index 3751850c2..bb2e140ea 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_audio.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart @@ -1,62 +1,62 @@ -import 'package:xml/xml.dart'; +import "package:xml/xml.dart"; extension on Uri { String get filename => pathSegments.last; - String get extension => pathSegments.last.split('.').last; + String get extension => pathSegments.last.split(".").last; } /// A preset sound for a Windows notification. enum WindowsNotificationSound { /// The default sound. - defaultSound('ms-winsoundevent:Notification.Default'), + defaultSound("ms-winsoundevent:Notification.Default"), /// The IM sound. - im('ms-winsoundevent:Notification.IM'), + im("ms-winsoundevent:Notification.IM"), /// The Mail sound. - mail('ms-winsoundevent:Notification.Mail'), + mail("ms-winsoundevent:Notification.Mail"), /// The Reminder sound. - reminder('ms-winsoundevent:Notification.Reminder'), + reminder("ms-winsoundevent:Notification.Reminder"), /// The SMS sound. - sms('ms-winsoundevent:Notification.SMS'), + sms("ms-winsoundevent:Notification.SMS"), /// Alarm sound 1. - alarm1('ms-winsoundevent:Notification.Looping.Alarm1'), + alarm1("ms-winsoundevent:Notification.Looping.Alarm1"), /// Alarm sound 2. - alarm2('ms-winsoundevent:Notification.Looping.Alarm2'), + alarm2("ms-winsoundevent:Notification.Looping.Alarm2"), /// Alarm sound 3. - alarm3('ms-winsoundevent:Notification.Looping.Alarm3'), + alarm3("ms-winsoundevent:Notification.Looping.Alarm3"), /// Alarm sound 4. - alarm4('ms-winsoundevent:Notification.Looping.Alarm4'), + alarm4("ms-winsoundevent:Notification.Looping.Alarm4"), /// Alarm sound 5. - alarm5('ms-winsoundevent:Notification.Looping.Alarm5'), + alarm5("ms-winsoundevent:Notification.Looping.Alarm5"), /// Alarm sound 6. - alarm6('ms-winsoundevent:Notification.Looping.Alarm6'), + alarm6("ms-winsoundevent:Notification.Looping.Alarm6"), /// Alarm sound 7. - alarm7('ms-winsoundevent:Notification.Looping.Alarm7'), + alarm7("ms-winsoundevent:Notification.Looping.Alarm7"), /// Alarm sound 8. - alarm8('ms-winsoundevent:Notification.Looping.Alarm8'), + alarm8("ms-winsoundevent:Notification.Looping.Alarm8"), /// Alarm sound 9. - alarm9('ms-winsoundevent:Notification.Looping.Alarm9'), + alarm9("ms-winsoundevent:Notification.Looping.Alarm9"), /// Alarm sound 10. - alarm10('ms-winsoundevent:Notification.Looping.Alarm10'), + alarm10("ms-winsoundevent:Notification.Looping.Alarm10"), /// Call sound 1. - call1('ms-winsoundevent:Notification.Looping.Call1'), + call1("ms-winsoundevent:Notification.Looping.Call1"), /// Call sound 2. - call2('ms-winsoundevent:Notification.Looping.Call2'), + call2("ms-winsoundevent:Notification.Looping.Call2"), /// Call sound 3. - call3('ms-winsoundevent:Notification.Looping.Call3'), + call3("ms-winsoundevent:Notification.Looping.Call3"), /// Call sound 4. - call4('ms-winsoundevent:Notification.Looping.Call4'), + call4("ms-winsoundevent:Notification.Looping.Call4"), /// Call sound 5. - call5('ms-winsoundevent:Notification.Looping.Call5'), + call5("ms-winsoundevent:Notification.Looping.Call5"), /// Call sound 6. - call6('ms-winsoundevent:Notification.Looping.Call6'), + call6("ms-winsoundevent:Notification.Looping.Call6"), /// Call sound 7. - call7('ms-winsoundevent:Notification.Looping.Call7'), + call7("ms-winsoundevent:Notification.Looping.Call7"), /// Call sound 8. - call8('ms-winsoundevent:Notification.Looping.Call8'), + call8("ms-winsoundevent:Notification.Looping.Call8"), /// Call sound 9. - call9('ms-winsoundevent:Notification.Looping.Call9'), + call9("ms-winsoundevent:Notification.Looping.Call9"), /// Call sound 10. - call10('ms-winsoundevent:Notification.Looping.Call10'); + call10("ms-winsoundevent:Notification.Looping.Call10"); const WindowsNotificationSound(this.name); /// The Windows API name for this sound. @@ -87,28 +87,28 @@ class WindowsNotificationAudio { if (!allowedSchemes.contains(file.scheme)) { throw ArgumentError.value( file.toString(), - 'WindowsNotificationAudio.file', - 'URI scheme must be one of the following schemes: $allowedSchemes', + "WindowsNotificationAudio.file", + "URI scheme must be one of the following schemes: $allowedSchemes", ); } if ( - !file.filename.contains('.') || + !file.filename.contains(".") || !allowedExtensions.contains(file.extension) ) { throw ArgumentError.value( file.toString(), - 'WindowsNotificationAudio.file', - 'File extension must be one of the following: $allowedExtensions', + "WindowsNotificationAudio.file", + "File extension must be one of the following: $allowedExtensions", ); } } /// Allowed Uri schemes for [WindowsNotificationAudio.fromFile]. - static const Set allowedSchemes = {'ms-appx', 'ms-resource'}; + static const Set allowedSchemes = {"ms-appx", "ms-resource"}; /// Allowed file extensions for [WindowsNotificationAudio.fromFile]. static const Set allowedExtensions = - {'aac', 'flac', 'm4a', 'mp3', 'wav', 'wma'}; + {"aac", "flac", "m4a", "mp3", "wav", "wma"}; /// Whether this audio should loop. final bool shouldLoop; @@ -119,11 +119,11 @@ class WindowsNotificationAudio { /// Serializes this audio to Windows-compatible XML. void toXml(XmlBuilder builder) => builder.element( - 'audio', + "audio", attributes: { - 'src': source, - 'silent': isSilent.toString(), - 'loop': shouldLoop.toString(), + "src": source, + "silent": isSilent.toString(), + "loop": shouldLoop.toString(), }, ); } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart b/flutter_local_notifications_windows/lib/src/details/notification_details.dart similarity index 69% rename from flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart rename to flutter_local_notifications_windows/lib/src/details/notification_details.dart index 692a92ee3..2c5122c50 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_details.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_details.dart @@ -1,172 +1,178 @@ -import 'package:xml/xml.dart'; - -import 'notification_action.dart'; -import 'notification_audio.dart'; -import 'notification_group.dart'; -import 'notification_header.dart'; -import 'notification_image.dart'; -import 'notification_input.dart'; -import 'notification_progress.dart'; - -export 'notification_part.dart'; -export 'notification_text.dart'; - -/// The duration for a Windows notification. -enum WindowsNotificationDuration { - /// The notification will stay for a long time. - long, - /// The notification will stay for a short time. - short, -} - -/// The scenario a notification is being used for. -enum WindowsNotificationScenario { - /// Reminders are expanded and remain until manually dismissed. - /// - /// This will be ignored unless the notification also has at least one - /// [WindowsAction] that activates a background task. - reminder, - - /// Alarms are expanded and remain until manually dismissed. - /// - /// By default, alarm notifications loop the standard "alarm" sound. - alarm, - - /// Calls are expanded and show in a special format. - /// - /// By default, call notifications loop the standard "call" sound. - incomingCall, - - /// Urgent notifications can break through Do Not Disturb settings. - urgent, -} - -extension on DateTime { - String toIso8601StringTz() { - // Get offset - final Duration offset = timeZoneOffset; - final String sign = offset.isNegative ? '-' : '+'; - final String hours = offset.inHours.abs().toString().padLeft(2, '0'); - final String minutes = offset.inMinutes.abs().remainder(60) - .toString().padLeft(2, '0'); - final String offsetString = '$sign$hours:$minutes'; - - // Get first part of properly formatted ISO 8601 date - final String formattedDate = toIso8601String().split('.').first; - - return '$formattedDate$offsetString'; - } -} - -/// Contains notification details specific to Windows. -/// -/// See: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts -class WindowsNotificationDetails { - /// Creates a Windows notification from the given options. - WindowsNotificationDetails({ - this.actions = const [], - this.inputs = const [], - this.images = const [], - this.groups = const [], - this.progressBars = const [], - this.header, - this.audio, - this.duration, - this.scenario, - this.timestamp, - this.subtitle, - }) : rawXml = null { - if (actions.length > 5) { - throw ArgumentError( - 'WindowsNotificationDetails can only have up to 5 actions', - ); - } - if (inputs.length > 5) { - throw ArgumentError( - 'WindowsNotificationDetails can only have up to 5 inputs', - ); - } - } - - /// The raw XML passed to the Windows API. - /// - /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. - /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). - final String? rawXml; - - /// A list of at most five action buttons. - final List actions; - - /// A list of at most five input elements. - final List inputs; - - /// A custom audio to play during this notification. - final WindowsNotificationAudio? audio; - - /// The duration for this notification. - final WindowsNotificationDuration? duration; - - /// The scenario for this notification. Sets some defaults based on the value. - final WindowsNotificationScenario? scenario; - - /// The header for this group of notifications. - final WindowsHeader? header; - - /// Overrides the timestamp to show on the notification. - final DateTime? timestamp; - - /// A third line to show under the notification body. - final String? subtitle; - - /// A list of images to show. - final List images; - - /// A list of groups to show. - final List groups; - - /// A list of progress bars to show. - final List progressBars; - - /// XML attributes for the toast notification as a whole. - Map get attributes => { - if (duration != null) 'duration': duration!.name, - if (timestamp != null) 'displayTimestamp': timestamp!.toIso8601StringTz(), - if (scenario != null) 'scenario': scenario!.name, - }; - - /// Builds all relevant XML parts under the root `` element. - void toXml(XmlBuilder builder) { - if (rawXml != null) { - builder.xml(rawXml!); - return; - } - builder.element('actions', nest: () { - for (final WindowsInput input in inputs) { - input.toXml(builder); - } - for (final WindowsAction action in actions) { - action.toXml(builder); - } - }); - audio?.toXml(builder); - header?.toXml(builder); - } - - /// Generates the `` element of the notification. - /// - /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-binding - void generateBinding(XmlBuilder builder) { - if (subtitle != null) { - builder.element('text', nest: subtitle); - } - for (final WindowsImage image in images) { - image.toXml(builder); - } - for (final WindowsGroup group in groups) { - group.toXml(builder); - } - for (final WindowsProgressBar progressBar in progressBars) { - progressBar.toXml(builder); - } - } -} +import "package:xml/xml.dart"; + +import "notification_action.dart"; +import "notification_audio.dart"; +import "notification_group.dart"; +import "notification_header.dart"; +import "notification_image.dart"; +import "notification_input.dart"; +import "notification_progress.dart"; + +export "notification_part.dart"; +export "notification_text.dart"; + +/// The duration for a Windows notification. +enum WindowsNotificationDuration { + /// The notification will stay for a long time. + long, + /// The notification will stay for a short time. + short, +} + +/// The scenario a notification is being used for. +enum WindowsNotificationScenario { + /// Reminders are expanded and remain until manually dismissed. + /// + /// This will be ignored unless the notification also has at least one + /// [WindowsAction] that activates a background task. + reminder, + + /// Alarms are expanded and remain until manually dismissed. + /// + /// By default, alarm notifications loop the standard "alarm" sound. + alarm, + + /// Calls are expanded and show in a special format. + /// + /// By default, call notifications loop the standard "call" sound. + incomingCall, + + /// Urgent notifications can break through Do Not Disturb settings. + urgent, +} + +extension on DateTime { + String toIso8601StringTz() { + // Get offset + final offset = timeZoneOffset; + final sign = offset.isNegative ? "-" : "+"; + final hours = offset.inHours.abs().toString().padLeft(2, "0"); + final minutes = offset.inMinutes.abs().remainder(60) + .toString().padLeft(2, "0"); + final offsetString = "$sign$hours:$minutes"; + // Get first part of properly formatted ISO 8601 date + final formattedDate = toIso8601String().split(".").first; + return "$formattedDate$offsetString"; + } +} + +/// Contains notification details specific to Windows. +/// +/// See: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts +class WindowsNotificationDetails { + /// Creates a Windows notification from the given options. + WindowsNotificationDetails({ + this.actions = const [], + this.inputs = const [], + this.images = const [], + this.groups = const [], + this.progressBars = const [], + this.bindings = const {}, + this.header, + this.audio, + this.duration, + this.scenario, + this.timestamp, + this.subtitle, + }) : rawXml = null { + if (actions.length > 5) { + throw ArgumentError( + "WindowsNotificationDetails can only have up to 5 actions", + ); + } + if (inputs.length > 5) { + throw ArgumentError( + "WindowsNotificationDetails can only have up to 5 inputs", + ); + } + } + + /// The raw XML passed to the Windows API. + /// + /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. + /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). + final String? rawXml; + + /// A list of at most five action buttons. + final List actions; + + /// A list of at most five input elements. + final List inputs; + + /// A custom audio to play during this notification. + final WindowsNotificationAudio? audio; + + /// The duration for this notification. + final WindowsNotificationDuration? duration; + + /// The scenario for this notification. Sets some defaults based on the value. + final WindowsNotificationScenario? scenario; + + /// The header for this group of notifications. + final WindowsHeader? header; + + /// Overrides the timestamp to show on the notification. + final DateTime? timestamp; + + /// A third line to show under the notification body. + final String? subtitle; + + /// A list of images to show. + final List images; + + /// A list of groups to show. + final List groups; + + /// A list of progress bars to show. + final List progressBars; + + /// Custom bindings in the notification. + /// + /// Text elements can contains "bindings", which are entered as `` directly into the + /// string values. You can then update them while or after the notification is launched by + /// using the binding name as the key here, and the value as any string you want + final Map bindings; + + /// XML attributes for the toast notification as a whole. + Map get attributes => { + if (duration != null) "duration": duration!.name, + if (timestamp != null) "displayTimestamp": timestamp!.toIso8601StringTz(), + if (scenario != null) "scenario": scenario!.name, + }; + + /// Builds all relevant XML parts under the root `` element. + void toXml(XmlBuilder builder) { + if (rawXml != null) { + builder.xml(rawXml!); + return; + } + builder.element("actions", nest: () { + for (final input in inputs) { + input.toXml(builder); + } + for (final action in actions) { + action.toXml(builder); + } + },); + audio?.toXml(builder); + header?.toXml(builder); + } + + /// Generates the `` element of the notification. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-binding + void generateBinding(XmlBuilder builder) { + if (subtitle != null) { + builder.element("text", nest: subtitle); + } + for (final image in images) { + image.toXml(builder); + } + for (final group in groups) { + group.toXml(builder); + } + for (final progressBar in progressBars) { + progressBar.toXml(builder); + } + } +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_group.dart b/flutter_local_notifications_windows/lib/src/details/notification_group.dart similarity index 74% rename from flutter_local_notifications/lib/src/platform_specifics/windows/notification_group.dart rename to flutter_local_notifications_windows/lib/src/details/notification_group.dart index e7be6e89e..13b4ab0cf 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_group.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_group.dart @@ -1,6 +1,6 @@ -import 'package:xml/xml.dart'; +import "package:xml/xml.dart"; -import 'notification_part.dart'; +import "notification_part.dart"; /// A group of notification content that must be displayed as a whole. /// @@ -14,20 +14,20 @@ class WindowsGroup { /// Serializes this group to XML. void toXml(XmlBuilder builder) => builder.element( - 'group', + "group", nest: () { - for (final WindowsColumn column in columns) { + for (final column in columns) { builder.element( - 'subgroup', - attributes: {'hint-weight': '1'}, + "subgroup", + attributes: {"hint-weight": "1"}, nest: () { - for (final WindowsNotificationPart part in column.parts) { + for (final part in column.parts) { part.toXml(builder); } }, ); } - } + }, ); } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_header.dart b/flutter_local_notifications_windows/lib/src/details/notification_header.dart similarity index 83% rename from flutter_local_notifications/lib/src/platform_specifics/windows/notification_header.dart rename to flutter_local_notifications_windows/lib/src/details/notification_header.dart index 4efa913bc..6ea42d719 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_header.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_header.dart @@ -1,4 +1,4 @@ -import 'package:xml/xml.dart'; +import "package:xml/xml.dart"; /// Decides how the application will open when the header is pressed. enum WindowsHeaderActivation { @@ -33,12 +33,12 @@ class WindowsHeader { /// Serializes this header to XML. void toXml(XmlBuilder builder) => builder.element( - 'header', + "header", attributes: { - 'id': id, - 'title': title, - 'arguments': arguments, - if (activation != null) 'activationType': activation!.name, + "id": id, + "title": title, + "arguments": arguments, + if (activation != null) "activationType": activation!.name, }, ); } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart b/flutter_local_notifications_windows/lib/src/details/notification_image.dart similarity index 76% rename from flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart rename to flutter_local_notifications_windows/lib/src/details/notification_image.dart index 26eb1fea9..62a12d8dc 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_image.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_image.dart @@ -1,8 +1,8 @@ -import 'dart:io'; +import "dart:io"; -import 'package:xml/xml.dart'; +import "package:xml/xml.dart"; -import 'notification_part.dart'; +import "notification_part.dart"; /// Where a Windows notification image can be placed. enum WindowsImagePlacement { @@ -31,8 +31,8 @@ class WindowsImage extends WindowsNotificationPart { if (!file.isAbsolute) { throw ArgumentError.value( file.path, - 'WindowsImage.file', - 'File path must be absolute', + "WindowsImage.file", + "File path must be absolute", ); } } @@ -54,13 +54,13 @@ class WindowsImage extends WindowsNotificationPart { @override void toXml(XmlBuilder builder) => builder.element( - 'image', + "image", attributes: { - 'src': Uri.file(file.absolute.path, windows: true).toFilePath(), - 'alt': altText, - 'addImageQuery': addQueryParams.toString(), - if (placement != null) 'placement': placement!.name, - if (crop != null) 'hint-crop': crop!.name, + "src": Uri.file(file.absolute.path, windows: true).toFilePath(), + "alt": altText, + "addImageQuery": addQueryParams.toString(), + if (placement != null) "placement": placement!.name, + if (crop != null) "hint-crop": crop!.name, }, ); } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart b/flutter_local_notifications_windows/lib/src/details/notification_input.dart similarity index 82% rename from flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart rename to flutter_local_notifications_windows/lib/src/details/notification_input.dart index c7b1a11c1..c535bc3c2 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_input.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_input.dart @@ -1,4 +1,4 @@ -import 'package:xml/xml.dart'; +import "package:xml/xml.dart"; /// The type of a [WindowsInput]. enum WindowsInputType { @@ -46,12 +46,12 @@ class WindowsTextInput extends WindowsInput { @override void toXml(XmlBuilder builder) => builder.element( - 'input', + "input", attributes: { - 'id': id, - 'type': type.name, - if (title != null) 'title': title!, - if (hintText != null) 'placeHolderContent': hintText!, + "id": id, + "type": type.name, + if (title != null) "title": title!, + if (hintText != null) "placeHolderContent": hintText!, }, ); } @@ -74,20 +74,19 @@ class WindowsSelectionInput extends WindowsInput { @override void toXml(XmlBuilder builder) => builder.element( - 'input', + "input", attributes: { - 'id': id, - 'type': type.name, - if (title != null) 'title': title!, - if (defaultItem != null) 'defaultInput': defaultItem!, + "id": id, + "type": type.name, + if (title != null) "title": title!, + if (defaultItem != null) "defaultInput": defaultItem!, }, nest: () { - for (final WindowsSelection item in items) { + for (final item in items) { item.toXml(builder); } - } + }, ); - } /// An option that can be selected by a [WindowsSelectionInput]. @@ -106,10 +105,10 @@ class WindowsSelection { /// Serializes this item to XML. void toXml(XmlBuilder builder) => builder.element( - 'selection', + "selection", attributes: { - 'id': id, - 'content': content, + "id": id, + "content": content, }, ); } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_part.dart b/flutter_local_notifications_windows/lib/src/details/notification_part.dart similarity index 92% rename from flutter_local_notifications/lib/src/platform_specifics/windows/notification_part.dart rename to flutter_local_notifications_windows/lib/src/details/notification_part.dart index d53fb035f..40f42a810 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_part.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_part.dart @@ -1,4 +1,4 @@ -import 'package:xml/xml.dart'; +import "package:xml/xml.dart"; /// A text or image element in a Windows notification. /// diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart similarity index 71% rename from flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart rename to flutter_local_notifications_windows/lib/src/details/notification_progress.dart index de662c84d..95bf16208 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_progress.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart @@ -1,11 +1,11 @@ -import 'package:xml/xml.dart'; +import "package:xml/xml.dart"; -import '../../../flutter_local_notifications.dart'; +import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; /// A progress bar in a Windows notification. /// /// To update the progress after the notification has been shown, -/// use [WindowsFlutterLocalNotificationsPlugin.updateProgress]. +/// use [FlutterLocalNotificationsWindows.updateProgressBar]. class WindowsProgressBar { /// Creates a progress bar for a Windows notification. WindowsProgressBar({ @@ -37,13 +37,13 @@ class WindowsProgressBar { /// Serializes this progress bar to XML. void toXml(XmlBuilder builder) => builder.element( - 'progress', + "progress", attributes: { - 'status': status, - 'value': '{$id-progressValue}', - if (title != null) 'title': title!, - if (label != null) 'valueStringOverride': '{$id-progressString}', - } + "status": status, + "value": "{$id-progressValue}", + if (title != null) "title": title!, + if (label != null) "valueStringOverride": "{$id-progressString}", + }, ); /// The data bindings for this progress bar. @@ -52,7 +52,7 @@ class WindowsProgressBar { /// called data bindings instead of actual values. This represents the /// new data. Map get data => { - '$id-progressValue': value?.toString() ?? 'indeterminate', - if (label != null) '$id-progressString': label! + "$id-progressValue": value?.toString() ?? "indeterminate", + if (label != null) "$id-progressString": label!, }; } diff --git a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_text.dart b/flutter_local_notifications_windows/lib/src/details/notification_text.dart similarity index 76% rename from flutter_local_notifications/lib/src/platform_specifics/windows/notification_text.dart rename to flutter_local_notifications_windows/lib/src/details/notification_text.dart index 93ddf7ab1..637595eef 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/windows/notification_text.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_text.dart @@ -1,6 +1,6 @@ -import 'package:xml/xml.dart'; +import "package:xml/xml.dart"; -import 'notification_part.dart'; +import "notification_part.dart"; /// Where text can be placed in a Windows notification. enum WindowsTextPlacement { @@ -38,13 +38,13 @@ class WindowsNotificationText extends WindowsNotificationPart { @override void toXml(XmlBuilder builder) => builder.element( - 'text', + "text", attributes: { - if (languageCode != null) 'lang': languageCode!, - if (placement != null) 'placement': placement!.name, - 'hint-callScenarioCenterAlign': centerIfCall.toString(), - 'hint-align': 'center', - if (isCaption) 'hint-style': 'captionsubtle', + if (languageCode != null) "lang": languageCode!, + if (placement != null) "placement": placement!.name, + "hint-callScenarioCenterAlign": centerIfCall.toString(), + "hint-align": "center", + if (isCaption) "hint-style": "captionsubtle", }, nest: text, ); diff --git a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart new file mode 100644 index 000000000..69cc2c6ec --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart @@ -0,0 +1,36 @@ +import "package:xml/xml.dart"; + +import "notification_details.dart"; + +String notificationToXml({ + String? title, + String? body, + String? payload, + WindowsNotificationDetails? details, +}) { + final builder = XmlBuilder(); + builder.element( + "toast", + attributes: { + ...details?.attributes ?? {}, + if (payload != null) "launch": payload, + if (details?.scenario == null) "useButtonStyle": "true", + }, + nest: () { + builder.element("visual", nest: () { + builder.element( + "binding", + attributes: {"template": "ToastGeneric"}, + nest: () { + builder.element("text", nest: title); + builder.element("text", nest: body); + details?.generateBinding(builder); + }, + ); + },); + details?.toXml(builder); + }, + ); + return builder.buildDocument() + .toXmlString(pretty: true, indentAttribute: (_) => true); +} diff --git a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart new file mode 100644 index 000000000..52b8e4991 --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart @@ -0,0 +1,318 @@ +// ignore_for_file: always_specify_types +// ignore_for_file: camel_case_types +// ignore_for_file: non_constant_identifier_names + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; +import 'package:ffi/ffi.dart' as pkg_ffi; + +/// Bindings for `src/ffi_api.h`. +/// +/// Regenerate bindings with `dart run ffigen --config ffigen.yaml`. +/// +class NotificationsPluginBindings { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + NotificationsPluginBindings(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + NotificationsPluginBindings.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + ffi.Pointer createPlugin() { + return _createPlugin(); + } + + late final _createPluginPtr = + _lookup Function()>>( + 'createPlugin'); + late final _createPlugin = + _createPluginPtr.asFunction Function()>(); + + void disposePlugin( + ffi.Pointer ptr, + ) { + return _disposePlugin( + ptr, + ); + } + + late final _disposePluginPtr = + _lookup)>>( + 'disposePlugin'); + late final _disposePlugin = + _disposePluginPtr.asFunction)>(); + + int init( + ffi.Pointer plugin, + ffi.Pointer appName, + ffi.Pointer aumId, + ffi.Pointer guid, + ffi.Pointer iconPath, + NativeNotificationCallback callback, + ) { + return _init( + plugin, + appName, + aumId, + guid, + iconPath, + callback, + ); + } + + late final _initPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + NativeNotificationCallback)>>('init'); + late final _init = _initPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + NativeNotificationCallback)>(); + + int showNotification( + ffi.Pointer plugin, + int id, + ffi.Pointer xml, + ffi.Pointer bindings, + int bindingsSize, + ) { + return _showNotification( + plugin, + id, + xml, + bindings, + bindingsSize, + ); + } + + late final _showNotificationPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Pointer, + ffi.Int)>>('showNotification'); + late final _showNotification = _showNotificationPtr.asFunction< + int Function(ffi.Pointer, int, ffi.Pointer, + ffi.Pointer, int)>(); + + int scheduleNotification( + ffi.Pointer plugin, + int id, + ffi.Pointer xml, + int time, + ) { + return _scheduleNotification( + plugin, + id, + xml, + time, + ); + } + + late final _scheduleNotificationPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, ffi.Int, + ffi.Pointer, ffi.Int)>>('scheduleNotification'); + late final _scheduleNotification = _scheduleNotificationPtr.asFunction< + int Function( + ffi.Pointer, int, ffi.Pointer, int)>(); + + int updateNotification( + ffi.Pointer plugin, + int id, + ffi.Pointer bindings, + int bindingsSize, + ) { + return _updateNotification( + plugin, + id, + bindings, + bindingsSize, + ); + } + + late final _updateNotificationPtr = _lookup< + ffi.NativeFunction< + ffi.Int32 Function(ffi.Pointer, ffi.Int, + ffi.Pointer, ffi.Int)>>('updateNotification'); + late final _updateNotification = _updateNotificationPtr.asFunction< + int Function(ffi.Pointer, int, ffi.Pointer, int)>(); + + void cancelAll( + ffi.Pointer plugin, + ) { + return _cancelAll( + plugin, + ); + } + + late final _cancelAllPtr = + _lookup)>>( + 'cancelAll'); + late final _cancelAll = + _cancelAllPtr.asFunction)>(); + + void cancelNotification( + ffi.Pointer plugin, + int id, + ) { + return _cancelNotification( + plugin, + id, + ); + } + + late final _cancelNotificationPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.Int)>>('cancelNotification'); + late final _cancelNotification = _cancelNotificationPtr + .asFunction, int)>(); + + ffi.Pointer getActiveNotifications( + ffi.Pointer plugin, + ffi.Pointer size, + ) { + return _getActiveNotifications( + plugin, + size, + ); + } + + late final _getActiveNotificationsPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer)>>('getActiveNotifications'); + late final _getActiveNotifications = _getActiveNotificationsPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>(); + + ffi.Pointer getPendingNotifications( + ffi.Pointer plugin, + ffi.Pointer size, + ) { + return _getPendingNotifications( + plugin, + size, + ); + } + + late final _getPendingNotificationsPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer)>>('getPendingNotifications'); + late final _getPendingNotifications = _getPendingNotificationsPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>(); + + ffi.Pointer getLaunchDetails( + ffi.Pointer plugin, + ) { + return _getLaunchDetails( + plugin, + ); + } + + late final _getLaunchDetailsPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('getLaunchDetails'); + late final _getLaunchDetails = _getLaunchDetailsPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + void freeDetailsArray( + ffi.Pointer ptr, + ) { + return _freeDetailsArray( + ptr, + ); + } + + late final _freeDetailsArrayPtr = _lookup< + ffi.NativeFunction)>>( + 'freeDetailsArray'); + late final _freeDetailsArray = _freeDetailsArrayPtr + .asFunction)>(); + + void freePairArray( + ffi.Pointer ptr, + ) { + return _freePairArray( + ptr, + ); + } + + late final _freePairArrayPtr = + _lookup)>>( + 'freePairArray'); + late final _freePairArray = + _freePairArrayPtr.asFunction)>(); +} + +class NativePlugin extends ffi.Opaque {} + +class Pair extends ffi.Struct { + external ffi.Pointer key; + + external ffi.Pointer value; +} + +class NativeDetails extends ffi.Struct { + @ffi.Int() + external int id; +} + +abstract class NativeLaunchType { + static const int notification = 0; + static const int action = 1; +} + +class NativeLaunchDetails extends ffi.Struct { + @ffi.Int() + external int didLaunch; + + @ffi.Int32() + external int launchType; + + external ffi.Pointer payload; + + @ffi.Int() + external int payloadSize; + + external ffi.Pointer data; + + @ffi.Int() + external int dataSize; +} + +/// See: https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.notificationupdateresult +abstract class NativeUpdateResult { + static const int success = 0; + static const int failed = 1; + static const int notFound = 2; +} + +typedef NativeNotificationCallback = ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer details)>>; diff --git a/flutter_local_notifications_windows/lib/src/ffi/utils.dart b/flutter_local_notifications_windows/lib/src/ffi/utils.dart new file mode 100644 index 000000000..ffac5b917 --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/ffi/utils.dart @@ -0,0 +1,61 @@ +import "dart:ffi"; + +import "package:ffi/ffi.dart"; +import "package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart"; +import "package:flutter_local_notifications_windows/src/plugin/base.dart"; + +import "bindings.dart"; +import "../details.dart"; + +typedef NativeCallbackType = Void Function(Pointer details); + +extension PairUtils on Pointer { + Map toMap(int length) => { + for (var index = 0; index < length; index++) + this[index].key.toDartString(): this[index].value.toDartString(), + }; +} + +extension IntUtils on int { + bool toBool() => this == 1; +} + +NotificationResponseType getResponseType(int launchType) { + switch (launchType) { + case NativeLaunchType.notification: return NotificationResponseType.selectedNotification; + case NativeLaunchType.action: return NotificationResponseType.selectedNotificationAction; + default: throw ArgumentError("Invalid launch type: $launchType"); + } +} + +NotificationUpdateResult getUpdateResult(int result) { + switch (result) { + case 0: return NotificationUpdateResult.success; + case 1: return NotificationUpdateResult.error; + case 2: return NotificationUpdateResult.notFound; + default: throw ArgumentError("Invalid update result: $result"); + } +} + +extension MapToPairs on Map { + Pointer toPairs(Arena arena) { + final pairs = arena.call(length); + var index = 0; + for (final entry in entries) { + final pair = pairs[index++]; + pair.key = entry.key.toNativeUtf8(allocator: arena); + pair.value = entry.value.toNativeUtf8(allocator: arena); + } + return pairs; + } +} + +List parseActiveNotifications(Pointer array, int length) => [ + for (var index = 0; index < length; index++) + ActiveNotification(id: array[index].id), +]; + +List parsePendingNotifications(Pointer array, int length) => [ + for (var index = 0; index < length; index++) + PendingNotificationRequest(array[index].id, null, null, null), +]; diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart new file mode 100644 index 000000000..b859c7876 --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -0,0 +1,57 @@ +import "package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart"; +import "package:timezone/timezone.dart"; + +import "../details.dart"; + +export "package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart"; +export "package:timezone/timezone.dart"; + +abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatform { + Future initialize( + WindowsInitializationSettings settings, { + DidReceiveNotificationResponseCallback? onNotificationReceived, + }); + + Future showRawXml({ + required int id, + required String xml, + Map bindings = const {}, + }); + + @override + Future show( + int id, + String? title, + String? body, { + String? payload, + WindowsNotificationDetails? details, + }); + + Future zonedSchedule( + int id, + String? title, + String? body, + TZDateTime scheduledDate, + WindowsNotificationDetails? details, { + String? payload, + }); + + /// Updates the progress bar in the notification with the given ID. + /// + /// Note that in order to update [WindowsProgressBar.label], it must + /// not have been set to null when [show] was called. + Future updateProgressBar({ + required int notificationId, + required WindowsProgressBar progressBar, + }) => updateBindings(id: notificationId, bindings: progressBar.data); + + /// Updates any data binding in the given notification. + /// + /// Instead of a text value, you can replace any value in the `` + /// element with `{name}`, and then use this function to update that value + /// by passing `data: {'name': value}`. + Future updateBindings({ + required int id, + required Map bindings, + }); +} diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart new file mode 100644 index 000000000..ac5092789 --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -0,0 +1,138 @@ +import "dart:ffi"; +import "package:ffi/ffi.dart"; + +import "../details.dart"; +import "../ffi/bindings.dart"; +import "../ffi/utils.dart"; + +import "base.dart"; + +void _globalLaunchCallback(Pointer details) { + FlutterLocalNotificationsWindows.instance?._onNotificationReceived(details); +} + +class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { + static FlutterLocalNotificationsWindows? instance; + + late final NotificationsPluginBindings _bindings; + late final Pointer _plugin; + DidReceiveNotificationResponseCallback? userCallback; + + FlutterLocalNotificationsWindows() { + final library = DynamicLibrary.open("flutter_local_notifications_windows.dll"); + _bindings = NotificationsPluginBindings(library); + _plugin = _bindings.createPlugin(); + } + + @override + Future initialize( + WindowsInitializationSettings settings, { + DidReceiveNotificationResponseCallback? onNotificationReceived, + }) async => using((arena) { + if (instance != null) return false; + instance = this; + userCallback = onNotificationReceived; + final appName = settings.appName.toNativeUtf8(allocator: arena); + final aumId = settings.appUserModelId.toNativeUtf8(allocator: arena); + final guid = settings.guid.toNativeUtf8(allocator: arena); + final iconPath = settings.iconPath?.toNativeUtf8(allocator: arena) ?? nullptr; + final callback = NativeCallable.listener(_globalLaunchCallback).nativeFunction; + final result = _bindings.init(_plugin, appName, aumId, guid, iconPath, callback); + return result.toBool(); + }); + + void _onNotificationReceived(Pointer details) { + final data = details.ref.data.toMap(details.ref.dataSize); + final response = NotificationResponse( + notificationResponseType: getResponseType(details.ref.launchType), + payload: details.ref.payload.toDartString(length: details.ref.payloadSize), + actionId: details.ref.payload.toDartString(length: details.ref.payloadSize), + data: data, + ); + userCallback?.call(response); + } + + @override + Future cancel(int id) async => _bindings.cancelNotification(_plugin, id); + + @override + Future cancelAll() async => _bindings.cancelAll(_plugin); + + @override + Future> getActiveNotifications() async => using((arena) { + final length = arena(); + final array = _bindings.getActiveNotifications(_plugin, length); + return parseActiveNotifications(array, length.value); + }); + + @override + Future> pendingNotificationRequests() async => using((arena) { + final length = arena(); + final array = _bindings.getPendingNotifications(_plugin, length); + return parsePendingNotifications(array, length.value); + }); + + @override + Future getNotificationAppLaunchDetails() async { + final details = _bindings.getLaunchDetails(_plugin).ref; + final data = details.data.toMap(details.dataSize); + return NotificationAppLaunchDetails( + details.didLaunch.toBool(), + notificationResponse: NotificationResponse( + notificationResponseType: getResponseType(details.launchType), + payload: details.payload.toDartString(length: details.payloadSize), + actionId: details.payload.toDartString(length: details.payloadSize), + data: data, + ), + ); + } + + @override + Future periodicallyShow(int id, String? title, String? body, RepeatInterval repeatInterval) async { + throw UnsupportedError("Windows devices cannot periodically show notifications"); + } + + @override + Future periodicallyShowWithDuration(int id, String? title, String? body, Duration repeatDurationInterval) async { + throw UnsupportedError("Windows devices cannot periodically show notifications"); + } + + @override + Future show(int id, String? title, String? body, {String? payload, WindowsNotificationDetails? details}) async => using((arena) { + final bindings = { + if (details != null) ...details.bindings, + for (final progressBar in details?.progressBars ?? []) + ...progressBar.data, + }; + final pairs = bindings.toPairs(arena); + final xml = notificationToXml(title: title, body: body, payload: payload, details: details); + _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), pairs, bindings.length); + }); + + @override + Future showRawXml({required int id, required String xml, Map bindings = const {}}) async => using((arena) { + final pairs = bindings.toPairs(arena); + _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), pairs, bindings.length); + }); + + @override + Future zonedSchedule( + int id, + String? title, + String? body, + TZDateTime scheduledDate, + WindowsNotificationDetails? details, { + String? payload, + }) async => using((arena) { + final xml = notificationToXml(title: title, body: body, payload: payload, details: details); + final secondsSinceEpoch = scheduledDate.millisecondsSinceEpoch ~/ 1000; + _bindings.scheduleNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), secondsSinceEpoch); + }); + + @override + Future updateBindings({required int id, required Map bindings}) async => using((arena) { + final pairs = bindings.toPairs(arena); + final result = _bindings.updateNotification(_plugin, id, pairs, bindings.length); + return getUpdateResult(result); + }); +} diff --git a/flutter_local_notifications_windows/lib/src/plugin/stub.dart b/flutter_local_notifications_windows/lib/src/plugin/stub.dart new file mode 100644 index 000000000..439a27bbd --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/plugin/stub.dart @@ -0,0 +1,57 @@ +import "../details.dart"; +import "base.dart"; + +class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { + FlutterLocalNotificationsWindows() { + throw UnimplementedError("This is just the stub implementation. Do not use this"); + } + + @override + Future initialize( + WindowsInitializationSettings settings, { + DidReceiveNotificationResponseCallback? onNotificationReceived, + }) async => false; + + @override + Future cancel(int id) async { } + + @override + Future cancelAll() async { } + + @override + Future> getActiveNotifications() async => []; + + @override + Future getNotificationAppLaunchDetails() async => null; + + @override + Future> pendingNotificationRequests() async => []; + + @override + Future periodicallyShow(int id, String? title, String? body, RepeatInterval repeatInterval) async { } + + @override + Future periodicallyShowWithDuration(int id, String? title, String? body, Duration repeatDurationInterval) async { } + + @override + Future show(int id, String? title, String? body, {String? payload, WindowsNotificationDetails? details}) async { } + + @override + Future showRawXml({required int id, required String xml, Map bindings = const {}}) async { } + + @override + Future zonedSchedule( + int id, + String? title, + String? body, + TZDateTime scheduledDate, + WindowsNotificationDetails? details, { + String? payload, + }) async { } + + @override + Future updateBindings({ + required int id, + required Map bindings, + }) async => NotificationUpdateResult.success; +} diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index 8ab4a199a..228d54dc9 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -8,13 +8,16 @@ environment: flutter: ">=3.0.0" dependencies: + ffi: ^2.1.2 flutter: sdk: flutter + flutter_local_notifications_platform_interface: ^7.2.0 plugin_platform_interface: ^2.0.2 - + timezone: ^0.9.4 + xml: ^6.5.0 + dev_dependencies: - ffi: ^2.1.0 - ffigen: ^12.0.0 + ffigen: ^7.0.0 # using such a low version to avoid Dart 3.0 flutter_test: sdk: flutter very_good_analysis: ^6.0.0 diff --git a/flutter_local_notifications_windows/src/CMakeLists.txt b/flutter_local_notifications_windows/src/CMakeLists.txt index 0e24a88a7..1f7e54677 100644 --- a/flutter_local_notifications_windows/src/CMakeLists.txt +++ b/flutter_local_notifications_windows/src/CMakeLists.txt @@ -3,14 +3,19 @@ # the plugin to fail to compile for some customers of the plugin. cmake_minimum_required(VERSION 3.10) -project(flutter_local_notifications_windows_library VERSION 0.0.1 LANGUAGES C) +project(flutter_local_notifications_windows_library VERSION 1.0.0 LANGUAGES CXX) add_library(flutter_local_notifications_windows SHARED - "flutter_local_notifications_windows.c" + "ffi_api.cpp" + "plugin.cpp" + "registration.cpp" + "utils.cpp" ) +target_compile_features(flutter_local_notifications_windows PRIVATE cxx_std_17) + set_target_properties(flutter_local_notifications_windows PROPERTIES - PUBLIC_HEADER flutter_local_notifications_windows.h + PUBLIC_HEADER ffi_api.h OUTPUT_NAME "flutter_local_notifications_windows" ) diff --git a/flutter_local_notifications_windows/src/ffi_api.cpp b/flutter_local_notifications_windows/src/ffi_api.cpp new file mode 100644 index 000000000..d812c318b --- /dev/null +++ b/flutter_local_notifications_windows/src/ffi_api.cpp @@ -0,0 +1,87 @@ +#pragma once + +#include + +#include "ffi_api.h" + +#include "plugin.hpp" +#include "utils.hpp" + +FFI_PLUGIN_EXPORT NativePlugin* createPlugin() { + return reinterpret_cast(new notifications::NativePlugin()); +} + +FFI_PLUGIN_EXPORT void disposePlugin(NativePlugin* plugin) { + delete reinterpret_cast(plugin); +} + +FFI_PLUGIN_EXPORT int init( + NativePlugin* plugin, + char* appName, + char* aumId, + char* guid, + char* iconPath, + NativeNotificationCallback callback +) { + auto ptr = reinterpret_cast(plugin); + optional icon; + if (iconPath != nullptr) icon = string(iconPath); + auto result = ptr->init(string(appName), string(aumId), string(guid), icon, callback); + return result; +} + +FFI_PLUGIN_EXPORT int showNotification(NativePlugin* plugin, int id, char* xml, Pair* bindings, int bindingsSize) { + auto ptr = reinterpret_cast(plugin); + auto bindingMap = pairsToBindings(bindings, bindingsSize); + auto result = ptr->showNotification(id, string(xml), bindingMap); + return result; +} + +FFI_PLUGIN_EXPORT int scheduleNotification(NativePlugin* plugin, int id, char* xml, int time) { + auto ptr = reinterpret_cast(plugin); + auto result = ptr->scheduleNotification(id, string(xml), time); + return result; +} + +FFI_PLUGIN_EXPORT NativeUpdateResult updateNotification(NativePlugin* plugin, int id, Pair* bindings, int bindingsSize) { + auto ptr = reinterpret_cast(plugin); + auto bindingMap = pairsToBindings(bindings, bindingsSize); + auto result = ptr->updateNotification(id, bindingMap); + return result; +} + +FFI_PLUGIN_EXPORT void cancelAll(NativePlugin* plugin) { + auto ptr = reinterpret_cast(plugin); + ptr->cancelAll(); +} + +FFI_PLUGIN_EXPORT void cancelNotification(NativePlugin* plugin, int id) { + auto ptr = reinterpret_cast(plugin); + ptr->cancelNotification(id); +} + +FFI_PLUGIN_EXPORT NativeDetails* getActiveNotifications(NativePlugin* plugin, int* size) { + auto ptr = reinterpret_cast(plugin); + auto vec = ptr->getActiveNotifications(); + return getDetailsArray(vec, size); +} + +FFI_PLUGIN_EXPORT NativeDetails* getPendingNotifications(NativePlugin* plugin, int* size) { + auto ptr = reinterpret_cast(plugin); + auto vec = ptr->getPendingNotifications(); + return getDetailsArray(vec, size); +} + +FFI_PLUGIN_EXPORT NativeLaunchDetails* getLaunchDetails(NativePlugin* plugin) { + auto ptr = reinterpret_cast(plugin); + auto data = ptr->launchData; + return parseLaunchDetails(data); +} + +FFI_PLUGIN_EXPORT void freeDetailsArray(NativeDetails* ptr) { + delete[] ptr; +} + +FFI_PLUGIN_EXPORT void freePairArray(Pair* ptr) { + delete[] ptr; +} diff --git a/flutter_local_notifications_windows/src/ffi_api.h b/flutter_local_notifications_windows/src/ffi_api.h new file mode 100644 index 000000000..456f970d1 --- /dev/null +++ b/flutter_local_notifications_windows/src/ffi_api.h @@ -0,0 +1,91 @@ +#ifndef FFI_API_H_ +#define FFI_API_H_ + +#if _WIN32 +#include +#else +#include +#include +#endif + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct NativePlugin NativePlugin; + +typedef struct Pair { + const char* key; + const char* value; +} Pair; + +typedef struct { + int id; +} NativeDetails; + +typedef enum { + notification, + action, +} NativeLaunchType; + +typedef struct { + int didLaunch; + NativeLaunchType launchType; + char* payload; + int payloadSize; + Pair* data; + int dataSize; +} NativeLaunchDetails; + +typedef void (*NativeNotificationCallback)(NativeLaunchDetails* details); + +// See: https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.notificationupdateresult +typedef enum { + success = 0, + failed = 1, + notFound = 2, +} NativeUpdateResult; + +FFI_PLUGIN_EXPORT NativePlugin* createPlugin(); +FFI_PLUGIN_EXPORT void disposePlugin(NativePlugin* ptr); + +FFI_PLUGIN_EXPORT int init( + NativePlugin* plugin, + char* appName, + char* aumId, + char* guid, + char* iconPath, + NativeNotificationCallback callback +); + +FFI_PLUGIN_EXPORT int showNotification(NativePlugin* plugin, int id, char* xml, Pair* bindings, int bindingsSize); + +FFI_PLUGIN_EXPORT int scheduleNotification(NativePlugin* plugin, int id, char* xml, int time); + +FFI_PLUGIN_EXPORT NativeUpdateResult updateNotification(NativePlugin* plugin, int id, Pair* bindings, int bindingsSize); + +FFI_PLUGIN_EXPORT void cancelAll(NativePlugin* plugin); + +FFI_PLUGIN_EXPORT void cancelNotification(NativePlugin* plugin, int id); + +FFI_PLUGIN_EXPORT NativeDetails* getActiveNotifications(NativePlugin* plugin, int* size); + +FFI_PLUGIN_EXPORT NativeDetails* getPendingNotifications(NativePlugin* plugin, int* size); + +FFI_PLUGIN_EXPORT NativeLaunchDetails* getLaunchDetails(NativePlugin* plugin); + +FFI_PLUGIN_EXPORT void freeDetailsArray(NativeDetails* ptr); + +FFI_PLUGIN_EXPORT void freePairArray(Pair* ptr); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/flutter_local_notifications_windows/src/flutter_local_notifications_windows.c b/flutter_local_notifications_windows/src/flutter_local_notifications_windows.c deleted file mode 100644 index e685ff5d8..000000000 --- a/flutter_local_notifications_windows/src/flutter_local_notifications_windows.c +++ /dev/null @@ -1,23 +0,0 @@ -#include "flutter_local_notifications_windows.h" - -// A very short-lived native function. -// -// For very short-lived functions, it is fine to call them on the main isolate. -// They will block the Dart execution while running the native function, so -// only do this for native functions which are guaranteed to be short-lived. -FFI_PLUGIN_EXPORT int sum(int a, int b) { return a + b; } - -// A longer-lived native function, which occupies the thread calling it. -// -// Do not call these kind of native functions in the main isolate. They will -// block Dart execution. This will cause dropped frames in Flutter applications. -// Instead, call these native functions on a separate isolate. -FFI_PLUGIN_EXPORT int sum_long_running(int a, int b) { - // Simulate work. -#if _WIN32 - Sleep(5000); -#else - usleep(5000 * 1000); -#endif - return a + b; -} diff --git a/flutter_local_notifications_windows/src/flutter_local_notifications_windows.h b/flutter_local_notifications_windows/src/flutter_local_notifications_windows.h deleted file mode 100644 index 79444598b..000000000 --- a/flutter_local_notifications_windows/src/flutter_local_notifications_windows.h +++ /dev/null @@ -1,30 +0,0 @@ -#include -#include -#include - -#if _WIN32 -#include -#else -#include -#include -#endif - -#if _WIN32 -#define FFI_PLUGIN_EXPORT __declspec(dllexport) -#else -#define FFI_PLUGIN_EXPORT -#endif - -// A very short-lived native function. -// -// For very short-lived functions, it is fine to call them on the main isolate. -// They will block the Dart execution while running the native function, so -// only do this for native functions which are guaranteed to be short-lived. -FFI_PLUGIN_EXPORT int sum(int a, int b); - -// A longer lived native function, which occupies the thread calling it. -// -// Do not call these kind of native functions in the main isolate. They will -// block Dart execution. This will cause dropped frames in Flutter applications. -// Instead, call these native functions on a separate isolate. -FFI_PLUGIN_EXPORT int sum_long_running(int a, int b); diff --git a/flutter_local_notifications_windows/src/plugin.cpp b/flutter_local_notifications_windows/src/plugin.cpp new file mode 100644 index 000000000..a04e2a77b --- /dev/null +++ b/flutter_local_notifications_windows/src/plugin.cpp @@ -0,0 +1,143 @@ +#include // <-- This must be the first Windows header +#include +#include +#include +#include + +#include "plugin.hpp" +#include "registration.hpp" + +using namespace notifications; +using winrt::Windows::Data::Xml::Dom::XmlDocument; + +notifications::NativePlugin* pluginPtr = nullptr; + +void globalHandleLaunchData(LaunchData data) { + if (pluginPtr == nullptr) return; + pluginPtr->handleLaunchData(data); +} + +optional notifications::NativePlugin::checkIdentity() { + if (!IsWindows8OrGreater()) return false; + uint32_t length = 0; + auto error = GetCurrentPackageFullName(&length, nullptr); + if (error == APPMODEL_ERROR_NO_PACKAGE) return false; + else if (error != ERROR_INSUFFICIENT_BUFFER) return std::nullopt; + PWSTR fullName = (PWSTR) malloc(length * sizeof(*fullName)); + if (fullName == nullptr) return std::nullopt; + error = GetCurrentPackageFullName(&length, fullName); + if (error != ERROR_SUCCESS) return std::nullopt; + free(fullName); + return true; +} + +notifications::NativePlugin::NativePlugin() { } +notifications::NativePlugin::~NativePlugin() { } + +void notifications::NativePlugin::handleLaunchData(LaunchData data) { + auto details = parseLaunchDetails(data); + this->launchData = data; + this->callback(details); +} + +bool notifications::NativePlugin::init(string appName, string aumid, string guid, optional iconPath, NativeNotificationCallback callback) { + if (pluginPtr != nullptr) delete pluginPtr; + pluginPtr = this; + this->callback = callback; + this->aumid = winrt::to_hstring(aumid); + const auto didRegister = RegisterApp(aumid, appName, guid, iconPath, globalHandleLaunchData); + if (!didRegister) return false; + const auto identityResult = checkIdentity(); + if (!identityResult.has_value()) return false; + hasIdentity = identityResult.value(); + notifier = hasIdentity + ? ToastNotificationManager::CreateToastNotifier() + : ToastNotificationManager::CreateToastNotifier(this->aumid); + history = ToastNotificationManager::History(); + return true; +} + +bool notifications::NativePlugin::showNotification(int id, string xml, Bindings bindings) { + if (!notifier.has_value()) return false; + XmlDocument doc; + try { doc.LoadXml(winrt::to_hstring(xml)); } + catch (winrt::hresult_error error) { return false; } + ToastNotification notification(doc); + const auto data = dataFromBindings(bindings); + notification.Tag(winrt::to_hstring(id)); + notification.Data(data); + notifier.value().Show(notification); + return true; +} + +bool notifications::NativePlugin::scheduleNotification(const int id, const std::string xml, const time_t time) { + if (!notifier.has_value()) return false; + XmlDocument doc; + try { doc.LoadXml(winrt::to_hstring(xml)); } + catch (winrt::hresult_error error) { return false; } + ScheduledToastNotification notification(doc, winrt::clock::from_time_t(time)); + notification.Tag(winrt::to_hstring(id)); + notifier.value().AddToSchedule(notification); + return true; +} + +void notifications::NativePlugin::cancelAll() { + if (!history.has_value() || !notifier.has_value()) return; + if (hasIdentity) { + history.value().Clear(); + } else { + history.value().Clear(aumid); + } + const auto allScheduled = notifier.value().GetScheduledToastNotifications(); + for (const auto notification : allScheduled) { + notifier.value().RemoveFromSchedule(notification); + } +} + +void notifications::NativePlugin::cancelNotification(int id) { + if (!history.has_value() || !notifier.has_value()) return; + const auto tag = winrt::to_hstring(id); + if (hasIdentity) history.value().Remove(tag); + for (const auto notification : notifier.value().GetScheduledToastNotifications()) { + if (notification.Tag() == tag) { + notifier.value().RemoveFromSchedule(notification); + return; + } + } +} + +NativeUpdateResult notifications::NativePlugin::updateNotification(int id, Bindings bindings) { + if (!notifier.has_value()) return NativeUpdateResult::failed; + const auto tag = winrt::to_hstring(id); + const auto data = dataFromBindings(bindings); + return (NativeUpdateResult) notifier.value().Update(data, tag); +} + +vector notifications::NativePlugin::getActiveNotifications() { + vector result; + if (!history.has_value() || !hasIdentity) return result; + for (const auto notification : history.value().GetHistory()) { + NativeDetails details; + const auto tag = notification.Tag(); + const auto tagStr = winrt::to_string(tag); + const auto tagInt = std::stoi(tagStr); + details.id = tagInt; + result.emplace_back(details); + } + return result; +} + +vector notifications::NativePlugin::getPendingNotifications() { + vector result; + if (!notifier.has_value()) return result; + for (const auto notification : notifier.value().GetScheduledToastNotifications()) { + NativeDetails details; + const auto tag = notification.Tag(); + const auto tagStr = winrt::to_string(tag); + const auto tagInt = std::stoi(tagStr); + details.id = tagInt; + result.emplace_back(details); + } + return result; +} + diff --git a/flutter_local_notifications_windows/src/plugin.hpp b/flutter_local_notifications_windows/src/plugin.hpp new file mode 100644 index 000000000..309b28116 --- /dev/null +++ b/flutter_local_notifications_windows/src/plugin.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include // <-- This must be the first Windows header +#include + +#include "ffi_api.h" +#include "utils.hpp" +#include "registration.hpp" + +namespace notifications { + +using std::optional; +using std::vector; +using namespace winrt::Windows::UI::Notifications; + +class NativePlugin { + private: + bool hasIdentity = false; + winrt::hstring aumid; + optional notifier; + optional history; + NativeNotificationCallback callback; + + /// Checks if this app was installed using an MSIX packager. + /// + /// See: https://learn.microsoft.com/en-us/windows/msix/detect-package-identity. + optional checkIdentity(); + + public: + NativePlugin(); + ~NativePlugin(); + + LaunchData launchData { }; + void handleLaunchData(LaunchData); + + bool init(string appName, string aumid, string guid, optional iconPath, NativeNotificationCallback callback); + bool showNotification(int id, string xml, Bindings bindings); + bool scheduleNotification(int id, string xml, time_t time); + NativeUpdateResult updateNotification(int id, Bindings bindings); + void cancelAll(); + void cancelNotification(int id); + vector getActiveNotifications(); + vector getPendingNotifications(); +}; + +} diff --git a/flutter_local_notifications/windows/registration.cpp b/flutter_local_notifications_windows/src/registration.cpp similarity index 70% rename from flutter_local_notifications/windows/registration.cpp rename to flutter_local_notifications_windows/src/registration.cpp index 71836993a..26ab8fb05 100644 --- a/flutter_local_notifications/windows/registration.cpp +++ b/flutter_local_notifications_windows/src/registration.cpp @@ -1,28 +1,35 @@ -// Huge credit to these StackOverflow answers: -// https://stackoverflow.com/questions/51947833/activation-from-c-winrt-dll -// https://stackoverflow.com/questions/67005337/how-works-notifications-on-windows-registry-no-shortlink - -#include "registration.h" -#include "include/flutter_local_notifications/flutter_local_notifications_plugin.h" -#include "methods.h" +#include +#include +#include +#include #include #include #include #include #include -#include + +#include #include -#include +#include -#include -#include +#include "registration.hpp" + +struct RegistryHandle { + using type = HKEY; + + static void close(type value) noexcept { + WINRT_VERIFY_(ERROR_SUCCESS, RegCloseKey(value)); + } + + static constexpr type invalid() noexcept { return nullptr; } +}; + +using RegistryKey = winrt::handle_type; -/// /// This callback will be called when a notification sent by this plugin is clicked on. -/// struct NotificationActivationCallback : winrt::implements { - std::shared_ptr utils; + LaunchCallback callback; HRESULT __stdcall Activate( LPCWSTR app, @@ -31,24 +38,21 @@ struct NotificationActivationCallback : winrt::implementslaunchData; - *utils->didLaunchWithNotification = true; - (*map)[std::string("notificationResponseType")] = (int) openedWithAction; - (*map)[std::string("payload")] = flutter::EncodableValue(payload); - (*map)[std::string("data")] = flutter::EncodableValue(inputData); - flutter::EncodableMap copy(*map); - utils->channel->InvokeMethod( - Method::DID_RECEIVE_NOTIFICATION_RESPONSE, - std::make_unique(copy), - nullptr - ); + launchData.payload = CW2A(args); + launchData.didLaunch = true; + launchData.launchType = openedWithAction + ? NativeLaunchType::action : NativeLaunchType::notification; + callback(launchData); return S_OK; } catch (...) { @@ -57,12 +61,9 @@ struct NotificationActivationCallback : winrt::implements /// A class factory that creates an instance of NotificationActivationCallback. -/// -struct NotificationActivationCallbackFactory : winrt::implements -{ - std::shared_ptr utils; +struct NotificationActivationCallbackFactory : winrt::implements { + LaunchCallback callback; HRESULT __stdcall CreateInstance( IUnknown* outer, @@ -76,34 +77,14 @@ struct NotificationActivationCallbackFactory : winrt::implements(); - cb.get()->utils = utils; + cb.get()->callback = callback; return cb->QueryInterface(iid, result); } - HRESULT __stdcall LockServer(BOOL) noexcept final { - return S_OK; - } + HRESULT __stdcall LockServer(BOOL) noexcept final { return S_OK; } }; -struct RegistryHandle -{ - using type = HKEY; - - static void close(type value) noexcept { - WINRT_VERIFY_(ERROR_SUCCESS, RegCloseKey(value)); - } - - static constexpr type invalid() noexcept { - return nullptr; - } -}; - -/// -/// A handle to a registry key. -/// -using RegistryKey = winrt::handle_type; - /// /// Updates the Registry to enable notifications. /// @@ -120,8 +101,7 @@ void UpdateRegistry( const std::string& aumid, const std::string& appName, const std::string& guid, - const std::optional& iconPath, - const std::optional& iconBgColor + const std::optional& iconPath ) { std::stringstream ss; ss << "Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications\\Backup\\" << aumid; @@ -213,16 +193,17 @@ void UpdateRegistry( static_cast(v.size() + 1 * sizeof(char)))); } - if (iconBgColor.has_value()) { - const auto v = iconBgColor.value(); - winrt::check_win32(RegSetValueExA( - appInfoKey.get(), - "IconBackgroundColor", - 0, - REG_SZ, - reinterpret_cast(v.c_str()), - static_cast(v.size() + 1 * sizeof(char)))); - } + // TODO: Decide if this is possible/worth it to support + // if (iconBgColor.has_value()) { + // const auto v = iconBgColor.value(); + // winrt::check_win32(RegSetValueExA( + // appInfoKey.get(), + // "IconBackgroundColor", + // 0, + // REG_SZ, + // reinterpret_cast(v.c_str()), + // static_cast(v.size() + 1 * sizeof(char)))); + // } // combine guid to class id ss.clear(); @@ -240,11 +221,9 @@ void UpdateRegistry( static_cast(clsid.size() + 1 * sizeof(char)))); } -/// -/// Register the notificatio activation callback factory +/// Register the notification activation callback factory /// and the guid of the callback. -/// -bool RegisterCallback(const std::string& guid, std::shared_ptr utils) { +bool RegisterCallback(const std::string& guid, LaunchCallback callback) { DWORD registration{}; const auto factory_ref = winrt::make_self(); @@ -256,7 +235,7 @@ bool RegisterCallback(const std::string& guid, std::shared_ptrutils = utils; + factory->callback = callback; winrt::check_hresult(CoRegisterClassObject( rclsid, @@ -268,13 +247,12 @@ bool RegisterCallback(const std::string& guid, std::shared_ptr& iconPath, - const std::optional& iconBgColor, - std::shared_ptr utils + const string& aumid, + const string& appName, + const string& guid, + const optional& iconPath, + LaunchCallback callback ) { - UpdateRegistry(aumid, appName, guid, iconPath, iconBgColor); - return RegisterCallback(guid, utils); + UpdateRegistry(aumid, appName, guid, iconPath); + return RegisterCallback(guid, callback); } diff --git a/flutter_local_notifications_windows/src/registration.hpp b/flutter_local_notifications_windows/src/registration.hpp new file mode 100644 index 000000000..0616eb261 --- /dev/null +++ b/flutter_local_notifications_windows/src/registration.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "ffi_api.h" + +using std::map; +using std::optional; +using std::shared_ptr; +using std::string; + +typedef struct LaunchData { + NativeLaunchType launchType = NativeLaunchType::notification; + map data; + bool didLaunch; + string payload; +} LaunchData; + +using LaunchCallback = std::function; + +bool RegisterApp( + const string& aumid, + const string& appName, + const string& guid, + const optional& iconPath, + LaunchCallback callback +); diff --git a/flutter_local_notifications_windows/src/utils.cpp b/flutter_local_notifications_windows/src/utils.cpp new file mode 100644 index 000000000..361b20fcb --- /dev/null +++ b/flutter_local_notifications_windows/src/utils.cpp @@ -0,0 +1,52 @@ +#include "utils.hpp" + +Bindings pairsToBindings(Pair* pairs, int size) { + Bindings result; + for (int index = 0; index < size; index++) { + const auto pair = pairs[index]; + result.try_emplace(pair.key, pair.value); + } + return result; +} + +Pair* bindingsToPairs(Bindings bindings, int* size) { + *size = (int) bindings.size(); + auto array = new Pair[*size]; + int index = 0; + for (const auto pair : bindings) { + array[index].key = pair.first.c_str(); + array[index].value = pair.second.c_str(); + index++; + } + return array; +} + +NativeDetails* getDetailsArray(vector vec, int* size) { + *size = (int) vec.size(); + auto result = new NativeDetails[vec.size()]; + for (int index = 0; index < vec.size(); index++) { + result[index] = vec.at(index); + } + return result; +} + +NotificationData dataFromBindings(Bindings bindings) { + NotificationData data; + for (const auto pair : bindings) { + const auto key = winrt::to_hstring(pair.first); + const auto value = winrt::to_hstring(pair.second); + data.Values().Insert(key, value); + } + return data; +} + +NativeLaunchDetails* parseLaunchDetails(LaunchData data) { + NativeLaunchDetails* result = new NativeLaunchDetails; + result->didLaunch = data.didLaunch; + result->data = bindingsToPairs(data.data, &result->dataSize); + result->launchType = data.launchType; + result->payload = new char[data.payload.size()]; + result->payloadSize = (int) data.payload.size(); + memcpy(result->payload, data.payload.c_str(), data.payload.size()); + return result; +} \ No newline at end of file diff --git a/flutter_local_notifications_windows/src/utils.hpp b/flutter_local_notifications_windows/src/utils.hpp new file mode 100644 index 000000000..63c74de0c --- /dev/null +++ b/flutter_local_notifications_windows/src/utils.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +#include +#include + +#include "ffi_api.h" +#include "registration.hpp" + +using std::map; +using std::string; +using std::vector; +using winrt::Windows::UI::Notifications::NotificationData; +using Bindings = map; + +Bindings pairsToBindings(Pair* pairs, int size); + +Pair* bindingsToPairs(Bindings bindings, int* size); + +NativeDetails* getDetailsArray(vector vec, int* size); + +NotificationData dataFromBindings(Bindings bindings); + +NativeLaunchDetails* parseLaunchDetails(LaunchData details); From d004ad388a5b5fc2a6510ff241fe5b7db8c95584 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 11 Jul 2024 18:31:32 -0400 Subject: [PATCH 052/112] Reduced C <--> C++ logic, embraced C API --- .../lib/src/details/notification_details.dart | 32 +- .../lib/src/ffi/bindings.dart | 100 ++--- .../lib/src/ffi/utils.dart | 30 +- .../lib/src/plugin/base.dart | 4 + .../lib/src/plugin/ffi.dart | 43 +- .../src/CMakeLists.txt | 1 - .../src/ffi_api.cpp | 172 +++++--- .../src/ffi_api.h | 51 ++- .../src/plugin.cpp | 368 ++++++++++++------ .../src/plugin.hpp | 48 +-- .../src/registration.cpp | 258 ------------ .../src/registration.hpp | 31 -- .../src/utils.cpp | 55 +-- .../src/utils.hpp | 21 +- 14 files changed, 521 insertions(+), 693 deletions(-) delete mode 100644 flutter_local_notifications_windows/src/registration.cpp delete mode 100644 flutter_local_notifications_windows/src/registration.hpp diff --git a/flutter_local_notifications_windows/lib/src/details/notification_details.dart b/flutter_local_notifications_windows/lib/src/details/notification_details.dart index 2c5122c50..c55558a6c 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_details.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_details.dart @@ -47,8 +47,7 @@ extension on DateTime { final offset = timeZoneOffset; final sign = offset.isNegative ? "-" : "+"; final hours = offset.inHours.abs().toString().padLeft(2, "0"); - final minutes = offset.inMinutes.abs().remainder(60) - .toString().padLeft(2, "0"); + final minutes = offset.inMinutes.abs().remainder(60).toString().padLeft(2, "0"); final offsetString = "$sign$hours:$minutes"; // Get first part of properly formatted ISO 8601 date final formattedDate = toIso8601String().split(".").first; @@ -61,7 +60,7 @@ extension on DateTime { /// See: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts class WindowsNotificationDetails { /// Creates a Windows notification from the given options. - WindowsNotificationDetails({ + const WindowsNotificationDetails({ this.actions = const [], this.inputs = const [], this.images = const [], @@ -74,24 +73,7 @@ class WindowsNotificationDetails { this.scenario, this.timestamp, this.subtitle, - }) : rawXml = null { - if (actions.length > 5) { - throw ArgumentError( - "WindowsNotificationDetails can only have up to 5 actions", - ); - } - if (inputs.length > 5) { - throw ArgumentError( - "WindowsNotificationDetails can only have up to 5 inputs", - ); - } - } - - /// The raw XML passed to the Windows API. - /// - /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. - /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). - final String? rawXml; + }); /// A list of at most five action buttons. final List actions; @@ -142,9 +124,11 @@ class WindowsNotificationDetails { /// Builds all relevant XML parts under the root `` element. void toXml(XmlBuilder builder) { - if (rawXml != null) { - builder.xml(rawXml!); - return; + if (actions.length > 5) { + throw ArgumentError("WindowsNotificationDetails can only have up to 5 actions"); + } + if (inputs.length > 5) { + throw ArgumentError("WindowsNotificationDetails can only have up to 5 inputs"); } builder.element("actions", nest: () { for (final input in inputs) { diff --git a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart index 52b8e4991..825417e9e 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart @@ -92,29 +92,23 @@ class NotificationsPluginBindings { ffi.Pointer plugin, int id, ffi.Pointer xml, - ffi.Pointer bindings, - int bindingsSize, + NativeStringMap bindings, ) { return _showNotification( plugin, id, xml, bindings, - bindingsSize, ); } late final _showNotificationPtr = _lookup< ffi.NativeFunction< - ffi.Int Function( - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Pointer, - ffi.Int)>>('showNotification'); + ffi.Int Function(ffi.Pointer, ffi.Int, + ffi.Pointer, NativeStringMap)>>('showNotification'); late final _showNotification = _showNotificationPtr.asFunction< int Function(ffi.Pointer, int, ffi.Pointer, - ffi.Pointer, int)>(); + NativeStringMap)>(); int scheduleNotification( ffi.Pointer plugin, @@ -141,23 +135,21 @@ class NotificationsPluginBindings { int updateNotification( ffi.Pointer plugin, int id, - ffi.Pointer bindings, - int bindingsSize, + NativeStringMap bindings, ) { return _updateNotification( plugin, id, bindings, - bindingsSize, ); } late final _updateNotificationPtr = _lookup< ffi.NativeFunction< ffi.Int32 Function(ffi.Pointer, ffi.Int, - ffi.Pointer, ffi.Int)>>('updateNotification'); + NativeStringMap)>>('updateNotification'); late final _updateNotification = _updateNotificationPtr.asFunction< - int Function(ffi.Pointer, int, ffi.Pointer, int)>(); + int Function(ffi.Pointer, int, NativeStringMap)>(); void cancelAll( ffi.Pointer plugin, @@ -190,7 +182,7 @@ class NotificationsPluginBindings { late final _cancelNotification = _cancelNotificationPtr .asFunction, int)>(); - ffi.Pointer getActiveNotifications( + ffi.Pointer getActiveNotifications( ffi.Pointer plugin, ffi.Pointer size, ) { @@ -202,13 +194,14 @@ class NotificationsPluginBindings { late final _getActiveNotificationsPtr = _lookup< ffi.NativeFunction< - ffi.Pointer Function(ffi.Pointer, + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>>('getActiveNotifications'); late final _getActiveNotifications = _getActiveNotificationsPtr.asFunction< - ffi.Pointer Function( + ffi.Pointer Function( ffi.Pointer, ffi.Pointer)>(); - ffi.Pointer getPendingNotifications( + ffi.Pointer getPendingNotifications( ffi.Pointer plugin, ffi.Pointer size, ) { @@ -220,29 +213,15 @@ class NotificationsPluginBindings { late final _getPendingNotificationsPtr = _lookup< ffi.NativeFunction< - ffi.Pointer Function(ffi.Pointer, + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>>('getPendingNotifications'); late final _getPendingNotifications = _getPendingNotificationsPtr.asFunction< - ffi.Pointer Function( + ffi.Pointer Function( ffi.Pointer, ffi.Pointer)>(); - ffi.Pointer getLaunchDetails( - ffi.Pointer plugin, - ) { - return _getLaunchDetails( - plugin, - ); - } - - late final _getLaunchDetailsPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('getLaunchDetails'); - late final _getLaunchDetails = _getLaunchDetailsPtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); - void freeDetailsArray( - ffi.Pointer ptr, + ffi.Pointer ptr, ) { return _freeDetailsArray( ptr, @@ -250,35 +229,43 @@ class NotificationsPluginBindings { } late final _freeDetailsArrayPtr = _lookup< - ffi.NativeFunction)>>( - 'freeDetailsArray'); + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer)>>('freeDetailsArray'); late final _freeDetailsArray = _freeDetailsArrayPtr - .asFunction)>(); + .asFunction)>(); - void freePairArray( - ffi.Pointer ptr, + void freeLaunchDetails( + NativeLaunchDetails details, ) { - return _freePairArray( - ptr, + return _freeLaunchDetails( + details, ); } - late final _freePairArrayPtr = - _lookup)>>( - 'freePairArray'); - late final _freePairArray = - _freePairArrayPtr.asFunction)>(); + late final _freeLaunchDetailsPtr = + _lookup>( + 'freeLaunchDetails'); + late final _freeLaunchDetails = + _freeLaunchDetailsPtr.asFunction(); } class NativePlugin extends ffi.Opaque {} -class Pair extends ffi.Struct { +class StringMapEntry extends ffi.Struct { external ffi.Pointer key; external ffi.Pointer value; } -class NativeDetails extends ffi.Struct { +class NativeStringMap extends ffi.Struct { + external ffi.Pointer entries; + + @ffi.Int() + external int size; +} + +class NativeNotificationDetails extends ffi.Struct { @ffi.Int() external int id; } @@ -297,13 +284,7 @@ class NativeLaunchDetails extends ffi.Struct { external ffi.Pointer payload; - @ffi.Int() - external int payloadSize; - - external ffi.Pointer data; - - @ffi.Int() - external int dataSize; + external NativeStringMap data; } /// See: https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.notificationupdateresult @@ -314,5 +295,4 @@ abstract class NativeUpdateResult { } typedef NativeNotificationCallback = ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer details)>>; + ffi.NativeFunction>; diff --git a/flutter_local_notifications_windows/lib/src/ffi/utils.dart b/flutter_local_notifications_windows/lib/src/ffi/utils.dart index ffac5b917..649f844e5 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/utils.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/utils.dart @@ -7,12 +7,12 @@ import "package:flutter_local_notifications_windows/src/plugin/base.dart"; import "bindings.dart"; import "../details.dart"; -typedef NativeCallbackType = Void Function(Pointer details); +typedef NativeCallbackType = Void Function(NativeLaunchDetails details); -extension PairUtils on Pointer { - Map toMap(int length) => { - for (var index = 0; index < length; index++) - this[index].key.toDartString(): this[index].value.toDartString(), +extension NativeStringMapUtils on NativeStringMap { + Map toMap() => { + for (var index = 0; index < size; index++) + entries[index].key.toDartString(): entries[index].value.toDartString(), }; } @@ -37,25 +37,27 @@ NotificationUpdateResult getUpdateResult(int result) { } } -extension MapToPairs on Map { - Pointer toPairs(Arena arena) { - final pairs = arena.call(length); +extension MapToNativeMap on Map { + NativeStringMap toNativeMap(Arena arena) { + final pointer = arena(); + pointer.ref.size = length; + pointer.ref.entries = arena(length); var index = 0; for (final entry in entries) { - final pair = pairs[index++]; - pair.key = entry.key.toNativeUtf8(allocator: arena); - pair.value = entry.value.toNativeUtf8(allocator: arena); + pointer.ref.entries[index].key = entry.key.toNativeUtf8(allocator: arena); + pointer.ref.entries[index].value = entry.value.toNativeUtf8(allocator: arena); + index++; } - return pairs; + return pointer.ref; } } -List parseActiveNotifications(Pointer array, int length) => [ +List parseActiveNotifications(Pointer array, int length) => [ for (var index = 0; index < length; index++) ActiveNotification(id: array[index].id), ]; -List parsePendingNotifications(Pointer array, int length) => [ +List parsePendingNotifications(Pointer array, int length) => [ for (var index = 0; index < length; index++) PendingNotificationRequest(array[index].id, null, null, null), ]; diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart index b859c7876..e081d957f 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/base.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -12,6 +12,10 @@ abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatfor DidReceiveNotificationResponseCallback? onNotificationReceived, }); + /// The raw XML passed to the Windows API. + /// + /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. + /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). Future showRawXml({ required int id, required String xml, diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index ac5092789..ff3cb5300 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -7,7 +7,7 @@ import "../ffi/utils.dart"; import "base.dart"; -void _globalLaunchCallback(Pointer details) { +void _globalLaunchCallback(NativeLaunchDetails details) { FlutterLocalNotificationsWindows.instance?._onNotificationReceived(details); } @@ -16,6 +16,8 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { late final NotificationsPluginBindings _bindings; late final Pointer _plugin; + + NativeLaunchDetails? _details; DidReceiveNotificationResponseCallback? userCallback; FlutterLocalNotificationsWindows() { @@ -41,12 +43,14 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { return result.toBool(); }); - void _onNotificationReceived(Pointer details) { - final data = details.ref.data.toMap(details.ref.dataSize); + void _onNotificationReceived(NativeLaunchDetails details) { + if (_details != null) _bindings.freeLaunchDetails(_details!); + _details = details; + final data = details.data.toMap(); final response = NotificationResponse( - notificationResponseType: getResponseType(details.ref.launchType), - payload: details.ref.payload.toDartString(length: details.ref.payloadSize), - actionId: details.ref.payload.toDartString(length: details.ref.payloadSize), + notificationResponseType: getResponseType(details.launchType), + payload: details.payload.toDartString(), + actionId: details.payload.toDartString(), data: data, ); userCallback?.call(response); @@ -62,26 +66,31 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { Future> getActiveNotifications() async => using((arena) { final length = arena(); final array = _bindings.getActiveNotifications(_plugin, length); - return parseActiveNotifications(array, length.value); + final result = parseActiveNotifications(array, length.value); + _bindings.freeDetailsArray(array); + return result; }); @override Future> pendingNotificationRequests() async => using((arena) { final length = arena(); final array = _bindings.getPendingNotifications(_plugin, length); - return parsePendingNotifications(array, length.value); + final result = parsePendingNotifications(array, length.value); + _bindings.freeDetailsArray(array); + return result; }); @override Future getNotificationAppLaunchDetails() async { - final details = _bindings.getLaunchDetails(_plugin).ref; - final data = details.data.toMap(details.dataSize); + final details = _details; + if (details == null) return null; + final data = details.data.toMap(); return NotificationAppLaunchDetails( details.didLaunch.toBool(), notificationResponse: NotificationResponse( notificationResponseType: getResponseType(details.launchType), - payload: details.payload.toDartString(length: details.payloadSize), - actionId: details.payload.toDartString(length: details.payloadSize), + payload: details.payload.toDartString(), + actionId: details.payload.toDartString(), data: data, ), ); @@ -104,15 +113,14 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { for (final progressBar in details?.progressBars ?? []) ...progressBar.data, }; - final pairs = bindings.toPairs(arena); + final nativeMap = bindings.toNativeMap(arena); final xml = notificationToXml(title: title, body: body, payload: payload, details: details); - _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), pairs, bindings.length); + _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), nativeMap); }); @override Future showRawXml({required int id, required String xml, Map bindings = const {}}) async => using((arena) { - final pairs = bindings.toPairs(arena); - _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), pairs, bindings.length); + _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), bindings.toNativeMap(arena)); }); @override @@ -131,8 +139,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { @override Future updateBindings({required int id, required Map bindings}) async => using((arena) { - final pairs = bindings.toPairs(arena); - final result = _bindings.updateNotification(_plugin, id, pairs, bindings.length); + final result = _bindings.updateNotification(_plugin, id, bindings.toNativeMap(arena)); return getUpdateResult(result); }); } diff --git a/flutter_local_notifications_windows/src/CMakeLists.txt b/flutter_local_notifications_windows/src/CMakeLists.txt index 1f7e54677..5a95e858c 100644 --- a/flutter_local_notifications_windows/src/CMakeLists.txt +++ b/flutter_local_notifications_windows/src/CMakeLists.txt @@ -8,7 +8,6 @@ project(flutter_local_notifications_windows_library VERSION 1.0.0 LANGUAGES CXX) add_library(flutter_local_notifications_windows SHARED "ffi_api.cpp" "plugin.cpp" - "registration.cpp" "utils.cpp" ) diff --git a/flutter_local_notifications_windows/src/ffi_api.cpp b/flutter_local_notifications_windows/src/ffi_api.cpp index d812c318b..c750d7119 100644 --- a/flutter_local_notifications_windows/src/ffi_api.cpp +++ b/flutter_local_notifications_windows/src/ffi_api.cpp @@ -1,87 +1,145 @@ -#pragma once - -#include +#include // <-- This must be the first Windows header +#include +#include #include "ffi_api.h" - #include "plugin.hpp" #include "utils.hpp" -FFI_PLUGIN_EXPORT NativePlugin* createPlugin() { - return reinterpret_cast(new notifications::NativePlugin()); -} +using winrt::Windows::Data::Xml::Dom::XmlDocument; -FFI_PLUGIN_EXPORT void disposePlugin(NativePlugin* plugin) { - delete reinterpret_cast(plugin); +NativePlugin* createPlugin() { + return reinterpret_cast(new WinRTPlugin()); } -FFI_PLUGIN_EXPORT int init( - NativePlugin* plugin, - char* appName, - char* aumId, - char* guid, - char* iconPath, - NativeNotificationCallback callback -) { - auto ptr = reinterpret_cast(plugin); - optional icon; - if (iconPath != nullptr) icon = string(iconPath); - auto result = ptr->init(string(appName), string(aumId), string(guid), icon, callback); - return result; +void disposePlugin(NativePlugin* plugin) { + delete reinterpret_cast(plugin); } -FFI_PLUGIN_EXPORT int showNotification(NativePlugin* plugin, int id, char* xml, Pair* bindings, int bindingsSize) { - auto ptr = reinterpret_cast(plugin); - auto bindingMap = pairsToBindings(bindings, bindingsSize); - auto result = ptr->showNotification(id, string(xml), bindingMap); - return result; +int init(NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, NativeNotificationCallback callback) { + const auto ptr = reinterpret_cast(plugin); + // TODO: Register the callback here + string icon; + if (iconPath != nullptr) icon = string(iconPath); + const auto didRegister = ptr->registerApp(aumId, appName, guid, icon, callback); + if (!didRegister) return false; + const auto identity = ptr->checkIdentity(); + if (!identity.has_value()) return false; + ptr->hasIdentity = identity.value(); + ptr->aumid = winrt::to_hstring(aumId); + ptr->notifier = ptr->hasIdentity + ? ToastNotificationManager::CreateToastNotifier() + : ToastNotificationManager::CreateToastNotifier(ptr->aumid); + ptr->history = ToastNotificationManager::History(); + ptr->isReady = true; + return true; } -FFI_PLUGIN_EXPORT int scheduleNotification(NativePlugin* plugin, int id, char* xml, int time) { - auto ptr = reinterpret_cast(plugin); - auto result = ptr->scheduleNotification(id, string(xml), time); - return result; +int showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bindings) { + const auto ptr = reinterpret_cast(plugin); + if (!ptr->isReady) return false; + XmlDocument doc; + try { doc.LoadXml(winrt::to_hstring(xml)); } + catch (winrt::hresult_error error) { return false; } + ToastNotification notification(doc); + const auto data = dataFromMap(bindings); + notification.Tag(winrt::to_hstring(id)); + notification.Data(data); + ptr->notifier.value().Show(notification); + return true; } -FFI_PLUGIN_EXPORT NativeUpdateResult updateNotification(NativePlugin* plugin, int id, Pair* bindings, int bindingsSize) { - auto ptr = reinterpret_cast(plugin); - auto bindingMap = pairsToBindings(bindings, bindingsSize); - auto result = ptr->updateNotification(id, bindingMap); - return result; +int scheduleNotification(NativePlugin* plugin, int id, char* xml, int time) { + const auto ptr = reinterpret_cast(plugin); + if (!ptr->isReady) return false; + XmlDocument doc; + try { doc.LoadXml(winrt::to_hstring(xml)); } + catch (winrt::hresult_error error) { return false; } + ScheduledToastNotification notification(doc, winrt::clock::from_time_t(time)); + notification.Tag(winrt::to_hstring(id)); + ptr->notifier.value().AddToSchedule(notification); + return true; } -FFI_PLUGIN_EXPORT void cancelAll(NativePlugin* plugin) { - auto ptr = reinterpret_cast(plugin); - ptr->cancelAll(); +NativeUpdateResult updateNotification(NativePlugin* plugin, int id, NativeStringMap bindings) { + const auto ptr = reinterpret_cast(plugin); + if (!ptr->isReady) return NativeUpdateResult::failed; + const auto tag = winrt::to_hstring(id); + const auto data = dataFromMap(bindings); + const auto result = ptr->notifier.value().Update(data, tag); + return (NativeUpdateResult) result; } -FFI_PLUGIN_EXPORT void cancelNotification(NativePlugin* plugin, int id) { - auto ptr = reinterpret_cast(plugin); - ptr->cancelNotification(id); +void cancelAll(NativePlugin* plugin) { + const auto ptr = reinterpret_cast(plugin); + if (!ptr->isReady) return; + if (ptr->hasIdentity) { + ptr->history.value().Clear(); + } else { + ptr->history.value().Clear(ptr->aumid); + } + for (const auto notification : ptr->notifier.value().GetScheduledToastNotifications()) { + ptr->notifier.value().RemoveFromSchedule(notification); + } } -FFI_PLUGIN_EXPORT NativeDetails* getActiveNotifications(NativePlugin* plugin, int* size) { - auto ptr = reinterpret_cast(plugin); - auto vec = ptr->getActiveNotifications(); - return getDetailsArray(vec, size); +void cancelNotification(NativePlugin* plugin, int id) { + const auto ptr = reinterpret_cast(plugin); + if (!ptr->isReady) return; + const auto tag = winrt::to_hstring(id); + if (ptr->hasIdentity) ptr->history.value().Remove(tag); + for (const auto notification : ptr->notifier.value().GetScheduledToastNotifications()) { + if (notification.Tag() == tag) { + ptr->notifier.value().RemoveFromSchedule(notification); + return; + } + } } -FFI_PLUGIN_EXPORT NativeDetails* getPendingNotifications(NativePlugin* plugin, int* size) { - auto ptr = reinterpret_cast(plugin); - auto vec = ptr->getPendingNotifications(); - return getDetailsArray(vec, size); +NativeNotificationDetails* getActiveNotifications(NativePlugin* plugin, int* size) { + // TODO: Get more details here + const auto ptr = reinterpret_cast(plugin); + if (!ptr->isReady || !ptr->hasIdentity) { *size = 0; return nullptr; } + const auto active = ptr->history.value().GetHistory(); + *size = active.Size(); + const auto result = new NativeNotificationDetails[*size]; + int index = 0; + for (const auto notification : active) { + const auto tag = notification.Tag(); + const auto tagStr = winrt::to_string(tag); + const auto tagInt = std::stoi(tagStr); + result[index++].id = tagInt; + } + return result; } -FFI_PLUGIN_EXPORT NativeLaunchDetails* getLaunchDetails(NativePlugin* plugin) { - auto ptr = reinterpret_cast(plugin); - auto data = ptr->launchData; - return parseLaunchDetails(data); +NativeNotificationDetails* getPendingNotifications(NativePlugin* plugin, int* size) { + // TODO: Get more details here + const auto ptr = reinterpret_cast(plugin); + if (!ptr->isReady) { *size = 0; return nullptr; } + const auto pending = ptr->notifier.value().GetScheduledToastNotifications(); + *size = pending.Size(); + const auto result = new NativeNotificationDetails[*size]; + int index = 0; + for (const auto notification : pending) { + const auto tag = notification.Tag(); + const auto tagStr = winrt::to_string(tag); + const auto tagInt = std::stoi(tagStr); + result[index++].id = tagInt; + } + return result; } -FFI_PLUGIN_EXPORT void freeDetailsArray(NativeDetails* ptr) { +void freeDetailsArray(NativeNotificationDetails* ptr) { delete[] ptr; } -FFI_PLUGIN_EXPORT void freePairArray(Pair* ptr) { - delete[] ptr; +void freeLaunchDetails(NativeLaunchDetails details) { + if (details.payload != nullptr) delete[] details.payload; + for (int index = 0; index < details.data.size; index++) { + const auto pair = details.data.entries[index]; + delete pair.key; + delete pair.value; + } + if (details.data.entries != nullptr) delete[] details.data.entries; } diff --git a/flutter_local_notifications_windows/src/ffi_api.h b/flutter_local_notifications_windows/src/ffi_api.h index 456f970d1..c22263c82 100644 --- a/flutter_local_notifications_windows/src/ffi_api.h +++ b/flutter_local_notifications_windows/src/ffi_api.h @@ -20,69 +20,64 @@ extern "C" { typedef struct NativePlugin NativePlugin; -typedef struct Pair { +typedef struct StringMapEntry { const char* key; const char* value; -} Pair; +} StringMapEntry; -typedef struct { +typedef struct NativeStringMap { + const StringMapEntry* entries; + int size; +} NativeStringMap; + +typedef struct NativeNotificationDetails { int id; -} NativeDetails; +} NativeNotificationDetails; -typedef enum { +typedef enum NativeLaunchType { notification, action, } NativeLaunchType; -typedef struct { +typedef struct NativeLaunchDetails { int didLaunch; NativeLaunchType launchType; - char* payload; - int payloadSize; - Pair* data; - int dataSize; + const char* payload; + NativeStringMap data; } NativeLaunchDetails; -typedef void (*NativeNotificationCallback)(NativeLaunchDetails* details); +typedef void (*NativeNotificationCallback)(NativeLaunchDetails details); // See: https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.notificationupdateresult -typedef enum { +typedef enum NativeUpdateResult { success = 0, failed = 1, notFound = 2, } NativeUpdateResult; FFI_PLUGIN_EXPORT NativePlugin* createPlugin(); + FFI_PLUGIN_EXPORT void disposePlugin(NativePlugin* ptr); -FFI_PLUGIN_EXPORT int init( - NativePlugin* plugin, - char* appName, - char* aumId, - char* guid, - char* iconPath, - NativeNotificationCallback callback -); +FFI_PLUGIN_EXPORT int init(NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, NativeNotificationCallback callback); -FFI_PLUGIN_EXPORT int showNotification(NativePlugin* plugin, int id, char* xml, Pair* bindings, int bindingsSize); +FFI_PLUGIN_EXPORT int showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bindings); FFI_PLUGIN_EXPORT int scheduleNotification(NativePlugin* plugin, int id, char* xml, int time); -FFI_PLUGIN_EXPORT NativeUpdateResult updateNotification(NativePlugin* plugin, int id, Pair* bindings, int bindingsSize); +FFI_PLUGIN_EXPORT NativeUpdateResult updateNotification(NativePlugin* plugin, int id, NativeStringMap bindings); FFI_PLUGIN_EXPORT void cancelAll(NativePlugin* plugin); FFI_PLUGIN_EXPORT void cancelNotification(NativePlugin* plugin, int id); -FFI_PLUGIN_EXPORT NativeDetails* getActiveNotifications(NativePlugin* plugin, int* size); - -FFI_PLUGIN_EXPORT NativeDetails* getPendingNotifications(NativePlugin* plugin, int* size); +FFI_PLUGIN_EXPORT NativeNotificationDetails* getActiveNotifications(NativePlugin* plugin, int* size); -FFI_PLUGIN_EXPORT NativeLaunchDetails* getLaunchDetails(NativePlugin* plugin); +FFI_PLUGIN_EXPORT NativeNotificationDetails* getPendingNotifications(NativePlugin* plugin, int* size); -FFI_PLUGIN_EXPORT void freeDetailsArray(NativeDetails* ptr); +FFI_PLUGIN_EXPORT void freeDetailsArray(NativeNotificationDetails* ptr); -FFI_PLUGIN_EXPORT void freePairArray(Pair* ptr); +FFI_PLUGIN_EXPORT void freeLaunchDetails(NativeLaunchDetails details); #ifdef __cplusplus } diff --git a/flutter_local_notifications_windows/src/plugin.cpp b/flutter_local_notifications_windows/src/plugin.cpp index a04e2a77b..755bec985 100644 --- a/flutter_local_notifications_windows/src/plugin.cpp +++ b/flutter_local_notifications_windows/src/plugin.cpp @@ -1,143 +1,271 @@ +#include + #include // <-- This must be the first Windows header #include +#include +#include #include -#include -#include #include "plugin.hpp" -#include "registration.hpp" +#include "utils.hpp" -using namespace notifications; -using winrt::Windows::Data::Xml::Dom::XmlDocument; +struct RegistryHandle { + using type = HKEY; -notifications::NativePlugin* pluginPtr = nullptr; + static void close(type value) noexcept { + WINRT_VERIFY_(ERROR_SUCCESS, RegCloseKey(value)); + } -void globalHandleLaunchData(LaunchData data) { - if (pluginPtr == nullptr) return; - pluginPtr->handleLaunchData(data); -} + static constexpr type invalid() noexcept { return nullptr; } +}; -optional notifications::NativePlugin::checkIdentity() { - if (!IsWindows8OrGreater()) return false; - uint32_t length = 0; - auto error = GetCurrentPackageFullName(&length, nullptr); - if (error == APPMODEL_ERROR_NO_PACKAGE) return false; - else if (error != ERROR_INSUFFICIENT_BUFFER) return std::nullopt; - PWSTR fullName = (PWSTR) malloc(length * sizeof(*fullName)); - if (fullName == nullptr) return std::nullopt; - error = GetCurrentPackageFullName(&length, fullName); - if (error != ERROR_SUCCESS) return std::nullopt; - free(fullName); - return true; -} +using RegistryKey = winrt::handle_type; -notifications::NativePlugin::NativePlugin() { } -notifications::NativePlugin::~NativePlugin() { } +/// This callback will be called when a notification sent by this plugin is clicked on. +struct NotificationActivationCallback : winrt::implements { + NativeNotificationCallback callback; -void notifications::NativePlugin::handleLaunchData(LaunchData data) { - auto details = parseLaunchDetails(data); - this->launchData = data; - this->callback(details); -} + HRESULT __stdcall Activate( + LPCWSTR app, + LPCWSTR args, + NOTIFICATION_USER_INPUT_DATA const* data, + ULONG count) noexcept final + { + try { + // Fill the data map + vector entries; + for (ULONG i = 0; i < count; i++) { + auto item = data[i]; + const std::string key = CW2A(item.Key); + const std::string value = CW2A(item.Value); + const auto pair = StringMapEntry { toNativeString(key), toNativeString(value) }; + entries.push_back(pair); + } -bool notifications::NativePlugin::init(string appName, string aumid, string guid, optional iconPath, NativeNotificationCallback callback) { - if (pluginPtr != nullptr) delete pluginPtr; - pluginPtr = this; - this->callback = callback; - this->aumid = winrt::to_hstring(aumid); - const auto didRegister = RegisterApp(aumid, appName, guid, iconPath, globalHandleLaunchData); - if (!didRegister) return false; - const auto identityResult = checkIdentity(); - if (!identityResult.has_value()) return false; - hasIdentity = identityResult.value(); - notifier = hasIdentity - ? ToastNotificationManager::CreateToastNotifier() - : ToastNotificationManager::CreateToastNotifier(this->aumid); - history = ToastNotificationManager::History(); - return true; -} + const auto openedWithAction = args != nullptr; + const auto payload = string(CW2A(args)); + const auto launchType = openedWithAction + ? NativeLaunchType::action : NativeLaunchType::notification; + NativeLaunchDetails launchDetails; + launchDetails.didLaunch = true; + launchDetails.launchType = launchType; + launchDetails.payload = toNativeString(payload); + launchDetails.data = toNativeMap(entries); + callback(launchDetails); + return S_OK; + } + catch (...) { + return winrt::to_hresult(); + } + } +}; -bool notifications::NativePlugin::showNotification(int id, string xml, Bindings bindings) { - if (!notifier.has_value()) return false; - XmlDocument doc; - try { doc.LoadXml(winrt::to_hstring(xml)); } - catch (winrt::hresult_error error) { return false; } - ToastNotification notification(doc); - const auto data = dataFromBindings(bindings); - notification.Tag(winrt::to_hstring(id)); - notification.Data(data); - notifier.value().Show(notification); - return true; -} +/// A class factory that creates an instance of NotificationActivationCallback. +struct NotificationActivationCallbackFactory : winrt::implements { + NativeNotificationCallback callback; -bool notifications::NativePlugin::scheduleNotification(const int id, const std::string xml, const time_t time) { - if (!notifier.has_value()) return false; - XmlDocument doc; - try { doc.LoadXml(winrt::to_hstring(xml)); } - catch (winrt::hresult_error error) { return false; } - ScheduledToastNotification notification(doc, winrt::clock::from_time_t(time)); - notification.Tag(winrt::to_hstring(id)); - notifier.value().AddToSchedule(notification); - return true; -} + HRESULT __stdcall CreateInstance( + IUnknown* outer, + GUID const& iid, + void** result) noexcept final + { + *result = nullptr; -void notifications::NativePlugin::cancelAll() { - if (!history.has_value() || !notifier.has_value()) return; - if (hasIdentity) { - history.value().Clear(); - } else { - history.value().Clear(aumid); - } - const auto allScheduled = notifier.value().GetScheduledToastNotifications(); - for (const auto notification : allScheduled) { - notifier.value().RemoveFromSchedule(notification); - } -} + if (outer) { + return CLASS_E_NOAGGREGATION; + } -void notifications::NativePlugin::cancelNotification(int id) { - if (!history.has_value() || !notifier.has_value()) return; - const auto tag = winrt::to_hstring(id); - if (hasIdentity) history.value().Remove(tag); - for (const auto notification : notifier.value().GetScheduledToastNotifications()) { - if (notification.Tag() == tag) { - notifier.value().RemoveFromSchedule(notification); - return; - } - } -} + const auto cb = winrt::make_self(); + cb.get()->callback = callback; + + return cb->QueryInterface(iid, result); + } + + HRESULT __stdcall LockServer(BOOL) noexcept final { return S_OK; } +}; + +/// +/// Updates the Registry to enable notifications. +/// +/// Related resources: +///
    +///
  • https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast-other-apps
  • +///
+///
+/// The app user model ID of the app. Provided during initialization of the plugin. +/// The display name of the app. The name will be shown on the notification toasts. +/// An optional path to the icon of the app. The icon will be shown on the notification toasts +/// An optional string that specifies the background color of the icon, in the format of AARRGGBB. +void UpdateRegistry( + const std::string& aumid, + const std::string& appName, + const std::string& guid, + const std::optional& iconPath +) { + std::stringstream ss; + ss << "Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications\\Backup\\" << aumid; + const auto notifSettingsKeyPath = ss.str(); + RegistryKey key; -NativeUpdateResult notifications::NativePlugin::updateNotification(int id, Bindings bindings) { - if (!notifier.has_value()) return NativeUpdateResult::failed; - const auto tag = winrt::to_hstring(id); - const auto data = dataFromBindings(bindings); - return (NativeUpdateResult) notifier.value().Update(data, tag); + // create registry key + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\PushNotifications\Backup + winrt::check_win32(RegCreateKeyExA( + HKEY_CURRENT_USER, + notifSettingsKeyPath.c_str(), + 0, + nullptr, + 0, + KEY_WRITE, + nullptr, + key.put(), + nullptr)); + + // put the following key values under the key + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\PushNotifications\Backup\ + // + // appType = app:desktop + // Setting = s:banner,s:toast,s:audio,c:toast,c:ringing + // wnsId = NonImmersivePackage + + const std::string appType = "app:desktop"; + const std::string setting = "s:banner,s:toast,s:audio,c:toast,c:ringing"; + const std::string wnsId = "NonImmersivePackage"; + winrt::check_win32(RegSetValueExA( + key.get(), + "appType", + 0, + REG_SZ, + reinterpret_cast(appType.c_str()), + static_cast(appType.size() + 1 * sizeof(char)))); + winrt::check_win32(RegSetValueExA( + key.get(), + "Setting", + 0, + REG_SZ, + reinterpret_cast(setting.c_str()), + static_cast(setting.size() + 1 * sizeof(char)))); + winrt::check_win32(RegSetValueExA( + key.get(), + "wnsId", + 0, + REG_SZ, + reinterpret_cast(wnsId.c_str()), + static_cast(wnsId.size() + 1 * sizeof(char)))); + + // now, we register app info to the Registry. + + ss.clear(); + ss.str(std::string()); + ss << "Software\\Classes\\AppUserModelId\\" << aumid; + const auto appInfoKeyPath = ss.str(); + RegistryKey appInfoKey; + + // create registry key + // HKEY_CURRENT_USER\Software\Classes\AppUserModelId\ + winrt::check_win32(RegCreateKeyExA( + HKEY_CURRENT_USER, + appInfoKeyPath.c_str(), + 0, + nullptr, + 0, + KEY_WRITE, + nullptr, + appInfoKey.put(), + nullptr)); + + winrt::check_win32(RegSetValueExA( + appInfoKey.get(), + "DisplayName", + 0, + REG_SZ, + reinterpret_cast(appName.c_str()), + static_cast(appName.size() + 1 * sizeof(char)))); + + if (iconPath.has_value()) { + const auto v = iconPath.value(); + winrt::check_win32(RegSetValueExA( + appInfoKey.get(), + "IconUri", + 0, + REG_SZ, + reinterpret_cast(v.c_str()), + static_cast(v.size() + 1 * sizeof(char)))); + } + + // TODO: Decide if this is possible/worth it to support + // if (iconBgColor.has_value()) { + // const auto v = iconBgColor.value(); + // winrt::check_win32(RegSetValueExA( + // appInfoKey.get(), + // "IconBackgroundColor", + // 0, + // REG_SZ, + // reinterpret_cast(v.c_str()), + // static_cast(v.size() + 1 * sizeof(char)))); + // } + + // combine guid to class id + ss.clear(); + ss.str(std::string()); + ss << '{' << guid << '}'; + const auto clsid = ss.str(); + + // register the guid of the notification activation callback + winrt::check_win32(RegSetValueExA( + appInfoKey.get(), + "CustomActivator", + 0, + REG_SZ, + reinterpret_cast(clsid.c_str()), + static_cast(clsid.size() + 1 * sizeof(char)))); } -vector notifications::NativePlugin::getActiveNotifications() { - vector result; - if (!history.has_value() || !hasIdentity) return result; - for (const auto notification : history.value().GetHistory()) { - NativeDetails details; - const auto tag = notification.Tag(); - const auto tagStr = winrt::to_string(tag); - const auto tagInt = std::stoi(tagStr); - details.id = tagInt; - result.emplace_back(details); - } - return result; +/// Register the notification activation callback factory +/// and the guid of the callback. +bool RegisterCallback(const std::string& guid, NativeNotificationCallback callback) { + DWORD registration{}; + + const auto factory_ref = winrt::make_self(); + const auto factory = factory_ref.get(); + + // The WinRT GUID constructor terminates the app if there's an invalid GUID, so check it here first. + if (guid.size() != 36 || guid[8] != '-' || guid[13] != '-' || guid[18] != '-' || guid[23] != '-') { + throw std::invalid_argument("Invalid GUID"); + } + + winrt::guid rclsid(guid); + factory->callback = callback; + + winrt::check_hresult(CoRegisterClassObject( + rclsid, + factory, + CLSCTX_LOCAL_SERVER, + REGCLS_MULTIPLEUSE, + ®istration)); + return true; } -vector notifications::NativePlugin::getPendingNotifications() { - vector result; - if (!notifier.has_value()) return result; - for (const auto notification : notifier.value().GetScheduledToastNotifications()) { - NativeDetails details; - const auto tag = notification.Tag(); - const auto tagStr = winrt::to_string(tag); - const auto tagInt = std::stoi(tagStr); - details.id = tagInt; - result.emplace_back(details); - } - return result; +bool WinRTPlugin::registerApp( + const string& aumid, + const string& appName, + const string& guid, + const optional& iconPath, + NativeNotificationCallback callback +) { + UpdateRegistry(aumid, appName, guid, iconPath); + return RegisterCallback(guid, callback); } +std::optional WinRTPlugin::checkIdentity() { + if (!IsWindows8OrGreater()) return false; + uint32_t length = 0; + auto error = GetCurrentPackageFullName(&length, nullptr); + if (error == APPMODEL_ERROR_NO_PACKAGE) return false; + else if (error != ERROR_INSUFFICIENT_BUFFER) return std::nullopt; + PWSTR fullName = (PWSTR) malloc(length * sizeof(*fullName)); + if (fullName == nullptr) return std::nullopt; + error = GetCurrentPackageFullName(&length, fullName); + if (error != ERROR_SUCCESS) return std::nullopt; + free(fullName); + return true; +} diff --git a/flutter_local_notifications_windows/src/plugin.hpp b/flutter_local_notifications_windows/src/plugin.hpp index 309b28116..2e8f79946 100644 --- a/flutter_local_notifications_windows/src/plugin.hpp +++ b/flutter_local_notifications_windows/src/plugin.hpp @@ -1,46 +1,38 @@ #pragma once +#include +#include + #include // <-- This must be the first Windows header #include #include "ffi_api.h" -#include "utils.hpp" -#include "registration.hpp" - -namespace notifications { using std::optional; -using std::vector; +using std::string; using namespace winrt::Windows::UI::Notifications; -class NativePlugin { - private: +class WinRTPlugin { + public: bool hasIdentity = false; + bool isReady = false; winrt::hstring aumid; optional notifier; optional history; + NativeLaunchDetails launchData; NativeNotificationCallback callback; - /// Checks if this app was installed using an MSIX packager. - /// - /// See: https://learn.microsoft.com/en-us/windows/msix/detect-package-identity. - optional checkIdentity(); + WinRTPlugin() { } + ~WinRTPlugin() { freeLaunchDetails(launchData); } - public: - NativePlugin(); - ~NativePlugin(); - - LaunchData launchData { }; - void handleLaunchData(LaunchData); - - bool init(string appName, string aumid, string guid, optional iconPath, NativeNotificationCallback callback); - bool showNotification(int id, string xml, Bindings bindings); - bool scheduleNotification(int id, string xml, time_t time); - NativeUpdateResult updateNotification(int id, Bindings bindings); - void cancelAll(); - void cancelNotification(int id); - vector getActiveNotifications(); - vector getPendingNotifications(); -}; + // void onLaunch(NativeLaunchDetails details); -} + std::optional checkIdentity(); + bool registerApp( + const string& aumid, + const string& appName, + const string& guid, + const optional& iconPath, + NativeNotificationCallback callback + ); +}; diff --git a/flutter_local_notifications_windows/src/registration.cpp b/flutter_local_notifications_windows/src/registration.cpp deleted file mode 100644 index 26ab8fb05..000000000 --- a/flutter_local_notifications_windows/src/registration.cpp +++ /dev/null @@ -1,258 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "registration.hpp" - -struct RegistryHandle { - using type = HKEY; - - static void close(type value) noexcept { - WINRT_VERIFY_(ERROR_SUCCESS, RegCloseKey(value)); - } - - static constexpr type invalid() noexcept { return nullptr; } -}; - -using RegistryKey = winrt::handle_type; - -/// This callback will be called when a notification sent by this plugin is clicked on. -struct NotificationActivationCallback : winrt::implements { - LaunchCallback callback; - - HRESULT __stdcall Activate( - LPCWSTR app, - LPCWSTR args, - NOTIFICATION_USER_INPUT_DATA const* data, - ULONG count) noexcept final - { - try { - LaunchData launchData; - // Fill the data map - for (ULONG i = 0; i < count; i++) { - auto item = data[i]; - const std::string key = CW2A(item.Key); - const std::string value = CW2A(item.Value); - launchData.data.try_emplace(key, value); - } - - const auto openedWithAction = args != nullptr; - launchData.payload = CW2A(args); - launchData.didLaunch = true; - launchData.launchType = openedWithAction - ? NativeLaunchType::action : NativeLaunchType::notification; - callback(launchData); - return S_OK; - } - catch (...) { - return winrt::to_hresult(); - } - } -}; - -/// A class factory that creates an instance of NotificationActivationCallback. -struct NotificationActivationCallbackFactory : winrt::implements { - LaunchCallback callback; - - HRESULT __stdcall CreateInstance( - IUnknown* outer, - GUID const& iid, - void** result) noexcept final - { - *result = nullptr; - - if (outer) { - return CLASS_E_NOAGGREGATION; - } - - const auto cb = winrt::make_self(); - cb.get()->callback = callback; - - return cb->QueryInterface(iid, result); - } - - HRESULT __stdcall LockServer(BOOL) noexcept final { return S_OK; } -}; - -/// -/// Updates the Registry to enable notifications. -/// -/// Related resources: -///
    -///
  • https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast-other-apps
  • -///
-///
-/// The app user model ID of the app. Provided during initialization of the plugin. -/// The display name of the app. The name will be shown on the notification toasts. -/// An optional path to the icon of the app. The icon will be shown on the notification toasts -/// An optional string that specifies the background color of the icon, in the format of AARRGGBB. -void UpdateRegistry( - const std::string& aumid, - const std::string& appName, - const std::string& guid, - const std::optional& iconPath -) { - std::stringstream ss; - ss << "Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications\\Backup\\" << aumid; - const auto notifSettingsKeyPath = ss.str(); - RegistryKey key; - - // create registry key - // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\PushNotifications\Backup - winrt::check_win32(RegCreateKeyExA( - HKEY_CURRENT_USER, - notifSettingsKeyPath.c_str(), - 0, - nullptr, - 0, - KEY_WRITE, - nullptr, - key.put(), - nullptr)); - - // put the following key values under the key - // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\PushNotifications\Backup\ - // - // appType = app:desktop - // Setting = s:banner,s:toast,s:audio,c:toast,c:ringing - // wnsId = NonImmersivePackage - - const std::string appType = "app:desktop"; - const std::string setting = "s:banner,s:toast,s:audio,c:toast,c:ringing"; - const std::string wnsId = "NonImmersivePackage"; - winrt::check_win32(RegSetValueExA( - key.get(), - "appType", - 0, - REG_SZ, - reinterpret_cast(appType.c_str()), - static_cast(appType.size() + 1 * sizeof(char)))); - winrt::check_win32(RegSetValueExA( - key.get(), - "Setting", - 0, - REG_SZ, - reinterpret_cast(setting.c_str()), - static_cast(setting.size() + 1 * sizeof(char)))); - winrt::check_win32(RegSetValueExA( - key.get(), - "wnsId", - 0, - REG_SZ, - reinterpret_cast(wnsId.c_str()), - static_cast(wnsId.size() + 1 * sizeof(char)))); - - // now, we register app info to the Registry. - - ss.clear(); - ss.str(std::string()); - ss << "Software\\Classes\\AppUserModelId\\" << aumid; - const auto appInfoKeyPath = ss.str(); - RegistryKey appInfoKey; - - // create registry key - // HKEY_CURRENT_USER\Software\Classes\AppUserModelId\ - winrt::check_win32(RegCreateKeyExA( - HKEY_CURRENT_USER, - appInfoKeyPath.c_str(), - 0, - nullptr, - 0, - KEY_WRITE, - nullptr, - appInfoKey.put(), - nullptr)); - - winrt::check_win32(RegSetValueExA( - appInfoKey.get(), - "DisplayName", - 0, - REG_SZ, - reinterpret_cast(appName.c_str()), - static_cast(appName.size() + 1 * sizeof(char)))); - - if (iconPath.has_value()) { - const auto v = iconPath.value(); - winrt::check_win32(RegSetValueExA( - appInfoKey.get(), - "IconUri", - 0, - REG_SZ, - reinterpret_cast(v.c_str()), - static_cast(v.size() + 1 * sizeof(char)))); - } - - // TODO: Decide if this is possible/worth it to support - // if (iconBgColor.has_value()) { - // const auto v = iconBgColor.value(); - // winrt::check_win32(RegSetValueExA( - // appInfoKey.get(), - // "IconBackgroundColor", - // 0, - // REG_SZ, - // reinterpret_cast(v.c_str()), - // static_cast(v.size() + 1 * sizeof(char)))); - // } - - // combine guid to class id - ss.clear(); - ss.str(std::string()); - ss << '{' << guid << '}'; - const auto clsid = ss.str(); - - // register the guid of the notification activation callback - winrt::check_win32(RegSetValueExA( - appInfoKey.get(), - "CustomActivator", - 0, - REG_SZ, - reinterpret_cast(clsid.c_str()), - static_cast(clsid.size() + 1 * sizeof(char)))); -} - -/// Register the notification activation callback factory -/// and the guid of the callback. -bool RegisterCallback(const std::string& guid, LaunchCallback callback) { - DWORD registration{}; - - const auto factory_ref = winrt::make_self(); - const auto factory = factory_ref.get(); - - // The WinRT GUID constructor terminates the app if there's an invalid GUID, so check it here first. - if (guid.size() != 36 || guid[8] != '-' || guid[13] != '-' || guid[18] != '-' || guid[23] != '-') { - throw std::invalid_argument("Invalid GUID"); - } - - winrt::guid rclsid(guid); - factory->callback = callback; - - winrt::check_hresult(CoRegisterClassObject( - rclsid, - factory, - CLSCTX_LOCAL_SERVER, - REGCLS_MULTIPLEUSE, - ®istration)); - return true; -} - -bool RegisterApp( - const string& aumid, - const string& appName, - const string& guid, - const optional& iconPath, - LaunchCallback callback -) { - UpdateRegistry(aumid, appName, guid, iconPath); - return RegisterCallback(guid, callback); -} diff --git a/flutter_local_notifications_windows/src/registration.hpp b/flutter_local_notifications_windows/src/registration.hpp deleted file mode 100644 index 0616eb261..000000000 --- a/flutter_local_notifications_windows/src/registration.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "ffi_api.h" - -using std::map; -using std::optional; -using std::shared_ptr; -using std::string; - -typedef struct LaunchData { - NativeLaunchType launchType = NativeLaunchType::notification; - map data; - bool didLaunch; - string payload; -} LaunchData; - -using LaunchCallback = std::function; - -bool RegisterApp( - const string& aumid, - const string& appName, - const string& guid, - const optional& iconPath, - LaunchCallback callback -); diff --git a/flutter_local_notifications_windows/src/utils.cpp b/flutter_local_notifications_windows/src/utils.cpp index 361b20fcb..f4f3e8df7 100644 --- a/flutter_local_notifications_windows/src/utils.cpp +++ b/flutter_local_notifications_windows/src/utils.cpp @@ -1,52 +1,27 @@ +#include + #include "utils.hpp" -Bindings pairsToBindings(Pair* pairs, int size) { - Bindings result; - for (int index = 0; index < size; index++) { - const auto pair = pairs[index]; - result.try_emplace(pair.key, pair.value); - } +char* toNativeString(string str) { + const auto size = (int) str.size() + 1; // + 1 for null terminator + const auto result = new char[size]; + strcpy_s(result, size, str.c_str()); return result; } -Pair* bindingsToPairs(Bindings bindings, int* size) { - *size = (int) bindings.size(); - auto array = new Pair[*size]; - int index = 0; - for (const auto pair : bindings) { - array[index].key = pair.first.c_str(); - array[index].value = pair.second.c_str(); - index++; - } - return array; -} - -NativeDetails* getDetailsArray(vector vec, int* size) { - *size = (int) vec.size(); - auto result = new NativeDetails[vec.size()]; - for (int index = 0; index < vec.size(); index++) { - result[index] = vec.at(index); - } - return result; +NativeStringMap toNativeMap(vector entries) { + const auto size = (int) entries.size(); + const auto array = new StringMapEntry[size]; + std::copy(entries.begin(), entries.end(), array); + return { array, size }; } -NotificationData dataFromBindings(Bindings bindings) { +NotificationData dataFromMap(NativeStringMap map) { NotificationData data; - for (const auto pair : bindings) { - const auto key = winrt::to_hstring(pair.first); - const auto value = winrt::to_hstring(pair.second); + for (int index = 0; index < map.size; index++) { + const auto key = winrt::to_hstring(map.entries[index].key); + const auto value = winrt::to_hstring(map.entries[index].value); data.Values().Insert(key, value); } return data; -} - -NativeLaunchDetails* parseLaunchDetails(LaunchData data) { - NativeLaunchDetails* result = new NativeLaunchDetails; - result->didLaunch = data.didLaunch; - result->data = bindingsToPairs(data.data, &result->dataSize); - result->launchType = data.launchType; - result->payload = new char[data.payload.size()]; - result->payloadSize = (int) data.payload.size(); - memcpy(result->payload, data.payload.c_str(), data.payload.size()); - return result; } \ No newline at end of file diff --git a/flutter_local_notifications_windows/src/utils.hpp b/flutter_local_notifications_windows/src/utils.hpp index 63c74de0c..6fd202f3a 100644 --- a/flutter_local_notifications_windows/src/utils.hpp +++ b/flutter_local_notifications_windows/src/utils.hpp @@ -1,26 +1,19 @@ #pragma once -#include -#include - #include #include +#include // <-- This must be the first Windows header +#include + #include "ffi_api.h" -#include "registration.hpp" -using std::map; using std::string; using std::vector; -using winrt::Windows::UI::Notifications::NotificationData; -using Bindings = map; - -Bindings pairsToBindings(Pair* pairs, int size); - -Pair* bindingsToPairs(Bindings bindings, int* size); +using namespace winrt::Windows::UI::Notifications; -NativeDetails* getDetailsArray(vector vec, int* size); +char* toNativeString(string str); -NotificationData dataFromBindings(Bindings bindings); +NativeStringMap toNativeMap(vector entries); -NativeLaunchDetails* parseLaunchDetails(LaunchData details); +NotificationData dataFromMap(NativeStringMap map); From 1cdc0c4a3ed30a0b386677f26fa7eda673791e7d Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 11 Jul 2024 19:43:13 -0400 Subject: [PATCH 053/112] Upgraded Dart to 3.1.0 --- flutter_local_notifications/pubspec.yaml | 4 -- .../lib/src/ffi/bindings.dart | 18 +++--- .../lib/src/ffi/utils.dart | 2 - .../lib/src/plugin/ffi.dart | 2 +- .../pubspec.yaml | 58 ++----------------- 5 files changed, 17 insertions(+), 67 deletions(-) diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index c75bd9b0e..39eb658ac 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -16,10 +16,6 @@ dependencies: timezone: ^0.9.0 xml: ^6.5.0 -dependency_overrides: - flutter_local_notifications_windows: - path: ../flutter_local_notifications_windows - dev_dependencies: flutter_driver: sdk: flutter diff --git a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart index 825417e9e..0d2f7a1d9 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart @@ -250,22 +250,22 @@ class NotificationsPluginBindings { _freeLaunchDetailsPtr.asFunction(); } -class NativePlugin extends ffi.Opaque {} +final class NativePlugin extends ffi.Opaque {} -class StringMapEntry extends ffi.Struct { +final class StringMapEntry extends ffi.Struct { external ffi.Pointer key; external ffi.Pointer value; } -class NativeStringMap extends ffi.Struct { +final class NativeStringMap extends ffi.Struct { external ffi.Pointer entries; @ffi.Int() external int size; } -class NativeNotificationDetails extends ffi.Struct { +final class NativeNotificationDetails extends ffi.Struct { @ffi.Int() external int id; } @@ -275,7 +275,7 @@ abstract class NativeLaunchType { static const int action = 1; } -class NativeLaunchDetails extends ffi.Struct { +final class NativeLaunchDetails extends ffi.Struct { @ffi.Int() external int didLaunch; @@ -294,5 +294,9 @@ abstract class NativeUpdateResult { static const int notFound = 2; } -typedef NativeNotificationCallback = ffi.Pointer< - ffi.NativeFunction>; +typedef NativeNotificationCallback + = ffi.Pointer>; +typedef NativeNotificationCallbackFunction = ffi.Void Function( + NativeLaunchDetails details); +typedef DartNativeNotificationCallbackFunction = void Function( + NativeLaunchDetails details); diff --git a/flutter_local_notifications_windows/lib/src/ffi/utils.dart b/flutter_local_notifications_windows/lib/src/ffi/utils.dart index 649f844e5..2db72e60b 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/utils.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/utils.dart @@ -7,8 +7,6 @@ import "package:flutter_local_notifications_windows/src/plugin/base.dart"; import "bindings.dart"; import "../details.dart"; -typedef NativeCallbackType = Void Function(NativeLaunchDetails details); - extension NativeStringMapUtils on NativeStringMap { Map toMap() => { for (var index = 0; index < size; index++) diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index ff3cb5300..71444755d 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -38,7 +38,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { final aumId = settings.appUserModelId.toNativeUtf8(allocator: arena); final guid = settings.guid.toNativeUtf8(allocator: arena); final iconPath = settings.iconPath?.toNativeUtf8(allocator: arena) ?? nullptr; - final callback = NativeCallable.listener(_globalLaunchCallback).nativeFunction; + final callback = NativeCallable.listener(_globalLaunchCallback).nativeFunction; final result = _bindings.init(_plugin, appName, aumId, guid, iconPath, callback); return result.toBool(); }); diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index 228d54dc9..0bd72dc1c 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -1,77 +1,29 @@ name: flutter_local_notifications_windows description: "A new Flutter FFI plugin project." version: 1.0.0 -homepage: environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.1.0 <4.0.0" flutter: ">=3.0.0" dependencies: - ffi: ^2.1.2 flutter: sdk: flutter + ffi: ^2.1.2 flutter_local_notifications_platform_interface: ^7.2.0 plugin_platform_interface: ^2.0.2 timezone: ^0.9.4 xml: ^6.5.0 - + dev_dependencies: - ffigen: ^7.0.0 # using such a low version to avoid Dart 3.0 + ffigen: ^12.0.0 # using such a low version to avoid Dart 3.0 + very_good_analysis: ^6.0.0 flutter_test: sdk: flutter - very_good_analysis: ^6.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec -# The following section is specific to Flutter packages. flutter: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) - # which should be registered in the plugin registry. This is required for - # using method channels. - # The Android 'package' specifies package in which the registered class is. - # This is required for using method channels on Android. - # The 'ffiPlugin' specifies that native code should be built and bundled. - # This is required for using `dart:ffi`. - # All these are used by the tooling to maintain consistency when - # adding or updating assets for this project. - # - # Please refer to README.md for a detailed explanation. plugin: implements: flutter_local_notifications platforms: windows: ffiPlugin: true - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages From a5cc1b088dcef7fd5026a33daa6592aa70936bd9 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Fri, 12 Jul 2024 14:20:50 -0400 Subject: [PATCH 054/112] Documented the C++ code --- .../example/lib/main.dart | 1 - .../example/lib/windows.dart | 12 ++-- .../lib/src/plugin/ffi.dart | 12 ++++ .../src/ffi_api.h | 35 ++++++++++++ .../src/plugin.cpp | 57 +++---------------- .../src/plugin.hpp | 28 +++++++-- .../src/utils.hpp | 3 + 7 files changed, 87 insertions(+), 61 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index c68e6f10d..4a190d935 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -174,7 +174,6 @@ Future main() async { initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) { - print(notificationResponse.notificationResponseType); switch (notificationResponse.notificationResponseType) { case NotificationResponseType.selectedNotification: selectNotificationStream.add(notificationResponse); diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index 2b3780833..5f3a00593 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -10,7 +10,7 @@ import 'plugin.dart'; const WindowsInitializationSettings initSettings = WindowsInitializationSettings( appName: 'Flutter Local Notifications Example', appUserModelId: 'Com.Dexterous.FlutterLocalNotificationsExample', - guid: '68d0c89d-760f-4f79-a067-ae8d4220ccc1', + guid: 'd49b0314-ee7a-4626-bf79-97cdb8a991bb', ); List examples({ @@ -113,7 +113,7 @@ Future _showWindowsNotificationWithDuration() async { id++, 'This is a short notification', 'This will last about 7 seconds', - NotificationDetails( + const NotificationDetails( windows: WindowsNotificationDetails(duration: WindowsNotificationDuration.short), ), ); @@ -121,7 +121,7 @@ Future _showWindowsNotificationWithDuration() async { id++, 'This is a long notification', 'This will last about 25 seconds', - NotificationDetails( + const NotificationDetails( windows: WindowsNotificationDetails(duration: WindowsNotificationDuration.long), ), ); @@ -300,7 +300,7 @@ Future _showWindowsNotificationWithDynamic() async { notificationId, 'Dynamic content', 'This notification will be updated from Dart code', - NotificationDetails( + const NotificationDetails( windows: WindowsNotificationDetails( subtitle: '{stopwatch}', ), @@ -376,7 +376,7 @@ Future _showWindowsNotificationWithHeader() async { id++, 'This is the first notification', null, - NotificationDetails( + const NotificationDetails( windows: WindowsNotificationDetails(header: header), ), ); @@ -384,7 +384,7 @@ Future _showWindowsNotificationWithHeader() async { id++, 'This is the second notification', null, - NotificationDetails( + const NotificationDetails( windows: WindowsNotificationDetails(header: header), ), ); diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index 71444755d..dfc3b23fe 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -11,6 +11,14 @@ void _globalLaunchCallback(NativeLaunchDetails details) { FlutterLocalNotificationsWindows.instance?._onNotificationReceived(details); } +extension on String { + bool get isValidGuid => length == 36 + && this[8] == "-" + && this[13] == "-" + && this[18] == "-" + && this[23] == "-"; +} + class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { static FlutterLocalNotificationsWindows? instance; @@ -31,6 +39,10 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { WindowsInitializationSettings settings, { DidReceiveNotificationResponseCallback? onNotificationReceived, }) async => using((arena) { + // The C++ code will crash if there's an invalid GUID, so check it here first. + if (!settings.guid.isValidGuid) { + throw ArgumentError.value(settings.guid, "GUID", "Invalid GUID. Please use xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format\nYou can get one by searching GUID generators online"); + } if (instance != null) return false; instance = this; userCallback = onNotificationReceived; diff --git a/flutter_local_notifications_windows/src/ffi_api.h b/flutter_local_notifications_windows/src/ffi_api.h index c22263c82..b619aa58d 100644 --- a/flutter_local_notifications_windows/src/ffi_api.h +++ b/flutter_local_notifications_windows/src/ffi_api.h @@ -14,38 +14,52 @@ #define FFI_PLUGIN_EXPORT #endif +// FFI needs to use a C-compatible API, even if the code is implemented in C++ or another language. #ifdef __cplusplus extern "C" { #endif +/// A fake type to represent the C++ class that will own the Windows API handles. typedef struct NativePlugin NativePlugin; +/// A key-value pair in a map where both the keys and values are strings. typedef struct StringMapEntry { const char* key; const char* value; } StringMapEntry; +/// A map where the keys and values are all strings. typedef struct NativeStringMap { const StringMapEntry* entries; int size; } NativeStringMap; +/// Details about a notification. typedef struct NativeNotificationDetails { int id; } NativeNotificationDetails; +/// How the app was launched, either by pressing on the notification or an action within it. typedef enum NativeLaunchType { notification, action, } NativeLaunchType; +/// Details about how the app was launched. typedef struct NativeLaunchDetails { + /// Whether the app was launched by a notification int didLaunch; + /// What part of the notification launched the app. NativeLaunchType launchType; + /// The payload sent to the app by the notification. Usually the action that was pressed. const char* payload; + /// The IDs and values of any text inputs in the notification. NativeStringMap data; } NativeLaunchDetails; +/// A callback that is run with [NativeLaunchDetails] when a notification is pressed. +/// +/// This may be called at app launch or even while the app is running. typedef void (*NativeNotificationCallback)(NativeLaunchDetails details); // See: https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.notificationupdateresult @@ -55,28 +69,49 @@ typedef enum NativeUpdateResult { notFound = 2, } NativeUpdateResult; +/// Allocates a new plugin that must be released with [disposePlugin]. FFI_PLUGIN_EXPORT NativePlugin* createPlugin(); +/// Releases the plugin and any resources it was holding onto. FFI_PLUGIN_EXPORT void disposePlugin(NativePlugin* ptr); +/// Initializes the plugin and registers the callback to be run when a notification is pressed. FFI_PLUGIN_EXPORT int init(NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, NativeNotificationCallback callback); +/// Shows the XML as a notification with the given ID. See [updateNotification] for details on bindings. FFI_PLUGIN_EXPORT int showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bindings); +/// Schedules the notification to be shown at the given time (as a [time_t]). FFI_PLUGIN_EXPORT int scheduleNotification(NativePlugin* plugin, int id, char* xml, int time); +/// Updates a notification with the provided bindings after it's been shown. +/// +/// String values in the `` element of the XML can be placeholders instead of values, +/// for example, `{name}` and then call this function with a map with a `name` key, +/// and any string value, and the notification will be updated with that value where `name` was. FFI_PLUGIN_EXPORT NativeUpdateResult updateNotification(NativePlugin* plugin, int id, NativeStringMap bindings); +/// Cancels all notifications. FFI_PLUGIN_EXPORT void cancelAll(NativePlugin* plugin); +/// Cancels a notification with the given ID. +/// +/// Only applications with "package identity" (ie, installed with an MSIX installer), can use this. FFI_PLUGIN_EXPORT void cancelNotification(NativePlugin* plugin, int id); +/// Gets all notifications that have already been shown but are still in the Action center. +/// +/// Only applications with "package identity" (ie, installed with an MSIX installer), can use this. +/// When your app does not have identity, such as in debug mode, this will return an empty array. FFI_PLUGIN_EXPORT NativeNotificationDetails* getActiveNotifications(NativePlugin* plugin, int* size); +/// Gets all notifications that have been scheduled but not yet shown. FFI_PLUGIN_EXPORT NativeNotificationDetails* getPendingNotifications(NativePlugin* plugin, int* size); +/// Releases the memory associated with a [NativeNotificationDetails] array. FFI_PLUGIN_EXPORT void freeDetailsArray(NativeNotificationDetails* ptr); +/// Releases the memory associated with a [NativeLaunchDetails]. FFI_PLUGIN_EXPORT void freeLaunchDetails(NativeLaunchDetails details); #ifdef __cplusplus diff --git a/flutter_local_notifications_windows/src/plugin.cpp b/flutter_local_notifications_windows/src/plugin.cpp index 755bec985..5e9abba42 100644 --- a/flutter_local_notifications_windows/src/plugin.cpp +++ b/flutter_local_notifications_windows/src/plugin.cpp @@ -25,12 +25,7 @@ using RegistryKey = winrt::handle_type; struct NotificationActivationCallback : winrt::implements { NativeNotificationCallback callback; - HRESULT __stdcall Activate( - LPCWSTR app, - LPCWSTR args, - NOTIFICATION_USER_INPUT_DATA const* data, - ULONG count) noexcept final - { + HRESULT __stdcall Activate(LPCWSTR app, LPCWSTR args, NOTIFICATION_USER_INPUT_DATA const* data, ULONG count) noexcept final { try { // Fill the data map vector entries; @@ -44,8 +39,7 @@ struct NotificationActivationCallback : winrt::implements { NativeNotificationCallback callback; - HRESULT __stdcall CreateInstance( - IUnknown* outer, - GUID const& iid, - void** result) noexcept final - { + HRESULT __stdcall CreateInstance(IUnknown* outer, GUID const& iid, void** result) noexcept final { *result = nullptr; - - if (outer) { - return CLASS_E_NOAGGREGATION; - } - + if (outer) return CLASS_E_NOAGGREGATION; const auto cb = winrt::make_self(); cb.get()->callback = callback; - return cb->QueryInterface(iid, result); } HRESULT __stdcall LockServer(BOOL) noexcept final { return S_OK; } }; -/// /// Updates the Registry to enable notifications. /// -/// Related resources: -///
    -///
  • https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast-other-apps
  • -///
-///
-/// The app user model ID of the app. Provided during initialization of the plugin. -/// The display name of the app. The name will be shown on the notification toasts. -/// An optional path to the icon of the app. The icon will be shown on the notification toasts -/// An optional string that specifies the background color of the icon, in the format of AARRGGBB. +/// Related resources: https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast-other-apps void UpdateRegistry( const std::string& aumid, const std::string& appName, @@ -192,18 +167,6 @@ void UpdateRegistry( static_cast(v.size() + 1 * sizeof(char)))); } - // TODO: Decide if this is possible/worth it to support - // if (iconBgColor.has_value()) { - // const auto v = iconBgColor.value(); - // winrt::check_win32(RegSetValueExA( - // appInfoKey.get(), - // "IconBackgroundColor", - // 0, - // REG_SZ, - // reinterpret_cast(v.c_str()), - // static_cast(v.size() + 1 * sizeof(char)))); - // } - // combine guid to class id ss.clear(); ss.str(std::string()); @@ -228,11 +191,6 @@ bool RegisterCallback(const std::string& guid, NativeNotificationCallback callba const auto factory_ref = winrt::make_self(); const auto factory = factory_ref.get(); - // The WinRT GUID constructor terminates the app if there's an invalid GUID, so check it here first. - if (guid.size() != 36 || guid[8] != '-' || guid[13] != '-' || guid[18] != '-' || guid[23] != '-') { - throw std::invalid_argument("Invalid GUID"); - } - winrt::guid rclsid(guid); factory->callback = callback; @@ -241,7 +199,8 @@ bool RegisterCallback(const std::string& guid, NativeNotificationCallback callba factory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, - ®istration)); + ®istration + )); return true; } diff --git a/flutter_local_notifications_windows/src/plugin.hpp b/flutter_local_notifications_windows/src/plugin.hpp index 2e8f79946..f67bcc0ed 100644 --- a/flutter_local_notifications_windows/src/plugin.hpp +++ b/flutter_local_notifications_windows/src/plugin.hpp @@ -14,20 +14,38 @@ using namespace winrt::Windows::UI::Notifications; class WinRTPlugin { public: - bool hasIdentity = false; + /// Whether the plugin has been properly initialized. bool isReady = false; + + /// Whether the current application has package identity (ie, was packaged with an MSIX). + /// + /// This impacts whether apps can query active notifications or cancel them. + /// For more details, see https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/package-identity-overview. + bool hasIdentity = false; + + /// The app user model ID. Used instead of package identity when [hasIdentity] is false. + /// + /// For more details, see https://learn.microsoft.com/en-us/windows/win32/shell/appids winrt::hstring aumid; + + /// The API responsible for showing notifications. Null if [isReady] is false. optional notifier; + + /// The API responsible for querying shown notifications. Null if [isReady] is false. optional history; - NativeLaunchDetails launchData; + + /// A callback to run when a notification is pressed, when the app is or is not running. NativeNotificationCallback callback; WinRTPlugin() { } - ~WinRTPlugin() { freeLaunchDetails(launchData); } - - // void onLaunch(NativeLaunchDetails details); + ~WinRTPlugin() { } + /// Checks whether the current application has package identity. See [hasIdentity] for details. + /// + /// Returns true or false if the package has identity, or null if an error occurred. std::optional checkIdentity(); + + /// Registers the given [callback] to run when a notification is pressed. bool registerApp( const string& aumid, const string& appName, diff --git a/flutter_local_notifications_windows/src/utils.hpp b/flutter_local_notifications_windows/src/utils.hpp index 6fd202f3a..575b73bed 100644 --- a/flutter_local_notifications_windows/src/utils.hpp +++ b/flutter_local_notifications_windows/src/utils.hpp @@ -12,8 +12,11 @@ using std::string; using std::vector; using namespace winrt::Windows::UI::Notifications; +/// Allocates and returns a char array representing the original C++ string. char* toNativeString(string str); +/// Allocates and returns a [NativeStringMap] with the given key-value pairs. NativeStringMap toNativeMap(vector entries); +/// Parses a [NativeStringMap] into a WinRT [NotificationData]. NotificationData dataFromMap(NativeStringMap map); From 37ad031259031544eed471c13bf916f5a5d50611 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Fri, 12 Jul 2024 15:16:14 -0400 Subject: [PATCH 055/112] Made more constructors const --- .../lib/src/details/notification_action.dart | 47 +++++++++---------- .../lib/src/details/notification_audio.dart | 2 +- .../lib/src/details/notification_details.dart | 4 +- .../lib/src/details/notification_group.dart | 2 +- .../lib/src/details/notification_header.dart | 2 +- .../lib/src/details/notification_image.dart | 28 +++++------ .../lib/src/details/notification_input.dart | 6 +-- .../src/details/notification_progress.dart | 9 ++-- .../lib/src/details/notification_text.dart | 2 +- .../lib/src/details/notification_to_xml.dart | 6 +-- 10 files changed, 51 insertions(+), 57 deletions(-) diff --git a/flutter_local_notifications_windows/lib/src/details/notification_action.dart b/flutter_local_notifications_windows/lib/src/details/notification_action.dart index 09bf912e2..1aa18d18b 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_action.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_action.dart @@ -52,7 +52,7 @@ enum WindowsActionPlacement { /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action#attributes class WindowsAction { /// Constructs a Windows notification button from parameters. - WindowsAction({ + const WindowsAction({ required this.content, required this.arguments, this.activationType = WindowsActivationType.foreground, @@ -62,15 +62,7 @@ class WindowsAction { this.inputId, this.buttonStyle, this.tooltip, - }) { - if (image != null && !image!.isAbsolute) { - throw ArgumentError.value( - image!.path, - "WindowsImage.file", - "File path must be absolute", - ); - } - } + }); /// The body text of the button. final String content; @@ -114,19 +106,24 @@ class WindowsAction { /// Serializes this notification action as Windows-compatible XML. /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action#syntax - void toXml(XmlBuilder builder) => builder.element( - "action", - attributes: { - "content": content, - "arguments": arguments, - "activationType": activationType.name, - "afterActivationBehavior": activationBehavior.name, - if (placement != null) "placement": placement!.name, - if (image != null) "imageUri": - Uri.file(image!.absolute.path, windows: true).toFilePath(), - if (inputId != null) "hint-inputId": inputId!, - if (buttonStyle != null) "hint-buttonStyle": buttonStyle!.name, - if (tooltip != null) "hint-toolTip": tooltip!, - }, - ); + void toXml(XmlBuilder builder) { + if (image != null && !image!.isAbsolute) { + throw ArgumentError.value(image!.path, "WindowsImage.file", "File path must be absolute"); + } + builder.element( + "action", + attributes: { + "content": content, + "arguments": arguments, + "activationType": activationType.name, + "afterActivationBehavior": activationBehavior.name, + if (placement != null) "placement": placement!.name, + if (image != null) "imageUri": + Uri.file(image!.absolute.path, windows: true).toFilePath(), + if (inputId != null) "hint-inputId": inputId!, + if (buttonStyle != null) "hint-buttonStyle": buttonStyle!.name, + if (tooltip != null) "hint-toolTip": tooltip!, + }, + ); + } } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart index bb2e140ea..23249214e 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart @@ -120,7 +120,7 @@ class WindowsNotificationAudio { /// Serializes this audio to Windows-compatible XML. void toXml(XmlBuilder builder) => builder.element( "audio", - attributes: { + attributes: { "src": source, "silent": isSilent.toString(), "loop": shouldLoop.toString(), diff --git a/flutter_local_notifications_windows/lib/src/details/notification_details.dart b/flutter_local_notifications_windows/lib/src/details/notification_details.dart index c55558a6c..315ff3b0c 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_details.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_details.dart @@ -66,7 +66,7 @@ class WindowsNotificationDetails { this.images = const [], this.groups = const [], this.progressBars = const [], - this.bindings = const {}, + this.bindings = const {}, this.header, this.audio, this.duration, @@ -116,7 +116,7 @@ class WindowsNotificationDetails { final Map bindings; /// XML attributes for the toast notification as a whole. - Map get attributes => { + Map get attributes => { if (duration != null) "duration": duration!.name, if (timestamp != null) "displayTimestamp": timestamp!.toIso8601StringTz(), if (scenario != null) "scenario": scenario!.name, diff --git a/flutter_local_notifications_windows/lib/src/details/notification_group.dart b/flutter_local_notifications_windows/lib/src/details/notification_group.dart index 13b4ab0cf..b8541b1c9 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_group.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_group.dart @@ -19,7 +19,7 @@ class WindowsGroup { for (final column in columns) { builder.element( "subgroup", - attributes: {"hint-weight": "1"}, + attributes: {"hint-weight": "1"}, nest: () { for (final part in column.parts) { part.toXml(builder); diff --git a/flutter_local_notifications_windows/lib/src/details/notification_header.dart b/flutter_local_notifications_windows/lib/src/details/notification_header.dart index 6ea42d719..3b23d8ce0 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_header.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_header.dart @@ -34,7 +34,7 @@ class WindowsHeader { /// Serializes this header to XML. void toXml(XmlBuilder builder) => builder.element( "header", - attributes: { + attributes: { "id": id, "title": title, "arguments": arguments, diff --git a/flutter_local_notifications_windows/lib/src/details/notification_image.dart b/flutter_local_notifications_windows/lib/src/details/notification_image.dart index 62a12d8dc..20cd04620 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_image.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_image.dart @@ -21,21 +21,13 @@ enum WindowsImageCrop { /// An image in a Windows notification. class WindowsImage extends WindowsNotificationPart { /// Creates a Windows notification image. - WindowsImage.file( + const WindowsImage.file( this.file, { required this.altText, this.addQueryParams = false, this.placement, this.crop, - }) { - if (!file.isAbsolute) { - throw ArgumentError.value( - file.path, - "WindowsImage.file", - "File path must be absolute", - ); - } - } + }); /// Whether Windows should add URL query parameters when fetching the image. final bool addQueryParams; @@ -53,14 +45,20 @@ class WindowsImage extends WindowsNotificationPart { final WindowsImageCrop? crop; @override - void toXml(XmlBuilder builder) => builder.element( - "image", - attributes: { + void toXml(XmlBuilder builder) { + if (!file.isAbsolute) { + throw ArgumentError.value( + file.path, + "WindowsImage.file", + "File path must be absolute", + ); + } + builder.element("image", attributes: { "src": Uri.file(file.absolute.path, windows: true).toFilePath(), "alt": altText, "addImageQuery": addQueryParams.toString(), if (placement != null) "placement": placement!.name, if (crop != null) "hint-crop": crop!.name, - }, - ); + },); + } } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_input.dart b/flutter_local_notifications_windows/lib/src/details/notification_input.dart index c535bc3c2..8f9368498 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_input.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_input.dart @@ -47,7 +47,7 @@ class WindowsTextInput extends WindowsInput { @override void toXml(XmlBuilder builder) => builder.element( "input", - attributes: { + attributes: { "id": id, "type": type.name, if (title != null) "title": title!, @@ -75,7 +75,7 @@ class WindowsSelectionInput extends WindowsInput { @override void toXml(XmlBuilder builder) => builder.element( "input", - attributes: { + attributes: { "id": id, "type": type.name, if (title != null) "title": title!, @@ -106,7 +106,7 @@ class WindowsSelection { /// Serializes this item to XML. void toXml(XmlBuilder builder) => builder.element( "selection", - attributes: { + attributes: { "id": id, "content": content, }, diff --git a/flutter_local_notifications_windows/lib/src/details/notification_progress.dart b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart index 95bf16208..ef1ca2f67 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_progress.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart @@ -38,7 +38,7 @@ class WindowsProgressBar { /// Serializes this progress bar to XML. void toXml(XmlBuilder builder) => builder.element( "progress", - attributes: { + attributes: { "status": status, "value": "{$id-progressValue}", if (title != null) "title": title!, @@ -48,10 +48,9 @@ class WindowsProgressBar { /// The data bindings for this progress bar. /// - /// To support dynamic updates, [toXml] will inject placeholder strings - /// called data bindings instead of actual values. This represents the - /// new data. - Map get data => { + /// To support dynamic updates, [toXml] will inject placeholder strings called data bindings + /// instead of actual values. This represents the new data. + Map get data => { "$id-progressValue": value?.toString() ?? "indeterminate", if (label != null) "$id-progressString": label!, }; diff --git a/flutter_local_notifications_windows/lib/src/details/notification_text.dart b/flutter_local_notifications_windows/lib/src/details/notification_text.dart index 637595eef..324316617 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_text.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_text.dart @@ -39,7 +39,7 @@ class WindowsNotificationText extends WindowsNotificationPart { @override void toXml(XmlBuilder builder) => builder.element( "text", - attributes: { + attributes: { if (languageCode != null) "lang": languageCode!, if (placement != null) "placement": placement!.name, "hint-callScenarioCenterAlign": centerIfCall.toString(), diff --git a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart index 69cc2c6ec..d5e637c9c 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart @@ -11,8 +11,8 @@ String notificationToXml({ final builder = XmlBuilder(); builder.element( "toast", - attributes: { - ...details?.attributes ?? {}, + attributes: { + ...details?.attributes ?? {}, if (payload != null) "launch": payload, if (details?.scenario == null) "useButtonStyle": "true", }, @@ -20,7 +20,7 @@ String notificationToXml({ builder.element("visual", nest: () { builder.element( "binding", - attributes: {"template": "ToastGeneric"}, + attributes: {"template": "ToastGeneric"}, nest: () { builder.element("text", nest: title); builder.element("text", nest: body); From b8c3e2744c3940b9a5f313d850b18bd946f2dda0 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Fri, 12 Jul 2024 15:17:23 -0400 Subject: [PATCH 056/112] Updated example --- .../example/lib/main.dart | 16 ++++++++-------- .../example/lib/windows.dart | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 4a190d935..c43a31e86 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -1112,7 +1112,7 @@ class _HomePageState extends State { WindowsNotificationDetails( subtitle: 'Click the three dots for another button', actions: [ - WindowsAction( + const WindowsAction( content: 'Text', arguments: 'text', ), @@ -1121,7 +1121,7 @@ class _HomePageState extends State { arguments: 'image', image: File('icons/coworker.png').absolute, ), - WindowsAction( + const WindowsAction( content: 'Context', arguments: 'context', placement: WindowsActionPlacement.contextMenu, @@ -1169,17 +1169,17 @@ class _HomePageState extends State { categoryIdentifier: darwinNotificationCategoryText, ); - final WindowsNotificationDetails windowsNotificationDetails = + const WindowsNotificationDetails windowsNotificationDetails = WindowsNotificationDetails( actions: [ WindowsAction(content: 'Send', arguments: 'send-reply', inputId: 'text'), ], inputs: [ - const WindowsTextInput(id: 'text', title: 'Send a reply?', hintText: 'Message'), + WindowsTextInput(id: 'text', title: 'Send a reply?', hintText: 'Message'), ], ); - final NotificationDetails notificationDetails = NotificationDetails( + const NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, macOS: darwinNotificationDetails, @@ -1240,12 +1240,12 @@ class _HomePageState extends State { categoryIdentifier: darwinNotificationCategoryText, ); - final WindowsNotificationDetails windowsNotificationDetails = + const WindowsNotificationDetails windowsNotificationDetails = WindowsNotificationDetails( actions: [ WindowsAction(content: 'Submit', arguments: 'submit', inputId: 'choice'), ], - inputs: const [ + inputs: [ WindowsSelectionInput( id: 'choice', defaultItem: 'abc', @@ -1257,7 +1257,7 @@ class _HomePageState extends State { ], ); - final NotificationDetails notificationDetails = NotificationDetails( + const NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, macOS: darwinNotificationDetails, diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index 5f3a00593..d4ad9aea9 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -132,7 +132,7 @@ Future _showWindowsNotificationWithScenarios() async { id++, 'This is an alarm', null, - NotificationDetails( + const NotificationDetails( windows: WindowsNotificationDetails( scenario: WindowsNotificationScenario.alarm, actions: [ @@ -145,7 +145,7 @@ Future _showWindowsNotificationWithScenarios() async { id++, 'This is an incoming call', null, - NotificationDetails( + const NotificationDetails( windows: WindowsNotificationDetails( scenario: WindowsNotificationScenario.incomingCall, actions: [ @@ -158,7 +158,7 @@ Future _showWindowsNotificationWithScenarios() async { id++, 'This is a reminder', null, - NotificationDetails( + const NotificationDetails( windows: WindowsNotificationDetails( scenario: WindowsNotificationScenario.reminder, actions: [ @@ -171,7 +171,7 @@ Future _showWindowsNotificationWithScenarios() async { id++, 'This is an urgent notification', null, - NotificationDetails( + const NotificationDetails( windows: WindowsNotificationDetails( scenario: WindowsNotificationScenario.urgent, actions: [ @@ -324,7 +324,7 @@ Future _showWindowsNotificationWithActivation() => flutterLocalNotificatio id++, 'These buttons do different things', 'Click on each one!', - NotificationDetails( + const NotificationDetails( windows: WindowsNotificationDetails( actions: [ WindowsAction( @@ -348,7 +348,7 @@ Future _showWindowsNotificationWithButtonStyle() => flutterLocalNotificati id++, 'Incoming call', 'Your best friend', - NotificationDetails( + const NotificationDetails( windows: WindowsNotificationDetails( actions: [ WindowsAction( From cb08f6f1e6ef291e55e3d978253d74b6d4d61bb2 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Fri, 12 Jul 2024 16:17:43 -0400 Subject: [PATCH 057/112] Documented Dart APIs --- .../analysis_options.yaml | 4 +-- .../lib/src/details.dart | 4 +++ .../lib/src/details/notification_details.dart | 2 +- .../lib/src/details/notification_to_xml.dart | 3 ++ .../lib/src/ffi/utils.dart | 29 ++++++++++++++----- .../lib/src/plugin/base.dart | 3 ++ .../lib/src/plugin/ffi.dart | 16 ++++++++-- .../lib/src/plugin/stub.dart | 9 +++--- 8 files changed, 52 insertions(+), 18 deletions(-) diff --git a/flutter_local_notifications_windows/analysis_options.yaml b/flutter_local_notifications_windows/analysis_options.yaml index 5595d746c..54e222c9d 100644 --- a/flutter_local_notifications_windows/analysis_options.yaml +++ b/flutter_local_notifications_windows/analysis_options.yaml @@ -48,5 +48,5 @@ linter: cascade_invocations: false # cascades are often harder to read # Temporarily disabled until we are ready to document - public_member_api_docs: false - flutter_style_todos: false + # public_member_api_docs: false + # flutter_style_todos: false diff --git a/flutter_local_notifications_windows/lib/src/details.dart b/flutter_local_notifications_windows/lib/src/details.dart index 252be856a..e7e714ac5 100644 --- a/flutter_local_notifications_windows/lib/src/details.dart +++ b/flutter_local_notifications_windows/lib/src/details.dart @@ -11,8 +11,12 @@ export "details/notification_progress.dart"; export "details/notification_text.dart"; export "details/notification_to_xml.dart"; +/// The result of updating a notification. enum NotificationUpdateResult { + /// The update was successful. success, + /// There was an unexpected error updating the notification. error, + /// No notification with the provided ID could be found. notFound, } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_details.dart b/flutter_local_notifications_windows/lib/src/details/notification_details.dart index 315ff3b0c..760513f97 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_details.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_details.dart @@ -110,7 +110,7 @@ class WindowsNotificationDetails { /// Custom bindings in the notification. /// - /// Text elements can contains "bindings", which are entered as `` directly into the + /// Text elements can contains "bindings", which are entered as `{bindingName}` directly into the /// string values. You can then update them while or after the notification is launched by /// using the binding name as the key here, and the value as any string you want final Map bindings; diff --git a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart index d5e637c9c..5adfb84a1 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart @@ -2,6 +2,9 @@ import "package:xml/xml.dart"; import "notification_details.dart"; +/// Converts a notification with [WindowsNotificationDetails] into well-formed XML. +/// +/// For more details, refer to the [Toast Notification XML schema](https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root). String notificationToXml({ String? title, String? body, diff --git a/flutter_local_notifications_windows/lib/src/ffi/utils.dart b/flutter_local_notifications_windows/lib/src/ffi/utils.dart index 2db72e60b..31b0d791a 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/utils.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/utils.dart @@ -7,17 +7,22 @@ import "package:flutter_local_notifications_windows/src/plugin/base.dart"; import "bindings.dart"; import "../details.dart"; +/// Helpful methods on native string maps. extension NativeStringMapUtils on NativeStringMap { + /// Converts this map to a typical Dart map. Map toMap() => { for (var index = 0; index < size; index++) entries[index].key.toDartString(): entries[index].value.toDartString(), }; } +/// Helpful methods on integers. extension IntUtils on int { + /// Converts this integer into a boolean. Useful for return types of C functions. bool toBool() => this == 1; } +/// Gets the [NotificationResponseType] from a [NativeLaunchType]. NotificationResponseType getResponseType(int launchType) { switch (launchType) { case NativeLaunchType.notification: return NotificationResponseType.selectedNotification; @@ -26,6 +31,7 @@ NotificationResponseType getResponseType(int launchType) { } } +/// Gets the [NotificationUpdateResult] from a [NativeUpdateResult]. NotificationUpdateResult getUpdateResult(int result) { switch (result) { case 0: return NotificationUpdateResult.success; @@ -35,7 +41,9 @@ NotificationUpdateResult getUpdateResult(int result) { } } +/// Helpful methods on string maps. extension MapToNativeMap on Map { + /// Allocates and returns a pointer to a [NativeStringMap] using the provided arena. NativeStringMap toNativeMap(Arena arena) { final pointer = arena(); pointer.ref.size = length; @@ -50,12 +58,17 @@ extension MapToNativeMap on Map { } } -List parseActiveNotifications(Pointer array, int length) => [ - for (var index = 0; index < length; index++) - ActiveNotification(id: array[index].id), -]; +/// Helpful methods on native notification details. +extension NativeNotificationDetailsUtils on Pointer { + /// Parses this array as a list of [ActiveNotification]s. + List asActiveNotifications(int length) => [ + for (var index = 0; index < length; index++) + ActiveNotification(id: this[index].id), + ]; -List parsePendingNotifications(Pointer array, int length) => [ - for (var index = 0; index < length; index++) - PendingNotificationRequest(array[index].id, null, null, null), -]; + /// Parses this array os a list of [PendingNotificationRequest]s. + List asPendingRequests(int length) => [ + for (var index = 0; index < length; index++) + PendingNotificationRequest(this[index].id, null, null, null), + ]; +} diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart index e081d957f..63de30dff 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/base.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -6,7 +6,9 @@ import "../details.dart"; export "package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart"; export "package:timezone/timezone.dart"; +/// The Windows implementation of `package:flutter_local_notifications`. abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatform { + /// Initializes the plugin. No other method should be called before this. Future initialize( WindowsInitializationSettings settings, { DidReceiveNotificationResponseCallback? onNotificationReceived, @@ -31,6 +33,7 @@ abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatfor WindowsNotificationDetails? details, }); + /// Schedules a notification to appear at the given date and time. Future zonedSchedule( int id, String? title, diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index dfc3b23fe..a17bca01e 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -19,15 +19,27 @@ extension on String { && this[23] == "-"; } +/// The FFI implementation of `package:flutter_local_notifications`. class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { + /// The global instance of this plugin. Used in [_globalLaunchCallback]. static FlutterLocalNotificationsWindows? instance; + /// The FFI generated bindings to the native code. late final NotificationsPluginBindings _bindings; + + /// A pointer to the C++ handler class. late final Pointer _plugin; + /// The last recorded launch details, if any. + /// + /// If the app is opened with a notification, this can be read with [getNotificationAppLaunchDetails]. + /// If a notification is pressed while the app is running, this will be passed to [userCallback]. NativeLaunchDetails? _details; + + /// A user-provided callback from [initialize] to run when a notification is pressed. DidReceiveNotificationResponseCallback? userCallback; + /// Creates an instance of the native plugin. FlutterLocalNotificationsWindows() { final library = DynamicLibrary.open("flutter_local_notifications_windows.dll"); _bindings = NotificationsPluginBindings(library); @@ -78,7 +90,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { Future> getActiveNotifications() async => using((arena) { final length = arena(); final array = _bindings.getActiveNotifications(_plugin, length); - final result = parseActiveNotifications(array, length.value); + final result = array.asActiveNotifications(length.value); _bindings.freeDetailsArray(array); return result; }); @@ -87,7 +99,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { Future> pendingNotificationRequests() async => using((arena) { final length = arena(); final array = _bindings.getPendingNotifications(_plugin, length); - final result = parsePendingNotifications(array, length.value); + final result = array.asPendingRequests(length.value); _bindings.freeDetailsArray(array); return result; }); diff --git a/flutter_local_notifications_windows/lib/src/plugin/stub.dart b/flutter_local_notifications_windows/lib/src/plugin/stub.dart index 439a27bbd..8dc06bf53 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/stub.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/stub.dart @@ -1,16 +1,15 @@ import "../details.dart"; import "base.dart"; +/// A stub implementation for platforms that don't support FFI. class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { - FlutterLocalNotificationsWindows() { - throw UnimplementedError("This is just the stub implementation. Do not use this"); - } - @override Future initialize( WindowsInitializationSettings settings, { DidReceiveNotificationResponseCallback? onNotificationReceived, - }) async => false; + }) async { + throw UnsupportedError("This platform does not support Windows notifications"); + } @override Future cancel(int id) async { } From a9b1892ff5c736f82a839d1cdb558f09db58b548 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 16 Jul 2024 19:03:57 -0400 Subject: [PATCH 058/112] Added unit tests and polished --- .../example/lib/windows.dart | 4 +- .../bin/test.dart | 40 ++++++ flutter_local_notifications_windows/build.bat | 6 + .../flutter_local_notifications_windows.dll | Bin 0 -> 368128 bytes .../lib/src/details.dart | 2 +- .../lib/src/details/notification_audio.dart | 10 +- .../lib/src/details/notification_details.dart | 6 +- ...ation_group.dart => notification_row.dart} | 6 +- .../lib/src/ffi/bindings.dart | 33 +++++ .../lib/src/plugin/base.dart | 3 + .../lib/src/plugin/ffi.dart | 53 +++++-- .../lib/src/plugin/stub.dart | 3 + .../pubspec.yaml | 1 + .../src/ffi_api.cpp | 9 +- .../src/plugin.cpp | 8 +- .../test/bindings_test.dart | 37 +++++ .../test/details_test.dart | 129 ++++++++++++++++++ .../test/icon.png | Bin 0 -> 1443 bytes .../test/plugin_test.dart | 71 ++++++++++ .../test/scheduled_test.dart | 38 ++++++ .../test/sound.mp3 | Bin 0 -> 37616 bytes .../test/xml_test.dart | 69 ++++++++++ melos.yaml | 10 ++ 23 files changed, 506 insertions(+), 32 deletions(-) create mode 100644 flutter_local_notifications_windows/bin/test.dart create mode 100644 flutter_local_notifications_windows/build.bat create mode 100644 flutter_local_notifications_windows/flutter_local_notifications_windows.dll rename flutter_local_notifications_windows/lib/src/details/{notification_group.dart => notification_row.dart} (94%) create mode 100644 flutter_local_notifications_windows/test/bindings_test.dart create mode 100644 flutter_local_notifications_windows/test/details_test.dart create mode 100644 flutter_local_notifications_windows/test/icon.png create mode 100644 flutter_local_notifications_windows/test/plugin_test.dart create mode 100644 flutter_local_notifications_windows/test/scheduled_test.dart create mode 100644 flutter_local_notifications_windows/test/sound.mp3 create mode 100644 flutter_local_notifications_windows/test/xml_test.dart diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index d4ad9aea9..d3afd4cb2 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -217,8 +217,8 @@ Future _showWindowsNotificationWithGroups() => flutterLocalNotificationsPl NotificationDetails( windows: WindowsNotificationDetails( subtitle: 'Caption text is fainter', - groups: [ - WindowsGroup([ + groups: [ + WindowsRow([ WindowsColumn([ WindowsImage.file(File('icons/coworker.png').absolute, altText: 'A coworker'), const WindowsNotificationText(text: 'A coworker', isCaption: true), diff --git a/flutter_local_notifications_windows/bin/test.dart b/flutter_local_notifications_windows/bin/test.dart new file mode 100644 index 000000000..5a5a33372 --- /dev/null +++ b/flutter_local_notifications_windows/bin/test.dart @@ -0,0 +1,40 @@ +import "dart:isolate"; + +import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; +import "package:timezone/standalone.dart"; + +const settings = WindowsInitializationSettings(appName: "Test app", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); + +void main() async { + await Isolate.spawn(main2, null); + await Isolate.spawn(main3, null); + + await Future.delayed(const Duration(seconds: 5)); +} + +Future main3(_) async { + await Future.delayed(const Duration(seconds: 4)); + // Scheduled: + final plugin = FlutterLocalNotificationsWindows(); + await plugin.initialize(settings); + await initializeTimeZone(); + + final location = getLocation("US/Eastern"); + final now = TZDateTime.now(location); + final later = now.add(const Duration(days: 1)); + await plugin.zonedSchedule(300, null, null, later, null); + await plugin.zonedSchedule(301, null, null, later, null); + await plugin.zonedSchedule(302, null, null, later, null); +} + +Future main2(_) async { + final bindings = {"title": "Bindings title", "body": "Bindings body"}; + await Future.delayed(const Duration(seconds: 1)); + // Bindings: + final plugin = FlutterLocalNotificationsWindows(); + await plugin.initialize(settings); + await plugin.show(503, "{title}", "{body}"); + await Future.delayed(const Duration(milliseconds: 100)); + await plugin.updateBindings(id: 503, bindings: bindings); + await plugin.updateBindings(id: 503, bindings: bindings); +} diff --git a/flutter_local_notifications_windows/build.bat b/flutter_local_notifications_windows/build.bat new file mode 100644 index 000000000..bf7478293 --- /dev/null +++ b/flutter_local_notifications_windows/build.bat @@ -0,0 +1,6 @@ +@echo off +cd build +cmake ../windows +cmake --build . +cd .. +copy build\shared\Debug\flutter_local_notifications_windows.dll . diff --git a/flutter_local_notifications_windows/flutter_local_notifications_windows.dll b/flutter_local_notifications_windows/flutter_local_notifications_windows.dll new file mode 100644 index 0000000000000000000000000000000000000000..6dc60d5ff108966d543c173737b969113e58b939 GIT binary patch literal 368128 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

s6)k=Meth&+CS7rQzcVKlRhP;V;tgJHG+^y!Ca% zchvA}2>;tVb;F;d;pY(k`+D&IQO%!h!oOdS^?hB#_aOZCO?A^VYK5%t2ipPPx#oJo zN~OVkoOv*;P6^nJ{8w$!qi{`H<*k^m!$dV^8#&O(fkqBAa-fj|jT~s?KqChlInc;~ zMh-M`ppgTO9BAY~BL^Be(8z&C4m5J0kpqn!Xyia62O2rh$bm)jfnH6<{)Pij&C#rP|)3XI>866eAFETdDs%g&0yYxvpm*XIMpQ&N3wD2{u7POK-; zc}-w)QL2YE>oZb)9{dGLrlkgA2{I+sj|TPH6HPo5$`U8w#S@-hO9?}e0t1pxkiGy3 z0H)|Tz!VpyK8D6)@VWu^E5h4ffUGE$kbz?PhcR~oB(tO_wVZeyLlUZbnly7zym~@9 z3m7`1=Qi}HNe64y*2fJUc!y#QI*N5&00R`3H4Dt(1lcQ49J`8mE5hPst-4F&&AZ5& z+fJEVUU$xm5bVE)za@QAeMC%pF%78$j77fHl9f~%C;`I*#VgEu>|EwSmtW@)qh6ss zf#OYplH^hSaPQLI*KkKc@p@vG#p=9Co$@lO#;@Tr_v;(H11cy5`o^i^%+z!>a!z%1 z;*zonWqGXwsPL)ZpRKO0N(>bDNln)kZC?Sl(0i#UGa^vt9<1bkOL0eoAV_63BX> z_>Vxb%`GqyH7ioH1l+C$0$qdNYyrPwUJ`zh3^%`^FospKAo1!#CoeL?=oi8Lfx^+L zxt{#yoy#iQP{L8n^5nII8l8xurz)N~WIW)~iO^3k0KA_3!>f~fWyg8ed)lI@I{94W zmoi)VE)pWi@#H0uv$=t88L8Qcg-@YA6XopATgdt>b^aqwJh=HsAUPafD`SQe{pF%y zxKse7dh!pgPTq1=aJa3=%Wzc#gJTRQmM%7yEz@=gd8Pm?TL3)GDHptTsPK+t$-uC9InDhMmmCp8d&lK6pLjaW>`X{mUP3y7PN zIv3c~)zTfdh=ieXGb=MpCEnK}m3Uo_M|G0QqSO>t%DR*k31Z=5ra?%nO&qk^>_&$A z)RraH)su=+XGy~pX;3?7lU6h0c2f|Vwwi)a{R}6PxI}lJP?p~T%o=7fOD6psz`-?E zR=z3In3kH#Um5y~$AuA7q)I&uyC}9!@ZbeG@~4^CPvc=_C0;=#GbbLk>qRmS^GICc znba$WQ?ocz!1ftWnD#*7s(7r0+DuMkYSLN+>s(54LwA1+tZaKWO=8EAh1J!CQ)KO9 z=p@QfS#_4c1`5lFilC(gJ>q$yoF)ZUE@0)MPul&PSp6wL#KSV)3k5DvkP6FSHl~@1 za@8F)0gggfSkVMDwJn?@rN3?5Kap%3hICsn|Jd}uEmH$EOG(Yo6ux4x32g9^4Z~oy z2`jB%&a>FC&J%+*EJGLk1D7ed+o9|jjFo|6u*mqf&1{6NzJeBD{F(|B zLlrWpkM3|Mmt7^?>5U;B`Z!JoG@P;x2-X>rM&mtSI?_vC37Y_g3a$X&v*b~aKL%RB zX`jI(zbL0md7Azu3kZBQSU?OcaN6l=-m8KXp$6Ok#ZpyU-E=9uGB{F!hDwoU=yJMn zo}f+O@IXybs#gXA#}=o~Kz3OYS`jB;6luZGqEhETgi9d+6gwh`$FGWgR1@zP9F{T{ zr3R#oK+gaxj`$e44zw)TWV}JNHoln77~z4YH0bS>oc=#;Xjll{?sBU{PweVBO4>C`Xlg z5DJ=ETt=@Dl%&TfNstU{v3C;khZ&kq%~Zo&(3xWBlwY6~Z*jLcooUAGTMJ}KhTt0q zOCUDT7J?gTZzhC;9hw4f|PAj^I&SOTtc**if{z(Io8%YI85mVL(y zz%0woWprBz*$UZ9Ejyv5f}cB~W07;LHRr}PlHU;1&~hoss>CbQJYd~6LJCofrgO=P zQcI<;Xk`Vm4y}|Hz-FrjmO-?6awVojp3Guzy-1mMK%s`qft6F&+cvMbrB0S>sgQ{WT`SCkMFhv8>0pQ?@ zTO!;V5Ld}vpy;}Bb%$Gu@1^1|p|QkoC0%k=z|}U>7*fS)7;9lUz;e{7!)Z034J?oc z1XRz#0lMYtAc-0gLlI1P2v`=BkGcmorkmhJc*%(JQHmlkPZG?mNdaq1t6FJxe$9g2 zE*wsQaO)jYZ#HNVBI_SVc;Tk!ZcxZI-6QlESdhfB8V_*+YNO9%F~icTdpT;lx*YQW zChqYja>d#*`WnB6>*5e;Ta+7Ywc;ul0wsi4>pzG7&opZiW)*u`3nHty)7UU=1B@w= z)fj0Q$pV*%5b6#IL|yiu;gYTTMx;gWOK5hmMA=*-1g?Ej3j)Oq5B6|X!RfhXi2^GB z5i(1bs6h1uNwP%J4*nIWd`P$~QDV_@!}@LV%f{W>I3bLtyn_F1QYTA@#R)-1e}z0E zVim&-00OGzvh2DN@lbDzElKR~2!xumN)wdED1Zqps~HcGelyN=VH3q=C^;d^-H#Mi z9dE;o08DA+k=43ATV(*!7``q-yhz61$3imxqMI1Mrlzrz{f_I@XNJ&5Th|v8c4+ki zHWbHw{(!wL87DPG5@$+)FOwBdmzQAjjArwbX(MS4*8`;o`?|_)(p5Wrk(f7(HyOqT zOD9=S*GW)8q5Boqsc9TjpQvB2anu1{n&<1hi&O9&>bL4~q&|_9iZZjSVmsMQ`24vYi#u_0caq34+lp)avVoEEU zlb`4qpd4otvB>5$vKB0r8q19gpy-WF2S_+CQk zuspmxUR+uqd8|VOMLYkbcwtsGMetBqJ3Y_d;uIS$f^QhZD56(y5j;ZVz$t<=X@4yd z9E3#+5y6@8?-50?oeq*pV1lN-(^%B7^>9K2w;M)&JD}sNCz8MX;3Ca(m zj0leB5RpXiTv8e;f^QxJ5&Vb76p;)}hb4liaUH^opnYEHqnw-bA1IDjeT@-tZWeOJ z6Fe}V1f|iUdKhvJ(>aR`AWeP?3*tR_2zpGh*dI^ZiI|PXW-Nz*Ir5^1r>Z4gqG%{= zq=ql{r{Gl2JEkuc+8elu+a(?B1&S|7GM001prlKT6;J|)*t8t`@G3{Wi72wvo?foq zlB=F!OVvbcxIiI3GwY%j7YH&^hmqQ0>OHY#;%1hlg3%A5Gq_4V1Qk1|&vHnr=0RF@ zU}TPxUnmVZNje0Ra{8=K{QQ~z}HLEIO!5c5@0_t5|N5N@TUc4$OHdbGM;v2BMg?oIX z+nQXrv8{DZ`Kkt24K%gA?Dq5Q_O4auR-I$o``hh9?Dngwj;T5tVd1#9`N$zfzXQhctkRWq>26M*;isfPl&!qc;U69lh3(I5B{Y1)U7I=o! zU*~#_-!TiM=u^|fk#p$S0u(_o-dnGAZK?c1BPcFQYy*Bggbf1}==u|6PB_{DFgmB< zN3LUenaRM63V0=9915McEUU!#HPkowf(H8c0T23k@=p&GuSn&{U_(59$OfFdA*bGf z;lm{I^n~)LP^_=~773=^hXmxR-czWElHEeP2lu0`2&o=}HA6)ydT4S--K#-n5C@0= zA!$S`T~P4y`Uk;DHGwRuPX^U68+~xSsU+^2`ydq3wIr@*sm^=P?YyDa(q;)9DftXk zF5@|Jb@FRH<2;3XV)C1A!eKlj2Tnng`yqlH=lt|^j13krD-5`4E=F5N2kT21Im$Pg=rU4ssxD;orWX!n#B8v%GQD)Q-bk#U zJ7X}a53D)XG`Cvf_dcuwYBi8YT*PIc5&ZEiC@!bdh1V!tljS7Wx}Jp&NdXl2f zF}zyXyu{)US{z~t{?!NHTCf*5_8Yzy6YGzU_SLpQm1&YhOUW}2K97MSfEE-_^YM1C@ZLE_Kk4urJ(F*0;Zh zp>`NisBiz{<;)-ghw4H9JSfh-j6ZEsRQ-KgUTMI%5?N{ocOkg2fr3|E1=$gRb^bJL z`f8DEEdXKWr`PwSCmll6{GM7!)QC*#lqZay6spiNo)f5WM~*gPNl;UV5gSJ0htcu| zEU!YD$sl!*(5WN{+fK1c1?m!%X=&3`b1|yr>dHdYpf^k;tK-Qe6tgLPS+oQaLk?3} zmh@r3J^FZP#sCCy)&~lt}aOxV2PjP&8@`0V>c!Ahyg@P=J$ln{dg$FG@?zKOu zO$39;kaMz*{3Sb50>v!?ljDi42EX?O9`=)r!m$x0Yb|3U(E5Nd1|}fDQ-!(&EOdM) zFF7%1J6}A5>7=W>9^_D)S@5aDi!4TkV{zF2apN8RZ)H3xjIO;u_RoboaEdQ_1xd(D z&9*iLkTaue?I>r3Ytn-OyK8kg9|?B#(zrag1`H$re#)`n&ruJP$q-YYE!){@j_iW5 zFQMqlilmb>S?c}?Mk`J1r}=4*kF&=3Pd`MqMxxZ!&-Y z_|!u2{N?I?u{T(!K*#a`Vs&8j+Ed68@85DkaZUXY0|F(?HpEO3IKuSC&$%VK{i z21JkTsK(77tUeVEtB-lM2y~0Tqe<1x2vQHo3kIo6a08R9i3F)JaT25^1Hc5SEl+m@ zsaAZ{YZ(DEOX6-Orjq2Za`=;QNq$p=wJE2;z;(v$O!?E8CQi7W$6AH5v(KPBxr4L} z(r8C&)yW%9iu2g!F=RgWUOJ%GiJs$1I(Hu;Q!-cNAd|+>d)4!mgqyoP`}eq*7-irb-je0JqnY{;Rr6vDx*Y+kYDE|b6{7+%;lH?2a@F(Fs{6PLsb;*BzkzQqzT3AuRas5lV zox%AMbeo=0qdIjxB3*Tm-N9zf&QEFRs#0`Ib7#v)7^;X-O;D-A$aBbe1tU2FGhWi2 zj`8|BTZX|+*K0i6zlm>i_}kEOaTxbo)Bla%u~|ff*Kl}#V~3zqA<`M{F9;8mF})jbb4FG(&yFupFU9h6pzuIhKaUIdCtzYQFfh!WD8^ zH8FAGJseQO=u53wW~>oAt@nEq?bgDwc*e#%rNSIgBSJe6-l}vRZbm|V2%eIR`1nA{ zG-+Zm_l1~KA)*KDZ>gqdFQuZgQhJ+7O+!e9I~|tm!OaEWiFa|P`tXEUd5A($N>k5L zY|*06mBjMk?kj-kigMLLqDf&x=oE|f-5QuPcx0~RdLSL)rg|(b1E#uGPt`zg0RsX} ztxML^q?NPXspAH+{F30rTWe>}3&a0h3ESoS8OvxPM_1rGnCA&9D6`9#}H4e*q!aYAbiEH&b{2FdRxE?iK4%k@}h zsk0%?+L&TaMt-MIRU^vp4`V4hMt++oPLvZikS^;d(&?1n<{~9VCc-W1CA0szM?(MDOpW<7U`WWJ1|>U z%GE#^z;Nb$|H$)hnwy|J?-6+f^D4;6m*8_k<@tO_d}Mh(Ok~Qk$9G&EVVOg1aj&IG zvVuWNJW-ZJg!6N#oLQ(xdEiqi7rQMQPs0RinAbEaU0Cya{vEm{qIsRqwGXd>7;*;5 z6!U6qB%(=-Q!dmS8#>jmNpy~BH0*G5~Wq&QfZhH-f$`HOUeUlYZ|B|ms@vZa6 z%cjC-3%_Ob9lv4meRzul-)B;QE;Rrjm_&!snP;!Xo8=;(>Pu6rqi3Gn-YJ~JfubBZ z#niPUb<=C`zfY7F{(tCdg7^pT2mJ3bJhJdV;l}?1z+-aau}^ z^Te>@-2nk6|L6|2um;y>jqq0XQ)YuCQ>=p2QRXqELt1NQ&HPD^K!(w$9NM>G>SgMM zjEiD{nq`UzXmMi;37cwaAXK!y07dA+(Lk2O6F)>>pUk}Sy6CZ+qtYGC?FNQbtT;^czNpzstPF2 z1&m0?p&Dq&Jd`%;5`PL+6H|dnu;R$XiC#W$iKls31P_+=;oWgXF`vCe9OGBuC(v|N zbp@i#Nr2$JLq6b`LTHRJj=wa0I@@^*>e#l7w-f*VbMae+Ww4Lh$4rU|Z8nHN4EuQU zU##2?n#17bsD%|VE@Z5GPsxhNwTtq~XKU-+v_|Ju;0EzZ@AZ|rwe@Zurgu|k?-Oe4 zoroi`l7y)^hv3d}9HyGMBsOMJ6HZ~3;+=nnij|dLqcI`_+6$iZBH)R0;+Yv94^{w! z{k~X)qvq-a+k&{3Rb3W_pb$ht*o)Lf*92WqL6k4s2w`9ftVqIou1SkJrACxXUsvs} zTgUWdSE+~aEWjk8V%%q0D#ohFDpqv^Po1c%q_b3DO#pN+_IRa4Zq*8u4qY%*8DKFK z4*>W_Z7$<)L~XXOgMM@{_y621;sy##N(>%uW&aPi7US2@RN?}9>AL436)$=`O<%wp zCh-3nl*i(CJ^p8--V@U#CMKq7Ol))h#Yq}Wnu$>kT47$w&Y}hOE^CdxwgnDzKiu7e zNE$9T)pkGJY&m^_Bg!nH-GvMH(>a9R5BEfl!DY{W-gwM@DWfvEeIzUBoW&nHZ`jHb zDrm|ivdC3W%kE;c4m&oO$PPD4Wu%^h8$nN%ogxqEP9-ULQWJw?1&ulMGKWMqCl|yc zUQdD#3#{4`t6hh*7V;iwLg2~j%;Iu6+A&vGb5x9YUvdV{R|t3WSCFTxWjAox!f^4A zJTII9T5+fooM|0*qEZG$im5N01QI_G2wTh@qKD>!p&eNRy`cDyXr_{|kHYk=Lt5UF zV5FDviNPb0J;G;MKZERTA_MwtN+(BF0}UxFayXYp`HD~7;vIo zEgE9DE=y+J`_Q>7r2Z}0Xz~H!Ss<5!@00?$pGa1QTO@rU6U$@*8;pb5!!i3~$8Ts7kT>Xgh*@93ILy@OCLO%itzk=`dF85v2EtUzlOA4u$F*0bZ~-BlS?U8()iT|MhhvvOP$J8#TVK4y zk*<#BLL&#hK@WIl=5;xh;P%AY&w;2U0M~>HzR$ru5>V3|Viy+3Kgui}ZjR!pex+`! zuEyOzEqS*D0tx-H9WOEn>O?Ce>)nuW8z#DHtQm}>CvOx74w$n99L|_aj#(<>U!1h4 zxIa)5+e-{U)d2*D9Z>o>An`S^)~HH+EnZC+Xg%kGXDE%K@tRocy**Wj=t|7do8P3W zN!8-O`ZDfyop!^`74YBQxu1c>uIcp5{u!g`J@x;#a_D&r-qlcsb=7fH8gh0VrHudr z!WpRLAA^(vm3cLnNg|ePbv+#hmN1)bcn(}VMTv1yQgxsBS!-Wt9=sm_9P)fpeuCLP zij=i71rEmR9&_09VhldLMJJksEhUau^F#5Mb@K5y%gyW98gbSo`9D;7s(xj%n%< z)B*J#ePi>R{4V2>^d!Z!fn~rQY9WH>K}Lh>_Rf{fYxmB0_gXz1v$0|OERQq9#2b11 zh)y{uk3|sA(tt}ItHrN|$m5nP8!V6aWYgT^>UgI zkl|6<$BTQ{B9E^?9c|>{?Bf=U9+v!oYNl15mVRn~u7tMqmOS2xx}ZGvOMyK8Lz_{H zeGKkuow4Ffm}tF4(ZZ*&0Mz}$FT%?uP>94<(ok)m0F1oD2_HhUp zKwr-72XysPF33*gC9*ZA03ZlFn`?YuUdus+Z9kj$MIk7!i$zIX_Ola@_CoDv6D(i@q`2k>hXu=34x(!c5W0KS z57<M)Lx*DK;7 z9J>%Q>PR4ref6nT5*hI*xOS+>3m8#?$D4Jz9PoJ}4qTj?=MWZTz?zAkIn2^dA(laq#1AZaqTzaVPAA zv#X0AZ47l>?qRF(Ho@W38Se zd(LB5QJz&)M2(lRpQ^J~0e` zF4vN0bK1q9iLJn&VD&F|sxFNmji;-E?jk{&bdw zcJSvYQ~(kA(?dePF#O2~!Q|r4a~|-gnH<;bD}QRlPlY>%X(empR5E=BLI9yVH-do@ zb4S8b3Fu5=8jdcg3%Dp?UV(eJsR=NqLc+Zg7hd)U=0JD_wF|xEMGx83AgB=mFQyqn z1J+C)X*clqpg)*rU6(Z)bsA4fcX2q;s9_D(D7U=GO?DnRy&_RKE~Bj9P&V%28Rnc7 zv|;bd1FQ9VV_C;_O*@`2m!5p8g~&Pgb%oX@%pt)w;YXd#S=IrM_9O%gE>|m9-SbP* z;>@d5v8g5x-dpddKwY8Z-a`jjcD4~8-0T0-{Hyo#yP|VOUMYY7LuJ?b9&VVNkC*0a zGXi_#RUSXpWwX}NTVQTsJZjNVw>EMnl=RTCMh{YzWk(2W|7hY-so45!d*-jVJ=;I@ z7hMpf|3d8cg#LvcT=ehiRSW$SVqlL4$<~@4>VKO4YVLcIbhLIa<|6d}>~+!q+c~w- z-vso}kz2ek4Y=@sM}9tn(pmJkwCKOBy^H@# z&a8$0wd8;5KGTm|uFGf((y9JF^|&ZhB-a_nKZf+5)1x-m8?HacO;>xN>h5RG53ZXM z?PA^f3|B*Z_QLA;&XH-X5HZNO=+gimh+o6v;<<0(cV| zK7@o8eZ7OPg;qC$&kU_usO$`MnI#D)aV<;YMqdG(CCO`FllQD_#gE!3(~BFx7L3L_ zo@*bC5Eu<^IaO6d`%86pIK%?|`NRTlK|i07d_Pnb>(}RZFHf8>m%g?np_HSZ-br5g zax)3RX>lTp*d>_v%jDznBTYZVIhD68n%NAN2Nwv3p?L7eu>)WM^9Tvz9yLr$L%H^8 z5Q5Xdx8{&jr_qk-$0I;Ts-Dvm;0>+|9?qjLy=E!_a5gzxn^lSqgFD=)sgKdKG=PhWyjI4) z3-m2iLzbTm4w!A)Tjy&`a$OBiI`FqTvFK+Y7W-*^-@$-CMJXvsK5PjW!qoL)lw}o8 z6}wTcicjJi38%_|QngHX08QwMLZOP45d=mE&H!?mxb%ocyrwS%9Zc*}w+nu+v=Wn- z0i~)Fag`*`cvZ{9pC?i#^o3(Sw!MuL&ktf)Bp81Ar@9);1GI|79U+_o9|_Igelpu? zRfgFCRsx7CU@n1XNp_lkw>=hzWylq~oto$P&DohFfE=h#COIEJu8*t3(M$o&A?loyz%>3_rq2N6un4$h=})V@;n*wy}4N(PoOd^;xl|; zue5NwP0RThD$W$Vv~54bE%VZ>ua@XP{CMMcaJ9An=!3O#_>XaHD306J$@U*y3HXo6 zmstK|9CFlvBs9>n%#bd)3MRjnKcW91g!Ugqt4^Qj&! zlCTv&>Rudifs%vRg3(gYsrJzbVfl|Yq4Ka3nQ$eiLdMYFi&mg+o(!>Tk1D7VtDOFW zqwY>41<5y)5b*e27DN39=`RLq?LTBTcRBqBJpjk1|6rc}<08W}AIh~)gAfk?@!RQP z{6}B#Cg?v%fqDmUHgkV+_>U%?&>*UQE4DgXZM$GvVn)S(TnRxUi_?K80KU+~s7)v7 zN2WR#HT_2{kO%$88puJY|5)4^5N`kRlTRp`@sgH_*N>%4MD`!U z1w)Ac_#88U90dJGfXoK9ilomy86yB?maKrKLNZFWR82EYQ{*EfkSlg~`H!Ad7vK|C z{B$ZOLI077^_&4%E@3HAmFTd}9(od;BP6{Q_PY?Nt85(Sqd_|sk*{0m;w*Z%;Go|I zyc>hDAd7P`7h9z5Zpxj}c86n`?(BNL(7E1rEE710IF>2LklET+6l-0xhWJ5(2?j*% z9YffTWm$7*csyaWW69yH;aGZdqT|;LLosMD-H!_3;KqyP@aS9Fdul6Ql2C|uX4}uV zlq8IkVjxL3$IUI$yTK=U`6k-&*xaF`zpazGus!d0cIQLT;|`2>terZF>7vBgOYp2a^2;IRcSFh_g_Ng`rS@ged{uq-vb>{NW4+3eD& z`LLt3vize8rKhC}E5-Go=#OJN;7F;a(**wvWuy`CmJo3$io=51Qi2>B*xRZjCrUR&l@MtDPb{(#7 zn*gft3r7GYlbC)YJ-oRW5CKjB)gjBcoIMyLU^nWozS&QB@VEiyYJtE^5m&0TRoP58 zC1#06AM+;Pcv5QlDQ!4xoAAu%aP2(CFg8K#ENntYm}=OBT=dqjfVS6<&$M`{Tev&U zXt;_4A5oKz=eS{d4|TLImw8}b1H~aax)p}GXyeeL;Po4oO&`E$iAPbt(#M7A>wy36 zcnAFIy5WzNZI{9SVBPTZ7>HZ+42%FDKE5=p^UHgdr9#9y$`G>8{vo=03nHSY+zlatFKTzXLXushPd;LZhF!{-&VV*FUPwf&%OJxXI zkf1yUij&9M@J~xnQ5Sqa8@@<3ly$*ZA7#>nf&fcR@4+}F6_6VKR@KeY4au#ojso(c<9tVFhojSgkbJQ{P=%6fD&8A5QEsVJ| ztrwG4bJ<&nzsQhF>jm8P|6a~NZ^s0`m(%Jg2D^Im{FBLR+i$*?^8+f4?Vq^KVf*2P zwBaVX&p;lW^OHFo#kdi+TfnS)U!MOFmij{3%ka+9Ss*?bt>ODIBCdG-&3e5427guY zP#}CHEHpt$*HLOT8G>7%{Qa;Cfg=G*+rc{}bH(Ux~K=DenFwqV3<=-M@2G{e9-_0cKOZ zo64DsI|-;e8Rg!Uo=kVoC74Pa(mR7+7HcQ5f?5 zq#)(tbME=T%G1VK{O}s9y1)5xyETW}__6MYdhz2mBTx>0Yy&?EMA!E-KTi8E$;S!S z9BSjoN1RdJ@{w-gvgPB@kHC*y>c;+-k1Y(+ATj$}KHe8%>(H0g9qPr8O(F>4s(|#m~t8?L0+5bTQfcnI8*`AK2ge(uSrIe6E!rUF*k>3X8WkKQ<`v zBaJcY{??bL4Bh*iAA_wmt<9c(N+(peJ^jk^RW?7meGh)5;E-Xz^Wy=t%h=!i$gGb& zz3s?)@#EumoVe3|%%gn7bAPbE@pphQ?pORVZv2NGvY+uk zg?khkHXHuj-ey1J@8!n-}xH^x@zBTi@|+{D)$@zF*h3gB$-E>}U2X z{#C!b_|p}^#eT)#-;I9}oKy7pYa9&%h3qbIxg?&pObR%Z)8AdvHzhl^X3qVX1#NeA z9!!`%(Zbhwbs0|HsQiO=Yv!6;?MAgX*K%qcz&U7THWI!zZhRfQ7uKef{I6p|j{amnENzNew=m4d{A;YY+%H1=oDciJ0sV3~!3DQp@gGo!wq;|v zn#!Pz=3Gvt`M$5~K`6r#uQ~b<1o5g9ti8~mrNQklcH+#rE}QRMGUxdz&y!;6;A<#+ z5w@4L@i7$Mhwv#H`0ij#H1P9bWuk!}UWXut(*O4X(cvEfs}U7F2&bZfe<_OflB1k* z^_y15U9zpH+sBpQOE0Ryt>-{M>8#ENH_j`kdkTd_< zVIN>QgZAmtI^^7%gVSCY?tpFJy;e~7G^%yBI{P3} zU#_MjcwseO=mJxKw-a^X{aPsXN$o($G<5*np9qe07b}^ino9T0JfJ^HJ$)!~Z6-KA ziJKze=60_v^$((3)4ec?$00(_PqRpWj!l2A8YK2i2=Jr>68=oX=jc2m-NH-g8zUn(jzuhU7S!0Fs2*| zUr=ZK800TRC&j!RCdu3dQ^SUY%(qVX8uD-UlWlM)NN*{DiBEN`uSSK=Zx~{Th|Cjy z1m`F6V-I-(F2>Z>v!?tQ`SYnyA}>Ou`KIDf*fHOKn$>6UFzY$a3M(RbQRn)AUfwTD zi*;Vp2;)VfH}5+ay~T+U`fDx)(~$8UQ%CZHi!h%hF;ULvn{Qn6`7^Qv+EbBCgz-<0cKku^@kcdqe1|-EVk5A?HGv59 z-|mUfInw&g+3w=s>#Za7cMXn0Bz%3`_-?8%z6kSqw)&s&#wgPGAAIeaPgNae(4JPv z_>T1$7D>hIzRX0TC&Nw8HT9(@624tux#rhuU-8Xx<9odhGiuK{gkP3D+WmKw>y_#r zzk376cdS=gBo(k}uWh}iRk-;3Joc}3J3}CJ^FzLw<+U~(^4c6Px5IzSIfHd<$=zFF zghR+0>t7>&bq8t2{|i*Ot%#exp#S?T%Js_o(#0P9vRRhO& z$jgIq^_fBh{>4Rd-r@G^_M?OH3MH>P2$sbhH2KvU%kxB*38w(pkPj0-et;Y39h19q z-}R~G-Uv(NBqv1v?e%^9_XwTc{PdAz!<8Rl@@=lXVGUjvS0eFa&1WutY}&$ud zIy~264gmDA_01yVJp3x;2e8UjJ}iy}foYVhBONm`zKcI`%{OIF z1o?Cg?wl`ft>s0gVtybUH!gdAo$t8U_QYKVU0vd8@vjzpZQt)7C?1`fgwf1CKSo_EQG-4q zY`kqIN)oc_fkphSOUU>((SQ!Y%!>_wlx5J*g;{`{E?u{$`N%)v1!#qMx zp({yfPopVkhmMXoFwDZU6!M@4rjVt+(YD0~!Eq!%^@Jz(N=a7Uk%1ED8>vqL-wJW< zB!*x)Rj1I-;MxGL;W)6StEs@MU&-WG*MLdvBHv`nA=7i%!AogI!V6=wF&ESa3M-o6 zQMjbSL8*bid6~@{)N zdo06`@7N3ok2Q?E1nmj9Xbp=ck^;UDQ4MK{aPbCII8>idB)Zo7Pu%MXWwGO_MR*}V zsWxD>T*Tn_&=Als@B%5!$wiIy4J`@yzEe-@jJZo zSRS8==Q}bT&&2!K0GFwM%hkVr)Gn0e_0|^~O>-eW+BnU;lB8UH2;XJ*lsV`ZS}~e8 z-(*|gCBP-u7iXfFBHm+kG(Ov1O2;`@Trt6Z{135C(exCqA}`ct2tV1m z5JBanqSQG8JViRG->_lgr*K#~p)9eezjHojmok?_<0;KDcEnuuFS364!Xo{pSO*Kv zQW|`-fCoP_$8LBBNI8Ds0K4O{{8S15c;8A%+=6miNqE9@J8$T<^rC|pTO=P1gKVzB zS3Kdk25*gK*tU%$mB?)=`a93+BTS3j%2vNuae`$Saip%8z+ehwSFjTh>0izUPdw|d zOz0(G1L#%wG4$?|pitB6anXCJrgsEPy`k6Vq*o|;jnY^tf`W2^>a>Hpg>1SyR91mR zUal*C`%V+N6N8gT{c95`5AF++_Y9y%)@_=+y)95YMLK|Z0%8ojO$8aX<5x3cfqLl= zi!DPqVuAAD@QFh5RRx~@Ez)mYkcRIeHDpW@M#umu`h5)3q)f~Uh`BBK4YjjIdItMg zyYXsKL#yc;P>j1^Y)#Ozs#d_#*xipq4^37KOn_n1_YlKUTVt$npyCOH1MiYXJUB3Z z4HY$vKQ#~F`UAxbM$e#x!pLN;_XLE&tegn~Abhkr68B07I4fkEGCTrFNIAm0h~nJcaMZGo(O zWVjYTY#B1QJ@q>L`PV-P4>3*9;SHt3>R;2J-@B3iBo8S$=u;ICAFM?LJdwvA(7^Fq z%J@4rSmWn3aQv^e3`L;3mic4k;^blW_!YGtmDDo-29DoS#{YmeNBE!9!12F^37tS)@z>S~#MxjKYYTjAkp^*x}hfu9UG`Zw_x zm^w=*@t6eMNy`Xf$3n~E)iQ}N=d)mXkSX@DV4*m0X(^0wt&AM0vA+#HWz5n!8Q$o(2AxpEF6nWKbYrj_uw?eNkitw3Rga#PT?!&&j z*=lG@9VefhO>Ow5NnAsBQIfFat$oKg2tuRXW=X<%`vG6oXF+_sSJj_B`#fDj0Oxwr zyB{RG&d32mDE!y5OROI}c?o%0jVQ1*QVin6Z{V9p1J5r*S@BbnW_NKQUJVx+4MV1L zJkI~X&ubx0`_uaclP3?U+oZv7%X<0BCs0sd7;Sw!ZBp}$lNVg2d3 z0#X+Z{=dZr)*t_oFbUD%e<4cxkNG+}{Ov!D4*rIZql4cqrnmn5xvReV233O>!S{UQ zv9%P9@QVxAR}8&s**BU{U!cZE{OeAW2Y2oSPrjUYAUwqI<+6(iP(8x$O z!=8^DzE32vv*EKM9d+}opg(S>EoN@j7uv64|J+6O2Ni)5`}?3EFh^dpEs+ij*5=Um z@*9P24Op&*i?NWeg%(Z5UUxf;0a%h%`;r;U5VC%?(!ax||I=?B^tb&aTKZeM=zm3& zLFmu87xW*!5A+**`%0Ad^M`HG>En1D;zeVBo~*Av8v2Jyh+KbtFWwp*J#7%zMMM9Q z5@^?-o+*%z`r%i35@MFCLYzxR>|*Pe;m@T}`iuUutFAx&Pe$pln?%W<=26l=J4$&N z9wq$QQNkY-CH&Ws46U(3q3gRSO8C2@gzp_C{83TD|59)Hr$6wix1xl9AWHZFQNo`Z zCH!$8)S|ptFi_)GHzh8fT>Uai*v0Tmk zH{G(Xz_zAbT?IZ^6}UKDu70Y+P({$kBkHxj!GJ@rUuapsdzkWG6~+8ye-mYV9X z2vWIv4`$6`m+-H9gn-W}pJaDQ!U-=q_tyGuzBkwM>RULkWBOyMFbuEO#|L@EC2->B z{BMa;e$I>%J|Rl@&k?_dpP3e*Q2Jkt0^T9MBeCpyJ+L?_A4VQCbi6a87?1o-h!Xzu zDEI#_MhQPAO8ASTgg+um`2R$)pXd5vxiYXTh$te?^)m2n>I={HS`+1Zje55hVnpA9 zH>f}_bUY9Yj**XiUC-Kz#}(8X+x;~^}lAceW9arIE^q%(EXhxOuK z6Qud`P9{FTY*q0Et&CK!bzKtoo0|{xl!KW96@15e-TUt#f93f{zH0&F`SM_QdF^2@ z_mKM7-KYK#?*N*7VU1_VdI>)JexzdrQ7v7C4et5FciR1*cv~|v;?Q;U@7sX>Vfeol zvJ#2^-@#b#EB~oP!zqv9xg424x$7f#atK)D{)4bnjWDRAzw3GcmIv#pm#AF5@(~b( z`qUcZ$-6u-NywDiL4^uGPishbf9NaJlPrdIwFq>JmwQp-m(s)QdplB})p>m{O5YB0 znSc88_Prf_Mh7qR2ay$D5Z^5jpWl*-jk^%=mTybN`TiA3Ts>4bjQoYCH=ExIH1y^@ z6NcXJ=iBto8TCI&udmnABut-LhiiE>H+QUv!7Vhx8Q!0vIpamfBX6bwXKFv-3$CAh zpkxv{8gpgtV)}rQF;3gCs(%EEcbd~z6oqz*G(_XNKr!UMY; zRs-5q-A6;`sDI;F3-_Cl7X1+pOW5l18=Mb6*Z~r4PW6D&>Tf$yj2gr&j2{{Q6r6e_VwjrPh|ZDACsij&2QL4 zgsR+RT=Ir>lOi06sk;w#xK}fH%dbF=L~XG@RvaR4NI@8Fp85!=xOJN5Kvlb{L-{qc zlH`fA;4tBu0!pnljdeO9hkzJ;BK_Fk*PP(Kw>#z6et)mwfpPm}NJgr-els}TM>_XXf?yuP)ZgqQ#OJyj`1`aYG9qK%-nj}Bi0YO65^5@%%xN@4?q$R$J$6=(_C zOFNehv7`YljCz}v^NXyMe{|_(@BHS!rMJ7XYhL%K{wF<%&X1 ztfi=(A11iWQXgS`8^Z3e@23s&+s5B2T0Ym0e%Lv;*gOU{MKzCl^5@`&((ksPUP^d6 z7uu&D<6%mTP$P8zOUR|K%cRU_`jVEX!P;mXYjNgKN-2wYXb}$Id28JG;%$8S4Z^ns zo*y*W^ab%9(;$3{pczhl={9{I%Hc?~^F4F58()fzZ$g9cjYeSLq%Vl?vHBF^`>@`lWJ0eXe!-!paMv;8?#Q`D#o z8GJmLv0tci1_~?VE1!b$SjGvzZvDnhGz_j76cAmGiIAZ2ra&L(4-ml5IyhZ~i8uzW z#@lE0A!wTVfa;#a69%7JY0Q3B%}CNggd^J!dU9%~C&NL{Nzv0I@f=@8>|#w8S%aD5 zS3kazZB73}N}HMf8!)@{dXZ1ZBKcz++#vK?)&{EXcqDHeittXE_>nIiQZsN~WbVMi zGMOi}bzlqHf@N7b+#oMe)--u&N)Cb4Sr7<{%rA2OD-ki)y+|HjH|w99iN#D~{Dqc2 zTy;zwOK#?zuD)7k%{ibpVjot@SLUBGojVaZ_c!07uGHQwQwQ;x9Ay$GU_J2k<}g10 zEZY`7|2zyr#p4$v!n`sSZ_qB8qZfG-dC1s41E6b;@2Ce~H5|vYH2b zHmfsJ`C5StVJ|W@_{@_v&Ex<{l0uNfS>_XU7Wb1#SO5&m(n6B17Jy{y>HbHswGoDS z9<~|A$JR16w=y)hGB{3w&8jwJ)hn`eWL1uHjt+IIGyC7>2i}8>6X`nnG3&J;KPIm< z{Mb3o#gAvT+Yir=Sn}g(I7Y$T5!1uk9g5O88ZXfY!~g zIPzK z=uP2?ITi8I&?spM}{Gm~sH6TEfD%p`BBb0$w%rO=r? z%R}S(%_KM{XD-NZT)!*a0Sg#+!Q7^vK<72&R|*cHV6_}?g^}`TUOluf<>Ay8vrP$f z-lR^#x`LXtTCTbU$GeJiNzr%McP_L1e`vema1g*rJn~NX!X1%KQN)lFq7T!FcuBoU zzfGc0hJ5)Y*n|Z(y5V=lKQK%>{t2i@pdDgK{i?SWJN`Ds<@y4U2_Oyzj?PeM@{Q1? zxgrs6T?(0My^2nLkiYwje+>vFA=K1si+`^8aTpAh6+d1Lsgd}xtk}i0BlI@fHr<#` zFn;{tdN6H-*ztX0+W&|66(Vmge)(Sv^6U6l48Kb8%!b*ZFy8*>1rf6F{7M}Hew_oS zvd{6AafbDZf4XyDwcq2POUVE#DvYqLs8HIvsqv{7VfX7BDhr34_N8#gexav} zgPtS)C+HEw6q!GB<_7swB4K|0RLSti*ptYv+umh$;X29M4GZpboQD^BvrdnLpan|e z^w|GAXHn@_-@}^nm4d0(!OgR`KrfDAR2r=_vE~22P5)UA`i~2xzZrMWgQ4>cpx=2t z+bN&mqK{n6QIBB1P)e0S*<%GpmOMzj9bvzD@3R)q)71#rj8eVdJQe~|UnO$uPh=+7 zBfi@r79Z09>6}JTS`hW(Fu*A-o1d{L52$7E8bY~W-Hb($P$jw0<$B+sV~G^s&FhE7 zDaYXZ=z8n0C&Bgl^9d5%fc5$WmRLt+>1y?SGi#r^L$dW#>ixK0U94%;W4*jKf8oK&qq3gRhAu0o}uGAlfl1O%05YbuY^RHh1*eJ>56vVc^A=OQIAknvb)*Ri1O z@4#!?%;cy44|~@FC--#yXS2J>5&XicDZzS_(GaVl-O6sV zC1lqmBqq}?jiN76RI93KtC6It8&pQ?5!9pF{zd&;cTK8?#W}N=Jn)>uo`=-QM+O9o3;#utI61s=?L0nxGH(y zfWGp5cDGKz45TbblXPD?{Jy%JpgjEETi-LSK>whN+3VSE^I$1-2$u^d(>4KUM^p-e z?mN8S8&ke_czOIO3<=vC_l}SDY;TLQ^}7H?BFnvP_}`prdi-1cpozaeAw|8tg$*B| zxtOW3p!o=@-(g5AP8OqYP^w0xbAVxUvRwrn&D@-@9eiKa7Y5_Q>D1rs@f1#i7 z%dhJT{2CGrbME$V_1wNKZ3urk__^hW45&OvN7k`jA!L)N+70%b_L%IblX+LvNGeoY z`_W4f=|!(P_!WeIA9y1V{ykJMTL*q?A^h*46l@*%BMZSlwmVzP$SAyqTXU&XYqOyZQ!5~!SQ$~npaxCkUPr+M(Zgjl3iW<7HE*JX=f_oT zPW^WO)(2qUoj^;=Ec}W>q5&yd7*oBp`s~+D4`jB}T+hAQ(j#KUVkt>vLA%W|e=5OZ zE;9Hs_@I1^al84M)33h_(l#?#W?ee0egzB*juZa!(0|T;#?zq|^7X$ze;+z&asxZb zc}KhoQx4NpUqOoLsZmFwr~Zj~L&=`<*I#4E4U?AZN@A{HHI;ji^lgCn98qh&A=+3B zbBlm;VCMu`3~od_2_=N7>a?+{wYaZKd;y1FKsuv;ulZ&n!kJP9^3VEeulfD{6jTy- zgdMXN;*N&YsRr=5Ry{kA+|7)|MOBFH$_&?8c4mNR#>un~0Zm&yMi`4CX?3?Jw!UFi zSnLspCvINIl~UHZF3MelPQWo->eM3E0>dThH(cr*!zIdkp1A?JdOi>kME^?IL>m3W zJ?I|)sUVuhKGKRRYKEfS4FA!2|64aF$Xz&f7(274oCI%LZ` z_t&Aa7&-`bq!kH4sTO_G*1P|E`FGHGWbTvyJIYmBh%xQbS2ztwg;uv?y;dR~zfxa& z(aS``E-!@Px-Qv*Z$lAaBa}S=Zgv1(UJ{@qAEy0Ul@ZvP+F8NRQ}F393-$lxYju;+ zQ$VDVx7F*eqTNRJ8Vq9VD8nvaUqb3jCQt7<#T7|CHz1ItAe0stT z6mQ>UV#<@pRVufAhj7-aWxa6wu4SJ~YfSszf{|9FeM8Vg+LDgoAT(q+Z)zbeyt->( zEwo#ze}z9lyQ;nD5??w#_Z}5Z4|h!S=yzj3JNi$;x$_OTq^{3XUe4Rn2NICW-KVy> zo{s~Yz21Zl@GRK$7GP++31=VS@g{(*@TKsr37C*PhQ6Rvx1u*Y(WZja-@=g*8E5SK zN&|!lTK9I}yT3gE0w0e$oc0Eo@Q3-9c)|QHFVp$I(#QOFVnibR>3z(9G6!|!Kj|vD z@84rYjQ<)^61~hJbNdsW79%6a;ZTQ$l_Ax*k3cENHJn@UrnFRF-6>;-EZ2~VBWdK6{n^j;#Sd3sw5U{!W>MIQan{!(0C*Vp9)a-{G0?Oehv`H+Dw7Q&g zZVv@>VfraJZfP$RIE6R4$L20O6?v_`LUNn%>yK5*;^1Q(D)>ovrPns6EHu7YCZm3Ne= zAu^+@R}NjAa}SkGl|uXiRz+Ra_nMtc5p@wq%YxPU_D8w(-8*#FlEM{w@pC4DH(~G; zrc-TK>o4OP&i8TTl{c%1_3Ez}pGf6?pYM*a>Uz}%3!pFMjek#g07cX4)=t~{2(i4x z{V(F6o@z1`nL*gVR+x6|gWT}|@ScD)6fIF8|NGSb#cJ^8hYnLqL25CT+As-a#Ep>D z%+Smb_|z|&iBYf^Aj2>|{Uq9`UUfnxEb6{|n2_cTzI!E%7^zibdFka(}3WuT#yfM1me^o92@(O-zJPlx$S)u z1B&{IEff7~?@nLIwWw@K*S`*ZBI>&KvH&GL$^{6hYy{1_H>VkM2y?W`9kIGBI!_*F zx@>zfjjlS+F^!Ho#A6zj)^ASzkRP(&4Ptlqy+3>A_1?Y$_Jh&ZOSDG-nq^vTQ1ceP zYT(GxGuOnF+Q8gXnNU-GZ-4{NZVUuXU=NN%d(48Zv0K{?Ru*1_1ac1I-{7FkS~qph zFoMK7S5|HT8qJ$a0B3YBOzZC=U#L)fq|-b`)y{2t77lAN&$231p3RC{Pj2x0_n&Sp z4#h>u^dG5&!7qQzEtT%mAAE2L)_4VWogE8qu++X+K!`ZN0=@+d8EHNF65GnT`EJzy z7-`dn0y06bBl8~p0;LZHL4sb*@B-!$$mUE6Zl?+a0_6Oi&+>Bq%fp28m!9iz{`B!K z=L14aPb(&SAUt=b7sCH)2*$E zRiXP(=(EJCDb;GX|4Mxo;FstsGBd60_$7Q5tH;yAf1crO;gML2$Hb{Q?$BuOgIyY- zg>jHpzp};Qhb=Zf&~ZA1$toXF!4N7ALfn8(^H?&af4lY^T{?{Ua5n)vjc^ ze96&8O7U|a)4l6A>94R&xW=SVodxJ0wq`?KA#Ur*AN6XF-u#0F%G>;nwRYiBz3@MK zw#(_|zFxcsE_L+crqi8XJT2n(qO*gvVKp;i;eR?yr$Psk*;7?#0&X^R{y_y7{+-(^ z&X74%30ZYI&GXUc_9e0F-InCN&=gTCGKQJGdeR$=ZlHHgIcg^RII%hadio2zVh%-t2f(>$u*E$>59)k9_i!!!SP?g zwE@$_nZNwuQc|?k{ACsg&{@u|D4HGN>U-hR?DL5+KZy#+6ObaOIGei92ji>na1L8Q zVY%gPoZ{Rr6Ine1B4oD9fK6I(-njvA7DGbl0*T?{vk0%nT zvV-Wm#+c>sK52$mDSTVM;`w7&DSTl+S1CXz%#aaSYB_zL?CzUkZyVV>4g@-OvDvs5 z&o_$vB=1*9>=nP!1aijIS-?T$Pn}C_U^l#8y*8L!3kuPADkjeQeJ~%qFbdbJ7eRgw zz2=sJh`ONgsOaZ^NF>jJigW+_n&29#&mhpH`KWjGmkSSSP2kn3^JNMx`(?uF7gRS| z<#FA9gFVTNma=?|{G(+x1F~2D)62u3o~pdMtK2rlC47z>_TZ1VfXecCGkTi;=v#lo zj(!Ds$#zRH%Yw87#bnINOpEg_I88dH1OsgwnG1%r+h-%(KT`b1jA|5ey`Rn~3KQ=$+GwdW3^iDKUNJo80@3^;r7zCIT1*(CLBNqc;%u7yKW)H^g;$)&X& z(hZz`j^hxFzOd9NT2;WX$x4~KIlhfPKt!B9Cr60z>+Aq&V%~7W>Och_N=B{;PpuB@ zh*9nA3-F{e1`iGOWLO?do*@>Zu30_HZkI}%g#`Ac7@PH`l^@lD4zG$X^lXdn*BIi> z?2gw?Pn+>R*B|i6d42H*2s|H=(WxB&$qb(Dve+eFRe^5DeBqHK*ONn6HqorEdhisq z@9(qO`ISFm3dkia-mLaTeqP#0BOsZEN8K|1sd+4le09)$a~i$F_-%R^|J!Gsxy!9% z-C-P#E)cq2Ex$;Uyk1p_tw}54sq-Bxq1gj|q!0WZ8vNBR_-P*SR7G*UGPqt>)P;W2 z-d0YW7kl8|cf1$vA85D-xo|h`>7uP+y$tTakoqV{H)L{)mnyP!$^3m)KMzNsdVHq#VMeqM^XCW>4%PtX*J7vEZYZ5@~Pr7L`M7gg6rRp5Ejs;Du?F0 z*^C5X^-bgR0yERg1%&HV`aa_*A?R|xIvWGOe>BmqI3Z9Zs9~|pt z@_RIsx18WG`2l-)#Fv1`JM8zluw0?VLTco>KJR-ruKn7@=J8$~?)~<*N4Nm{)?|BE zMD200RuEyYS<_4Yy)Wrs{y`Z0set+lzpABw`A>ro^auW3Mj`s}DYU+6MDb|~8I*;L ze_{GJt$DIn&eEcKJn4~fw-H+U@P@2($Bd6Bsy=0E?PJHucqx;3rzbS0+4~#JXbe`V zrK5wR9BPDW=6o2{%BFVeh;B-LqIYk%|GX0b)OdNpqaT<=404lg{bADc*K|!Qil!Iv zw~K)g_FG}t3Xu(s-W6Lzb%$_k#E0jb_EQqWT2a$_f)!^rAl79=zu%>T2G%s@k%vd> z+NvsTI@yb{^_x6f3vdmcV$8bH!?=zmLn4x+`Lw~=%AcjQ9X<8kN44RB9biNnw9 zgip40B5bKM@ntPpSbHNAFi!!@+aNqR_*sGpocdfQ-TYcRKIFhPQ8(qI?`Fnd+6 z3>|=I4tbVZ0BOS*U16YE>RcbR-8HlyXlVCC{i49srA+#5p(@SJC0O57t&-nK8&F!9O< znPn1TUZP<#%D_Cx|pe2lLN!rR@)CnBOGK-y#ZaXOw|?vw;~zl;U`pI$r+M4HM8iyL zn8y+3Tn&>^2IlbwW|%NDFv9`!4g*u-zT$&ov?f3G;D8q3w(^FmE+5V}$uMt+-U^*9NAM zoeyn52GPRKD2kzxfzRmpZ5H9uzDrfzba{C|-9oy+lE8F#EpkD} ziiZ@qMb+ap$mGl}XM4?xSld!}VgTxgj?j`kx(LbMoQJ8*VEP zlyMWkG}A&3`Ghgh|B=Z%Uj#fD0RYD9w)C=4oxn4@(!|xcgTf^>Ud3G<0z&XZ~9;&fIoMTa#V$nd` z{;ZOCDGP#cxPv$N*|!fsgCVtNC`8|yiSB7waU`v9Fh`Sm6Jn~rLg#7RmLspzh)ZZs zEtGN6iJL1X*TUKr%$U~*_P+=x5D`!|pK@GfLs-miNTR$ffayb2riqt=OKiD*_Aswp zkE}vXsnB#BZZdK`xW<)hTD48YPH1pH$evFFY3q{Dp@m3eKF36@sDTE4ki#`hQ~+7U zL|mEoF3HjPP&a}`U)oY=4tnrv-yl*Q=HMO+kfRQ9(0Cv=Dj{a5yG|VecEnb{it*i< zYDWNY;UdRM+ghI93RLP1Dv1&&?F!5fBA|VYO2~p=sNqA>lIL7vfmYcig5}O&HuTnZ zaxK6v%qvfEpak++2{lLxWFF&G0g-f=i}u~ij10YsPo8`_<4dvi_k(H9XilLn*Xpw_ z8>`{ewe+O0B>eeigaPPn0{_n>%QnQnmE5`w;lJ;lC2j-ypJWdJTS)&f%SW&V7liv~ zu(}g+*(Fh66Q9Sz&w>tr zQlVvs@TY3}q1?IjyP?j`r9LSrKRY_~;I4s)y8V<~7hM7Wmg$Klp#097+}+U8Bs~Pe z(rcU%y*p?wb_G_{%6=9cAjru6PV`~X)neXXL1zZ^i^smyBO3_X9B3Odp}S`!Ee7c- zc}8y>$28fSI}RV*n>!BW%~8tTvY~5(6@hhuQAo;$uE+N(e)c?pKLiptM~0i_5|`@4 zEr_RUu1Wi+W9o+KoG0m=!_XxWr@9ETvbqSWt3`&|K-Hwuqp!{hFksgaQoP)>rDt=~m7u++Wh+s3v!Z^pmn0m!3!;wD_HkvC! zUi(q*6C~Gr_VTcNTy>snuWOBwlfATEqD*R58=>z-u{HO+1r&g~7}R>{4Tto9gf&uN zKCyFt?Zg>}2Lf$-%E?$lz%f`txYLF3Gd5x@8aeUO)?ML5*=`{?Su)jt^Mph_Uqv1? zuU0nRrL&=L4%S{odkqMuUqa6&>~hb~Fy%6*lO8yS)X1v?LIx{V1+O5A-0Bp$-6^sU z>#om+jzkqa(mb_{Xyd#W(SGwRyWx%{^lNO1R0q-mE9y5a#=l`6c8amzEaEA|tyj@F zVVT48mKw{TfA}a2uXE#atV~md#a2@q9Aj^3?FuF&EARP?GZW}Sj9%sfZQ^PU3_Q;j?o=V!n{zt}a=;!YX@|U4{Vl4>n?$rLg zsYs%q`HZe;gNc;dPcyYEM50C(uxpomxwW5ZYG?jz=n!4|2Ti(mp~TIcic8yHHx)_r zX{PqgCQ@qu!$}$?h!tyR0lRj|ms|T^u?v80XZ~#H-uI;K3s^fOAa2dX`;Mt;6>!%q ztc8js!KiMRN+2700v#>fwU|9NR4l@UwU}E8|Hv;2j+XE;{VP0rn9ZY7?qD#W!{}!T zLTU8|9Y-EU&ysb)=&MkZX7o2O0g%c!v-0hIp&LmuR2^p3=tY)N`w%1|>lo=}bjbzE z^o7xNVZ-S2-W7VCcp7m6_IwE=pQ!f%IghAg7p5M)say|qPJMU?k&le z+glgR(7na{+0am3`;(`V4yZnjSoI3@v+R7RUic$$xD)>*xrMWKur!!LeG0@7>Pcvd zgR})8PNuhx*Z9Ruq#!-l=!+&q0%;bok(PY9NI!70MwW(UqK8`~yiETJ$E~y3L(2VHHy*0>XO~f{ zzIP^xv#hpT%B9tHkoTWm&Xkv!Lj5X2M*VszbiZK0C{@mMkzm&a+sb(lyY z+~G!9Ek-1eWdWOTk}sEV*K!jE`jz>!pXn6qZZlQLlEf!jguXkaycJyq(kfBZl=V_PI1{5Iuucq$re2ApYcg12o9;oS6dEeYJOik}S zGs;;&be7qT6Okowga+GY_Kh+)U=mWjGO)NIs124RjoT}HDMN+amZY5LOBtr`j*^t) zd?~}%XqpjP#FsK+O(k0?Wk&f@s@4p4Qr;IuY~!}wnjucgUwkPtFU8cy;{fp6-%(xD zKPj;wPYkoT9dWUynKj2`8H3`giGpjLDDRR{?x(L=wmTF+J}n@ z!5hAMcp%WB5~A+l*Vg3ZEg-yPA|Jz?33*O%vDdu|nc^&c>Cw8M(OYRXQuGX3@8>%k zj7+8Tfkai?G!KwF!N$_pg@M?>@LOPNE?Cv8XMl*{mke;Ni7fOP%S4fb%%jjHYRKhB zdrF}BASJtH(rS_s)u>t4D%A5DX)inwsU~*>DN3IwILi>Sg2+Ds#rsAdZ+%#QA|2ph z!!RN2g+-@E?1RQ(8O#U4Zq+Nz_MfQw;Z##a@wTu<1@I-LC?VBr_81F&T@={@<#k{E zoi-WSi~1M+2Znk$TBbjI8coyH{#&rUN6T!1axTpuBjz5OOH2`G*4c1{D)nIR65(Op z&W$n}i{Fl(9?$5$0YR5jmen%MfwNvccNB>spX*?q854FRhHMVQlF!y|OLee3Ef>u| z!L@IK{(-137*4ak888Ex14|ccU@`3mYbG7B{fHaSOIEi%%I)0^gCMavTD_2M%~Y{` zlL6KwO0#;=N~BPMHU+A`dMCe2LcRg>H6qbcI_slvl#@N%$xrR4joV zRj*uDtE7}RU$UWl&?F>wv10v$5EmuY3Me*w)4~E~i~?p1sacp79r9kL>gfp1$hqB_+$|CiPt!jvWueY z<68PoHl|G6@uppS9?Z=RG_+xFc0NRgKj%U5cgZ56Z{%*#Mtu$Pc*MQ~K|-IXF4fZs zcWoE)M#(|_bwoz&ZSo=4uDeid8X_07IDR|uo8(P$`qN4V)uzLN7KFYDNv4XrsHj!$ z*MFPSlzoVK!r2&ZE5t4Q_3G0;E4-y59I6t`;k0-I5ihFRzVmnV@1pK2&T)FGXx2-Z zbu#F9#rV>hk7nlmNpujqMIY^K5gMX9S{R8T(bdnEI`jympDpz%SPR0zZ8cuSaebe3 z_3t-&mFLr_Qp^#B-Ii*c2Q2${;BgKYiTHoG)V-+z_pYrQCj28pf! zpK?m}&xU483+1y+=?p4H2{UC6;O^V z!AnRfpd2rP1yYcz`+};ylM?{RGFmzED;sJ823>tns%hlT(~s}LA?wqR&D3C)nqZ82 zm<*fsV&fZpKcidBGFLx(^@G@#GVQ@30j}Quqt%_p?7x$D8nb|YhE(V^T=ik5F=s+j z?P-iQ(>jc`9(9ds9J%4vkU=Z#>*(zL<8pr~r_E6s~_;VR10i-VE!;v-RL0EXpo>KJ`sf{Y9xL+%SGZ#J|r&DNKAE+xEb=1 zhXmjlB!Vsymyy5iqg_G`?)}u%h#5p;q%{yhXOkM^J&62mq?a0VG$N}%?=aD*LWls4 zL1Y1^O`00>M9{7daOMsLoQ4<;X}#n;TF1x3P*!5^&x$dx4zw_KErmf_NPh-7`$NrOigZC9mXxv zXq!}jE6mI;uw>jnqx8#XjK$()w=loJ!usVtaJ~*{-nT>I(Wz&1;_8=2VlYpc>lbgI zs7LlFbE2?rF9Q)LFdH)spv|Q6w7Dy@o}!%aZX+$QKTA-PvlbEFWR59dHgrG_RorOV zmxpVm)_f?P!U@&l{bV`Z+ZEXktJRrLL#OI<+Sc2xn7qQq zh|U?YdKAWcRRgt>k?Z&Bt?Hk!?}-;$BdgqDKh1pz$0F9ZHJBQij~fcsTb=}QF8t28nj$}hnilq1bxWAKbUDypPsF+iHGX* zU9HlxV(Y;x09kKosl}JaM($<;Mt%OOSu|^P)il~M@Jv+qg49+Dx`+wjE#rVjK#nL` zdcKaVP3ojc!t?iusUNUcf|g0pspDMM;XtviZ1IEf)_mLIFxycdf!jm8L&kImrt696 zPu=(*Ak5WsxJ>NA%W9V+l2tNMbqWdTo#fF@?0Q{{SpKt8#@&4f#x`*SG^yJrJD;D| zDugEe$kQ>{o=l%u=R>5W&0!c`g0huYHGTTStZWJ$2Ih#8h?%|AQHaF;D{A;!N3pU8 zWD5f`Au~s=T)(8vrUm3B&VFf8$2R~OLSV36O)){ZD78XU`*dYUZZ_7%e{aa_B(f<>bw zkVqr)@Wyc7gl*C+7Li(?qReyI0$V=o_BWktVWpTLViainF?r90qGL2B80Ni*usX|7 zvE9Cd4ypypzYM)%J3q`X!qlRK*3I@FfZFUSrMGZ-TDwb?pk~xNHFcMiwwEc<)9MKE zpv*>FYuKml#!hi+lJ*BU59K_oZLCrHG7Fb96qD`_s!>$pnKQjPu!loxG&$aqu?qRy ziM2GvQ!|@v3U?dA1Gw%NwVFjUGUhas!9Y@Rh8nt#rFQVZG8EXXXKla4UJ5k~Gg0l; zS`M7N!efsoHR~$uVIr(AO&gl5z=4yBC3y7^l9Jdk2;+*58|1_VaqpFm3p#NXPFxM+ zDtg`i>hQh&;jwSPb6!k!j#B9b2=LB>ePGe1nS^1(m{(%3!o8NKe}VRUCT&Tn-T00mUz@ z51=u|0o7}{70B&zKrxsNUHYtXKuJ;bx*P?nS3et1oyDN-fND7rs_e+aY*p5c;&}?f z=!e9{xc0cT4O>M=NYCTQOlU0-iNt2B)kNQGN8^E^2)=b588ETTdM3tuAjD<*NT51( z71A44{V9xGsnCPm&nSX;oUwW=?4GEchBKO*VT7Yt3}-YihG$;47)qONwf!)2cF$=3 z1=rS4ZeT=L4#Qr4eurh9|IfOX&z<7YoM7{r-HLQ1lX*{&)K-3 zN&OZ*s`ps8JfK1p#_yNdtx|V!)D?Ex1GjfI}^uK96ZDiS<>=O$4ZTFo^-)v zVDoQ~up3lW4~sCR)jv2*(0piiY29T~o-rvFwxiF0{cWbhZDMU2qbfzl9yfs?%!C** zb!rzAv)uGw7k`ww>yxN1p10rCY<2=tbL1%OBCdo}QnX?(s@WW-~hc)zn z$tvLr)bC}9X^UZ5$|D=xDS&{Ut#h=qs#jvtt5#C=-rK!)Lk$^#2~A9Z>slv(4&mWx zJmQ-uG)XVq-&A*E;nV;fN8af>L_=EC6bhcO8-nfX022hwjR=1q{B3S)myQUlt7u-6 zQH5>u`=x_)7vm`NMOQe@auaH)S3d%A?e_^yKM1>HVFzGaA%nERFMtrseq9!M2h!S= zf%}7j?f1|~o8jPhB7)xGpc7Y|sR)@2Juv{+fNgU2`kO0nrNXv7E$6KQt)H?|q#;EM z&5f!rfvkEqS*OPBgYTgF#qOfU3UxLx%dux|CmxtaJMT8o)g)aD_;EFzMC-Hjt`G1# z{P6DuvRf5?*bjf{zWs#n*S}C1!m=1hG4F8M7ZX?>1nlQrq!43l?fO1O@cb9K79!48 zi1W}x>hpwQ)kLW@V(K%-R}@P|TWsEMQs)?7f@rWNHD)(08IfsJG_((pU~a>XsdwtM z)J46@8~A(b`?s_=N8ZILsUF#xQUSv?J%|K|Qwix-@(XPPQD9}#B*((hAJ)rkN?Wa$ z4}(cE5tAgm76KCRXy8QrPK3ggrhD28`bH=-@LE%dU>1q;kE&a#V+)Q(`5oRDc>C;9 z9G=myL`5`bl5afHlg&3C9gL2nd07ynzLme51nsRn$e`C-@>|$@jH%x#|8ES7|3vO< z-s!-CXPMa&NzcNjF>)Z`3ySCOz4iT1b^0r;t`S>(Jvlt0$}u>ihr&7HnL8e8r@fdy zLBHt}TT@OG1*Qa!U+(h==9p9gajF1V`_v*2fepJ%hR-pBHu#UjiE{M^&S{K@^x7y7 z{-5>Yf7ITE@%QBaAyklYEl1RjMe_Grd4WU+eiU*)4ipHi4&#sdVF;CpTyw${XdNS? zIAn{dkS$OotYXI4_rBzcEyC*EGVZ~|?qK}E$G&nw&AuLOdg3UV)r`}#8vC)8ez?7z zmiYCDJ)VoY8w$7l9ND5TOG?4aFg?PfRT$g(X9)-WtuV+t*!2Iy(BF~&{zK@!L@D@A z|3iU?j@%C}1rew8j^s7y|3Wg=dyW274}1X!1NOpQJ*-}6ej+EKtV&8FcyIM_Q0LAZ zWod1k3CYT@?*NCGzA7jpS<&Ze$Y_Wt7di0buM&pW0l(@_+7PhU zQHZlv^*Nfq$h4f9Y)BKtlmPG+MlA<2Dg$|7z8@Ii@{8~a1DwC=1Q)nmRu(Y8d7#RH z^ejd?C@C}->P$fLH|5Tve}J%R7^&;)Ht_kY&NWD+C32yhnhI9$Wnl4tyda!y?g%$e%sscR%nc*gYZ*G2OJjzEv zBP8&YC37KxF-T~{4~e^hzX%7VqGh$Loy28WUAKeABFOeI62IzarVnc*632`A3kWd- zF-%AZM+KssSy(Co)os9ymXbJt%wIIlVqKSTU_r64L;@^})1!KiMA@;(n`PmozY z4aP?m%)*x<@p%>@+iu?r55|COiJw5Oo{F$~oC93IzU_R-LeSlu;_komk0dc_Klp^{ zRZ6R!P*l6WXZhko+mTq)^x|ifCb#i}TB0I|KT;#WkX|Bu!U|tl+hO9dnH%mEni(xx z*B}_>wepz}a1DR+&9Z=6S&Ir1o()ia!}kK%tA+4AzbYSJbP;WM;MHaD8W_H>ZUVl8 zbMY;t-)V&T>3c8qw#N57AANr@A|GEF%x!c@U4Dxbj`TI;#ivYjic)t9v27xgc&Y1~ z&$;tQt=u@tK@Y`L6^pSlPM{DgJ5sH;-(YH;aedKeHFpS;>oSKH$QQO3kEH>0Q=vp; zj+AK1RI(t)-rVUX?C?Hop`J!Rpm<5ZH!Q`;fh$TS29W zdXGBDG}3Fd7ueqiDV$&19%$R1`CH8XyPogL=mYt|yMzyv2V|I!B13fThC%3?X*x6g z^{B6jh?6T1BK$yM)(#OBffUgd5sgwr+b28)&D()|+0dsj zS1`M#3h#?0#uhf1Jto=fkv*~b(&|OB)R$K;BI=M&5DD{xY9X)ApRbl#dav!e8;b>7 z(ykLxhiO;gZMM2yt$RDfZCKR-gr!%7XhC%oRE(M*rn24|KGr;b5_zd%r`D?m+GOY( zK5*b=qFkojis4@I`EXbsJ_Y>;+>>JtDS(EW*hjyBguL7H9COM>VoD5Htfq;*NjWEAs4tRLweV%`O{ocbY~VK6vfcB|ap z-=6!x=%w3}OE&L(hSEB}0o@S<2CeLDe)&KhGMbhQJF`)8XfYaqq9Z-2rCv^Ey z11k>%O#3m_>3{n1K;;86&*EV6ER^X0+7ARw`v+DY251G>REe4J+qZ+ zF9ts;E~Qev`{<*d@*D?&+h~0yZ1Ew{M}pk(_pB1_^5z%QHmyST)Cl)ipl|#PnP*QD zOOg=NcuaNN0Nxg`Lt$)ULLM2`z4ejqU4ymBigQg-j%B98bx{sXPGGbfNcb&nk3AsM zQU`vN6~=hrtg`2O22{*GEg5z5bc>U?RR$wJ_lE$=y z^d}G5{pl{fqcp-O`~m7{93F}k>?AT=x0?m)*Bn0G>d4y0GFoNph z4?d8om=r>c=LCRk6q2ftop$(DE<}WzNsZZ$|BZ*KY19eSp! z!UgIpRsPubT=K6NK>a6p@LzvG|M72yf5GP0mOj*<+n)Z09r{IBD_ujvg;)L49)3je zVuW%$3t<%i%M1S$@Hh3h{w>Y#;E#clrU*t7_ZBs{uiiGON($P@^@f%JX}q3JtUXma z%a*R!+M?Yy4PevQ`?JgzVhtY~-sE&v^}@ww6r}aj%4)*`JR2JHeHbTj7Gk3_D$9nR zx?NjRb-YACVxG5SOjt3DBzvs`z%-`W&|{L6R-Y$ZAwK7TV^}S!gf*e>$L6Ytz;8w& zvmH&sD{j-~Qp~oGgjCX$52Xk#q$0+e$NmC+^X+$i)2BRmiT822+q!dO2Yo2A)u0JP zGFuI*@5QkJajsO>ZGv3rtp+WKfln~9oBo|h+roC}<9CV)NBL`|^qY;?G(#`Vn|Ds= zo`u!WY53M($KcCKEv)4QA6UIaK6<0xyt83NxpOD$$nTX07)BQ4!EU{B6A`u}Hr>Mh z!g46GRNsqLJp6+46Zw6k{;ucua(vtMa|br_ui1-Gz*jI*rvi>y%;q9I{?m)u4DcTp zy!mY}vyR^mj^&rook>|`C1tqEx-$*I&WA`-S`H4sgWW*4hE;U{B^wXbhl;N}GS*`S z6}4y8Td$Mx?NcBewh`z6G*(eZzG&xHn7bQWXrwmN`5cFFnhm{nhb*4xmz`vb<1}ZB zBOTs+n`auZi202j2)m1aHq^3Q^L@lUSX|Ox$Xm}sO(Iv0eFb#Gd-!_>(>#!>s^Rt{duo)bYLH+F z^T7*GI7v_KUbhvam$!&x0s9|}#F2z6_psn&^Ug8hw7PZ^SPtwVyo(&rPMAuMCUeD{ zm5s1EbTep-^DcD(mP87A)J$WrFag#~tE=p8h-UVuUchEJccUsf)^hjA34o~D9Z#DpHcHvcFERD9-A)WMatDY z9wH5E48fRcAC=_#c^F+AXjsjR=i;AIN;)a77Uj9SmrW)KU6!$6FMvC za9U4NTWT;YwXSU~!Cw8x@uGaAuGAiw{Pc~Y9arG-TiJJ|j^~If>J5a$9^X;aXesFx7=}@x7`a{K58J-B12jF6$J;Iv1bkN^nqa{v-OC|Mb80 z-alUW!rywn|CZ6Md1758f1p^iz4=Mc)T_itB*3^9ZuyrN!`%GGiD-E9f7zG6Ad$Bq zGPnM7-V%I?`dR;pte%VS4>f&>?=xa6=I8^RzpvI#J|T1KD=)6Pq;PKj^TgMfgOsNl zZ+^{>u!hrW8T?QG5o#tMKj`zcY61WIxcbYl{f8jzp)Ef>^97i%?)vawz$}omk8r17 z4q_(gx2GbqAu>2eo18&&+bB)WD4xP{XCr%Xmq{vAjSR-*Ty{f2lam=?a?T>8RA}}C zVsb`tooY7py>IwT&bEc@Pm6YyePL#z50k}Q`~f41sjn zDeai|gkw5C3akp4&di8*JdTWL$E_p=)-wP%8+ze-UpuN7iRrBIz!d>(Q(qe5^oKa{ zP#k=YS(4#@JT;CRR4 z7+Ui<0)#L1BaBtdz(8i&gr~5%uo3gpE*>m1u?Kp-$9ld;dA^5b;Ox=)oihu2zmM&k z%jj-^Hx18YqXw-5O?#a7<#MG?Uxl`q0y!B>de}}k;Q%6}f>Q8vz`6b~kb4?D0bGV3 zio3voa3C|pTnbjbvZ8;tc;(jOh7Ep+jRUOD-#$u9jaA+d9QXj^q9$J+_*)I8w+at@ z;cssmzqd3!7v8@+Y}(_j%2(b9#^MNeGYkhkg4t2o&^b##f*HlOrHFsLlgYKRFjzyl z>Sg7owPsDbkboxB(K2cy4FivVN^8w*cppbkdcL{ggDf$&5Sob5dYNOT#y#oyk(B2}}X>+28iwUbMe%9}v7z zU_oztUHncd3D54At@WF$f6OHChL@pzazsw5l=FD9$m8J*SRcc{!s6+^4MVfn`d~t` z@|kB~bLb_-RftI(e$>BU8@1NLE>H*`Ryh&jOF{^4KrmU=iTr!vk|&(bsqEPiAgz?^ zo(|GIjWd(e@WFquS6E7|`3sPSVPhelzI6TuT0c!kHa}sX-Hkis)?f!Mz|oQYzkm9G z_x}+Nvhyd4>TaXCGm#I^5GqP7_LsW5V5u*n6fLLMQ3mgmLQ^9^kf~|GGE@@7r_@P=IBUtspaTtc ztpE;A-k1KtfkY$5-VQgW&~`LyZ8`b`36eXBx8Bq0X>ev@bJkP&Yl!LUxfX=9Qdnzc zBV;8fh(6J41nV`C5`eZwuPk68nT@>40IP9OxrM+o$2#F-h?-f>*#92a@aAJb&r2I{ zmm@&&vCcEE;R1spQxgH{g@pHTN<|%~bQ{TAT$Ek-BEm2$5zMht-1&g4@8G;UkKFHo z_Ia4LLz*jIT&{;uAMpx^g|$qOb$ML&O82oRCcLP-dLeH>BuSYs8@h23P)5c%Zd8g< zXfft7{G&a^cK&9Q2%obCjI9gJW+OzCI#t>TM>{v@;jUqnjkthEu#soLUL012n7QK`#BJ zb9X{kSuOK96Pu0yLR)|NSEk4|0isEc+yv0z$mIy;aAfbAyoi48AT zYTk2z33lC0UCb}^aG2bHKWHBGMm8o_icoR>2J(mM3#`>T@T4rvHtem1V%hbjt%PrW z3;Z!&e&T&tBHmCTu6T1%8*5&5h{J8AoJf)bppr;c z9)Gv1-y2q$_l>rD&vC3=1|{P-R={qST23X7uc+CtDsXe(H!9Umz2^z{KQ^qI%C1g@ z&iIhd8Cu&veHIk}Jqx~?rGLkI^w%&fBOIezAp7Hj+Ix`OcR}qE4&C647Q%?CM_>yB zXypTFsYLAe8}9Py^HE^jCxyGbvZ0F0WyemKf~Fn-Vi20w#Y!kBW~G|_Z%fbR2WZ-8 zz`kVory^|N5HLvFkO|#A%k2oL7|*T4OAu`@OUtb`VL9$DDg|87T~C#=;Drz5@bBev zDndt-*U&e}NOk@c=vRHOh&KvkL(eb3^f|M_@Ti9J&Bw`P!nAs&Wehu%5qcxGykAp5 z&uuLN7v2A*HP;AG!ZA#6z=4>x+k5==C^lqOO<6@p=1Y1|>gG6uOvuU0NMw{IEy|x_ zyc{y%>*b6(hDsbcqPqRm^4d1xu-H!Rhp`bn&Ko+AZK5tk6A|eXoe^)%Yfy(G2BHpQ z{Tkl#$mlT#%yoP0*&25Em9}l%`e}`^iSKwsw5N)U@dXzf+m+sMh^K2 zko_!t28BnjJaqo;5#I4a48EY+0n<@7HRABWz+43bYFhz(uB5flSA4wXW&E3S#mC;K z)f*vAdqE!XaCGbWVSB6dKPfF|Jg~JorP5OGah!w9dag()wHV@|=T;%7hFAP5(^BdG zNwnS;xEOjv7rR}DA;Z2+RsHk{8q96mV?T8(TpnpP7y~MFZ?Si@ir?oXmU`}Z2kvN3 zY+pSK#5Q$x93vd_zhKt3>;**ip3&Ze?=zN0{n+I1ftf8j_5iK$X@GFiXq}9yp z(rRChd%*NA4wZl`d-gHZ$uyBC6Q6`-!D5&Na=f~X3iTCbOk^bn(@Pp{V=8K z)o&g~8|16;0rC#-Vfp78xh#+KphMvR(ae~tJn9zjJm^dSC;h+g%!B64^UQ+;k>N;swFI&uevzJX?hhH;#T&YZxmpJnNDI4)(!yB$v8jXCAALN?>FY<=rMXT1j}ANfM`%cYGYFnm zyJN@!#qy1c0{w?UfafT*F&48DN{Y*WB*5nd{~!QzU|qM)<`^80k`9-IST=^n*g#lvs4kgwO1}6@yQCPQ zP*;*5IaAm4YY39QZ&V&~_viQ4bn|b7O>t|3jSsfUfkVZ{j5`gDfg`y! zF!%=s?v+D*@jqFahrZ@Cu%10EIUcQ<--iBM@K~|-xb!hqz%_q_J(bJztzt?| z{d)PJ%-?i&9wvSABCJpd-tOHL(RJ?aIr!$ZE%33_P!4^<7-arYf!FC`WFCE{DWdj* zpwXM3uSo0;{+R!I+K(=VDubUqd|o+DY;T)Z7ui3uoctrRt=;hDc)azZt%XyH&ocw& zD(ms9Za^17yv++Pl|4AAhSKyXeG7uXxk?1q?u}*!WU2dB8$oNU(b*1w%ebrOdZ*YB z=t_A-#ln7yhbwSJ*h~@wb+kiYM6zqGnU7dY%<*ff=$USEiGCEFG{nh6^B5Znv^U~; zGK4z>Y1vtEKZbhltS|`N1%sulXQV-(S1|{zVUM#Z_NBK*HiH|$fld{1%dDs zYmlUO(em6aA(HSCbpsZz7n`h>+L^eUrEFa%8VcQF@=$R$tmb)f9-HUnkXwJVp`AZ3 zx8=&}L9==WG&QKah;ghdbDYNG5~&U#xPc~(!raW!jbC)4UMV*#@h@Re69CO>i3{uN zgr%JlW$0Gb!kP5|*3CcWVmV;+i>)U?vOOcuq4(J%PlGT1kQL+%bRJ{z2O<|0F@Qx1 zi=}{BnilcuQBTCyv9^ zO&A1wUP5k@<0$6f;4}vpci^XbWl;U&9_0Czo99yo3QUvvxa_)B?xt$7x4^UxQ_~1^ zsTAP!gWGH1QNe^Et=^^^3JpfYYXOVB*N;}^ikRs&J!fG|TcD+R`8 zSB#|={|wng%0E8*^a-Y)P@q2)Fyj}rf6af54(d<)|G(ue)!&Xi14YOw0xXBtABh_L z@0=pa{<#U=`+vg!|KB9x|7UwW@=sf+#@t*>VaNA>-v%B>pT*eRxO(eukLd?DwzG6& zyx-7xjG_k*zrcBrXiMY<4Ml4P)wtVG5du;OP zPQ#J&D_ei$bof~~4fB=?tYz^35%?dMP;*x-4$rLJT!ER(e}PHov{y(QQPY;&o}FDB zQM((WrgmIKnq#)9FB9KZ9GlnQ&P4X@yEw7kW)s;yGOMOn@%?^%1XDDd=?^HSt;1aK zf6x>A|90-d{aN-Ijv7$4jxNBqf#1N8dvp~W8zB@iwe)5!9#M5SBnVN~>(y(6$+Z~d zd|Mgw%k#)ay?PF%c&lpEUODN#iD2go<>5{h262qz)mw~k|C+bWa~If8C71AGTIX1PS!`U= z#%Q4)-L@!h>;5eT-`!u%bDlK7quJ2cj`po~wtm^fFJL?koVn+yn{L9GgWWzD%+H1} z$UdRA6IWr!K){;aEv7uyKr6}?!zQA+qRxd%L}1_Ql~C^0D;sO~RkJY)=z_SbfVK%= zN8ldrA!-u9XJTt;TbjyWn1(Y8YOGuBE4SOF$4CVMUBP}P|8J3>)$F%8D9N9K1q6}B zn(wyl(NCa%LV=Gj1-#=OmwweJhRjy&%sl!ogI0p>gXLIo2|YLebKmP<{(Zh(#8ByD zs|t*t*dO|l>_?u0ruz0H9|~U7`%z2mM;@e0y8Dq&{F+^I42c~pv>*A1>)rjx2LXVt zsl`m+dPIr8|oJBNA6>{6wOwkzvvDA6|T-y?*Q2%Y*rNBCkfk@0!8|(>PzUa z>DprPxkA1AK(q$7kqxKC6H(9IP{4uNOa4v$%>U=E{=xqvyb@dNP%U;Iw*|X4d3TY% zlW}Km4(&kD*-JMB@4qyaWh(gtGZ24adxo7tG&22##Oy@w({-WKrO@dpv?oi4aY}Y5 zKlVhyJ*{;QXv0*$ex%M^LZz&?r}n(eO|#fr%8jY2ks=4bMjc~_U^X;)m{$b?Lju)n)EGo3v9r*> zjS%-+r7E-E7&L&bF583zy`Bd(`+IFgRN#zfRIOVC`5K7k*(S{?3A9*yqw2OcGz7Y| zg)AUGkY;{iwHo`L>7c+dO?eI!S021JPHy8h2h4|oKojYZ$^?-u0%bW7zY{@~LOXy; zqUg(z3pEWJo-}FT_>syB9m_35H*m2h{>pZJ=#uymPj%Tiy5ZE!Aaa(kjq(Tq6AEK$(aN*^?82bRD4736!%0%2*_2 zLvcdkXJ&T|Y!ne}CVR&MqwKu2|^$ zbhys@*c>Fx6p`w-_@D$0m(X#*27W#o7XX7Z03_1Ub194r<_M8H|HAtWof~<90KJI{ zzbZXn-z}yw!375Ni>~5{a%N$`diy17-CFS-S5L(i3~6;4%CKH9vMJV!xR?=m13n3H z;k5|M1)%vw_y-K5OcTqrunhi~GIa}dnH8pt{G!Y%gh_PhnPnOPN-$Ebzr<{~139!T zBU>1sU8b6LE2Z6`axVOh)odT$9Jg!Smo)+#$oPG=8(1d!x|3(2j_G`ebn`y5;OHEe zz9g@dmy0(eDBzsS$73M~;&p{?{P3TZLRYH$L7*;hv~Zx@9Wzl_19w3$tlf-k`udoO z)v4E52K+J+TUBa^I#YSZqdYBlX|5+gY*~T&!80fY^5G@%pt-)ISxu5`FpxnS;_uUTiMd`XF~0~u&oGMNkiYgvt9{A3h{28TOQoOHPKaqt8P%CuW;$Qr>!E(v zO8ih~yJ`o+%4f5x)abAQ@4|TQ>Ur}+Y@z{Fbhf6PdbksMRg+J@8qqDN5c$cNSCU?& zuygW%<*%WP7qMAMK8LpfX`h&k4$p4MhnL^s&jI&_L0pkdi=rIGO`TJ}B-?wv*ATzd|jZE{PoEw9v1O*gbO1xu$V?H^qrk`$^rslF3N#ck)me;3;;v-)!)IqIBx~+jjCraWd&%ik)TU-i>K)g&C@iz8#>5; zqFFZU8(3b%dz|VuiRMce@vXpCX`$!(`k&6goEDGwddGE}_ z)`F>--H3h6KAVON+JPh|)uIf^+FP_0i+?Z%tYm9#)M(DkZ%omL>I!Pj;vDFVmuLzX ziBJC`h?sDoNtw6)T+BWE zD}ZyuIrQcrA!=0y3EE2zALDr}1Tje5h#&qV7qzRgUnVZ=8E8_cf$yjpn>2?#0b>Gu zjz^tL_ByqI+yz~|oH0ak8(Z0zBbSr8t4F^lR* zEJ>c|s#;LSJP~B8Y6UoW^>hV(ppe-Nt*rT_5nhJDnh=;`l^*^kO0>M zhP1*S_;kkgq?>6AQwp}lQtxBn0r5Eo{Yv^sH!Cw-$Ndm-sK-eD-guE!y+zwe1|+h8 zpAU2S>faUi%R_N}8$%1wsg?J2>L+Q!6!@^G8|&4f7_>nFiN>yv3G&&PL>HQuFxu5< z^b|70wBC%7a`oy~8Wo6b$sD00u0sR>Q5qiLXiqiHa~Cq;v@jv8;Er-yAW%R5nTv%TH<-x&x{w~MM5I1w@9rctp zuI_BtNX2o<8*G<4pbVUa^!Sxts#iS}1N0Z37L<99Y!<<#b#hfG8VI)G0dpn&!)~{` z?VbD3ZHuJ}c3Z44;zbmUzdeq9H?4YQC;%_zUGu2~U#XfOrT>)hQ=`GQOobs9jBWj=X ziWDx^KQoT?_0Mc5veK33wAYXSpV2?1K@`Ei2Vz~Ya54Os9pl4){^yI7XPpOKcA%%7^Q=mW62SK0LKfMp(h_a%z3 zyszo8zd1r;M`bPS8h7ew22QP&n`o8FK+4%8{)@)+kc)wSIZ9hFV4~^;8M4#rxwEO} z?Ll{$_NXC;rB<07zi@N3@EO@!sum2u#z1Oc-^Us(%l~m^{ajmWN7ycYd z|Ldd@9UdLPr2w&QChW(rU;bcLaD1x)c!zGD`L{cmc?f8x+Zf3(aF~FQPF$TBG3~Rn z-pO_c#(R6ReIa!<$B6Y)wG7u$X|}7`P`d!=OuF&i?hJx&{S5R-h+V!#Q*D3yA%u?p z-6RuymjzU?79F|tB$$-Ls>RXE_hi1Y0&JjJA4?T&c&~(I2K0q6CR!we8Ek0luwd&^I>-knWtVnT zoy#(wOuY1Ci4|lF$^xn#iZT@DP?jj{>dC{E5stp9(GXcPHU$fC&;>YRu`p<@tEdIZ z5K)o4K$rv7f!7DpcT|z|r271`9I8KvNVJz!U(Uzvj%0_FqPmNOr+zdd#v6O^1ojJp zJHu!Zr@D}a2wRB`LW9TZPolz3fImf&-K?3X-OPhB7h&Et1@cZPIFVjCaj zZ>I-AVF2lWy!%Q>KyxcB+!+!&^b4CRts3tLpIm}DGC)uD>Xe8)h$hR+u;k=~kl8+fdzS-=ER45=+tU3X&Zf8S> zQgU7LRWCXpODJp)`X314;+z`ney^FQarq(2*tksh;i7M&4lM?<2AA&uasgbJ5xBg_ zR;5C%llW8BDY*Oy`o)J!D{uO)fDFpPOdj^N*Y-%axUzw-B4`J0x%Yvk`;^7p&)w@dzBDSun#@7eMH96{H>9{JILRW@^_T{9W8%%k-xji-#zd*mtWDJ=+1gFUQ|sw0n#t* zih(m6XdNQGoWZF&rZyrKI&l<7)o8XYsQw6NOjl;GdgToM-Un{2FL7Giq#@f&)%Fl>DT9i^3`V=Sw$dPCc(m)A(=|HvaTTZs5;n~e8gJx z2#%pFda8Br6W|2~+fS)o@u-!2$4dV8%{!;loSRZReMv*b^d+Aiae|lt!2*pD{csj0 zy-O~t9epZ5hbR2eN^Y`}J%{ab#i`a39K}h#S3M>gm^G_<%tGiibVl;jpfz*Eq|W*K zzR#X(+aBwilPaw8wVhMy7{N=);U|Ogm=shEGqJkN z#}9?iE{qic75*1JnZgbSGyqJ}L<68CAJphC`eW!Yy|Fe1w7QH$r6I|P_otKT2j~zNM8!aD0 zJ1^UfAAE)f{*P&x7bKrnPf{Nyj~dZAf3#rcPyM;1o>`H7!dmplw%f>P=bYJJim}q- z|6s1%KRpm|fFgX4FAP!aIQ;mV1_c6b2Qg2$`X>Q@0{{MR$n0Dl}LZv zbLO7_)kI-yN#Z2nW?Kg1ltw_7__b<~J|rlIhFh$e(F#@&E4-KjbY3@G&_Nb7m?94N@Dr<5&*iVjrhl=!vWw#F-Ub^KWMj6wAg?V+f zNo7wae*m*&z@~Ey!jsrA3&K-{gfPgAl-kU29T>y_G<$8JbqqJLHFGi^Cq#4^X9>3- z6CkKM+3QqCUP1<{O{>4H_2{?BRYN~BD1diN)~>CY^)~j}w3R(*t$n1X=X)o9kj*ET z*7vA42n#QJCoM;xrgc24nrYc5W1D53T(7yRX3c0(t4#T{*9%r?^npDG<)>c8@P@rV zr=$JU3EtM`jxXRSvICS3|ICa|A?a#d=7Bl`<(VbOuVD4P(UHu8=1ePtBb44@6F(T6 zU4+d>yTuU}|1l-TGV7Av?GWrU2mltJ7(Hh& zqcatm`()Er0kx4&_#!bvpJR^BCydGOj?dt%NT97^e0QdW*uwikv>|5q1`a>rj6k53 zNvAMIlMM!CdD=*qA?FgCYMu2lR4FC)A~0Am`bdosZ!|#;hX$1lgH~_>+t+DD+QR7b zfxzAoaER$-w9itH!(f2W5WzDLgzt*;_7Epg%5k>? zc1WjJ*i+I!7<uKL*K(qYb)svV!xfltddf1?VmOp z3J9CUwK}EVfrmkKf~)hOh&dETYxf2rJ8apdA^=2=Dt)vVcs@#acoHNU2o1{YL_c^0 z5C>&OK%J+mcK#Yje{~V1w-_srHVkmkKffv*R7Du|#0nUV;w@Yeb)^Ws7&Tzqd`Z@X zwdsd$tXu(LZo9LTVww%nBW2cz@yhmvWQGbOff4~mhBF6e4kG=)0addBM3vnq zn-0bS9fwg+*gmn9L}b{|d^mRbI_K)Nh}si1=IRxjjm4h!sZPKp(#CS5;;AjequTfx z)H;bj<*kQjj@IOr_FhP|4&%6l4eY#kBdo8`2xAizeN=ydWe4SSF6Iwpb51s4Z&qWqfnPho-Q}dg@MYRWr@RMO9w%R4PQM3cq-Kd z&9$!bFvKA%9+f}ZEfHkypoQd+j}+g{&k9IwLzJP`XAjG z5ty;a_Ro7I+jc)kwpGZsuJXC$paAOS0@$905c{)kO2(d)3|t#DTb+a{k6{t&06MR+ zWI!FU9kjQ!t93jX3Hrgv0s^pgAYL55n(#_JnVz0*1naY*AB}~7GFf>Ks>kN%%DeDu zliBhbnqH!A!MBn9`o!T4s41|3&6=OAJcQM!msB2rU$Abi(E3dfjkmBQ4*%6PAdcN7 zr;ya@CJZUc;xPCiCX(waZ$K9Q(>(M#lt9oV>w zSb2bb6^<$3Qw$i0fU(%3ofB3+f;9-u^2Y;$GPeh)EuvM8wg_*R|U-0lR8pIdd+0 zs%_`SRMlh4%K{CnD#*LfsQ`X^{)+F$nJfj(Nk-s&ImWcJ($va z3)D}b0IcLO$XoR%0OHt+Z3R^p-g?JH&&Y00^&q|(%SDSBpQ`!=vaT6SBoGf8+x2jz zDpmC@1BkZC&G?>1eqa6)qF+VvM#1R4_WW?So~B_C)=KGo9#@-`-`p1cY4;BXj)6o z;(o8z+Az0*N6^Dyk|9$ZCJ7@ZdHC6kNWOrH1309$tz%N4^>nz})#Id3qWb{raPr2y zqtS^oi48+q3U9?>B4G(gibSiwGA31dra#{k5{pckA!zy@-zD3j1zspHnwI`SMoSVj}j@sSPt4PH7NF`_U7!_d^5&jgucgA-Tay zKG2x#nO;5h4QtWAS}!H>VvYO1L~ne&G5H}^44UbSCq1zm(QyC4C@KpG_WHrp>Xo~F z3eCR=8hBdrchi&4lJ91LzosW%D_=au`akyG1u&}O-XA{+2?PWZlqgu#qXvy27+xBA zxgh~JnqUl|Sffcu77_``y4gU$S1{6&uCdf=OW(A$z0`VbuC)r(M-UZITajw(qu$20 zw=Sg*+Wy+&Gyl(L9=o$AyUB)t7w&&gGCQyD_dDPD&dhfnXUgm!zis zTzUr~K@~~x-GLaVEWM&Vr8)okl#8sq=TnAIMQ7)g=iQK3*6;TTy5GF9!Ewq*xRc1ds1@`gK)c%?6_H46h`W8p=CAU7H5Kz*bVhga z0J`?n?|vx>74zqT(OXeHQ3Q=5Dt*HhsA^o0Dc?XJ_df z#aMNuG)52R^XA0U&4<2A&t!W8?*1 z>dston%otT^pZOui;|6(HG_?JcfL@jtU9Q)H?rE6-*A*)>*(q{V4HJ$yld>6Rz#D~JKH`hj;72pq@Uf4uDDZL-MmCV{^X|ZF z#IxOjwBSpV!3gx6YeOerGrC1p=iA4UboWjux;~Cb5#vd*sEL%(gfPX!E+kP!gZE<4 z4+;^}8z`?=Zc7gLl<@C0{jKk%tVwrQ?L^BcM33bTr0m=^tuw3=MXd;XQQ=e91K-^? zVjZpBscT^IQ80+FmGR4D{8f?hOfM7Zp>W@so^0g>hPULkjcy6-+$F9UFjA6LJG5TG z8{EJAd)6(Cy1O&)g0nmGVf^kGnqoned%82X!WU~DLpKynFL(QCV7fc=R^VW^Jw)vH z`Pl^!!>7S;@SkF!gbA~|Ej90UqzXav)fb9h$P?L|z~HvjuiZ+X#7a);w}84kwFx2Q zX?JP^Tu_q;TdQ2#xHS>)SwL{a&BWZDl0^B1!viB~r;%_`imd~a+<_7I=|CWE-Hd&5 zWAiY5aF({tW&AeV|8_DJoO`Jx7}?}8@D%HRN?gUi6XHrkhI0aYguEso8^Khnc?4qo znL3IE4|!p^Ldfge%x56=eOhg)D{rBUuEwvF(UovX87)yRoo&jf7-48kSkM@q1nC6#_nCmj5Xs| zX3Pf{Gluz|+ghC=*T0&$BH>1;2p+k57)WCXg8rR}U|7o;1*~>w?(CwN(AUG~4zwJm#SCL(fEpBFi#E=YAQ-}3l$6Wz&*PeHE`Wv1 z#mgD=-)hku%Uy~fhek!nwToHBo+u2L zC=3@_Prj{2Wo;>yRY`Z|?%&9)xtTS$Wd7RiGfn zMN#X3BnHg5+*9AQ9bGC^+TKBdqT#Off2*zc6rQwG2PVtFdcUXcq@5V94ujCfuvLgf zBfc5O0v>`kresA@&TI15Xd;TTr`})NgK||~aY2akxDe%Wn({a-C}wP9c)VFm!L1}& z7zN+80Z8cJZo~qM)gqkN!^x2;YFf(pLzi`@&*{WG-d_^-R}4EDqnD`rL)O$9Nhp_MWKD55ehf?ZOl__1kcQ@6qEeD}#Jb z+?{%_0t}>Xf~$2;N?Yn@i4aKj(`xPdEAVRJRCrbzE5P@V7x0y|&%N`vw)>=gm>r9z zs(~^HR16^M?i^4V>~1|gIDNxR1no{e6Xf{aK_GP;T&;W3+EP!EDWtV_+2CvK?$q}u zCR?cj<}h5Xd+fHj>m%^>3Yb7hL-yO{Yo{B1%m9|GWD zxC5yV!qvLR1zC_xAoU(vtzB<{S}UBOmIZ1plnyq(r#3J;ea9gzl%!V4_-w?dG2YuV zsT+Vle0;Z+Vzs4?X72>=&R|As*AVRYKsCb3D1SzvPA0kk4Y7?F$pp^bBi>!6Cc)D> z=gqY!#nsrRkova?g1M)W_#faD#4XEa;^KCN9Jk;F(+66ugLQEbVuRZG~8C!H0qSQsjq$ znD3#@b!T1!RE`YBAUV;qIXdFGj7JUC9dYCs!z)@h#~96YVSzxU1&*ybU0II= z-2sF}Y$>Z3L3%5m9o|%ifgLK9v6ZYq1Yk$rnDFi~hyXyX+gKq+uHj~lGxcOZY#+#O zeIjWi;^fXYxv?r@!rzD3nSaazHcrHwrp(J<>oH;Ok$*i;B3~}^BQ|?|ZdK!|t|nam z1`)J2k4b4-E}ZL|Z9DagV|k4NBH%llK5@7Wedo}JyvR7q^_8Ti$-(^?uu+HG>eFnb zq;|XopHxwpTBZf>I|J{y2C|7zjPDHvrN{eY1VicrICr3@B2#kJ@F+~a{d;n}qb51x zZ$P6)KZb?E*645b9PpDQ{E+Q=C}K>ndteFpDEupa{tPsvr*3>nhmVsWqoV8duyizH zU-BR;`0^y=AY8Hi!rzK_NXmXqR2&HjtZQGnaG+L zy_z9I2MZBi&5x>2?IQ*u)7%}^hAI`MT8Z_Dh)pY4Fcd8<^oF@s)(P?5MNaS}%s+am zQE|{4E9qs*8vIin{swxnYg|P3oaZwbHBrLFkv@0)Aw^uT!xl74rmt!&HhJ{e;uyg| z9P}@Zp#K&qhFip{BcP9Z^vf?s)-03|2Dw}s8>DNKsABj*p2D@swR&K+N%lWzs;_;F zrjs>+}vQ&G%;NaL99aPU&8y2BczoY-;jhP-Z&SK^+cZr;#0!SnQaeaNxCNPa=SFEe;t?0UnWWv3;JFx zV)Uf%J2Lv~|Fjg76x6SKKL!5e&J-6yTINhyiCIee2F_fbPtAfSDR>Xu$WHo(5p=YV z7`z^fwLd>a1p3$Lm#+6^<#2w3FGT(mQONxbItIPAyZvqv9sCXwL0leQqhcg&=WVlw zVh;6LNi2QC57ExMGmBs3?Bd#{Plce^{)3eoY6E;Qv8r%4RG{R9#O}O9NDT4}o-DE^HU|`l zxTnH#{~ARCA`S}o$-SaH(l>CDC?>BX?gi|${lTAy;QV)BasTTGbq8MV&YbuHQpOjO zDC!jmzfXssBEkE?oA>U_fBu_(WOq8!-|$xYhGq`}+ zvUoxAuV>(?fajGz76e{seLp$<&QkcgUl4l>mgikMuB5m(1Lz1|csm zxO8zriGRrQ`E40_fmZ_0{Ci*P;W6oVo(7-*UYO*a=AOg74(XpwMab@gqLO7+Ekpp< z;(DtLMFZE;N^2FH2U-|>W!2Wjv@50~9(HxHwTO02iB&+mZk{!Tws*RfL)$-DhBp^k zMb`4Rf}&g5VoQQ=Vf-$Ow#ml(k=?SXX3Az$k|*Yc!N(L(VAp}=sFKH`4IE3Fu0`UU zY%dmi(2VL&Rm}){PZ)v)ZWq@@SYIkwPjbbc5^9KcfUxoaB!?h{sMD-p?!fyDvl%w+ zEx9jXdE4pYO;fNE!osRY1KvI_x(!zMj{nNV)Z2Wh>Q`h z+bi7I(j+S~@JvqdzMs`vROjH|1WNjSy8^ooCQJ1ta)cK1A7=~2#w?fLc(m6k3&7f# zB@n9I#N=+T%R0b)OhTRf%iVr+_x!dgMS(vqFKC;Zw=G9xaml;bgH{}Pr#tiK&q4lN z)n0)Gw}Qa@JMd}s8h4=hufhBg6i)hG3&o4^;=gX=Cv?j%MT?uwkgB}dh`lF=ZhR^( zK9@`1a5lW@kC%*|__qSAatwA4+LssjI4|(u^PzmkthqOK1G0Dj7ZJGYAiU?J!4a zslP^u;QT)k_w=7V67AIREEa1$Y;0PkZspV*3O><%Qm%cE^jj z;@{Abjdq5`4;$HE)1B%62bCc5Ppm-k%Y}h|6}2@z?hfGPJo?ckh@O-8g{hDSs<414 zYA^m3s?tSB7?cR-!uIf@(-7@T%0-$;{94XEp_J!0 zfbP>w6n>iwyT){9-mYkKIFkPO{A*|JY8n|RZkYA*njwRVpPu!e?{wrra>InK5(zbZ z?Z94e7MNd7f$^x6!0X+aD|eyaX-|0@ULg+@g&RRQ{aq9rha;pVN(16x2Nb};MDxag zuX4d89J_>Qx-QdPHF7tl7`|26ykA^@8so zfhD`z^YFs%`4nV!1yb0JOHjJ;jx!~zIPjd50t*{CS7LAJ0wt`wiX;ls=l`QS^GB#@ z*v3PtfbFlq7s@suuLC3rixO*T5!mx(*so+Byy&-Nz{@gVX?Nzi2*?XOFOuX*q$Z#UP1f3?7~5Nf)`^*=5Ae4g4UmUTla_Ett*zoc~y4n ziZU_eYP&oG@NJiK0qMy#P#bNR+q;KUpoDf`KE{Gx58nDI@h#lN-I*ggp@<&evmO_6BSEt>Em9ivv6=p3d`;evVUE5E>I5xNp%O+uQI>z<*gaB+8 zRpkvYYQH%xFMt=Ue@7Qw zL+K=zO*_g4J9Be`bJJO;5s`lXuI|h~KbdTO4ny}|XVF6Dd;OzZ-y80~u=TxD{o`8S zJI8-o>w6_lLvTuk5WQghU91SawEbE%5wSS%()N*bzQWLGkK)IsZ|l9zM?ff_ylK2LQ6T?6Xn&N`8g3Uq@&U> z@nt(ZgX79x+gVt*L9?OaH@VxdODhNr%A2+Cw%WXQ7xtjOIBTE3q+sGR?y46=K-C_1 zU`XN4cTgXP?{T+ZomPyTOk{$o{2XRPuoc5Kd2!IexrJ4oSg#vYIBTcx{NljQAQnH+ zeP5LpI7uuyP27{$HdGF5LscZ0FYwQSq6}ULr!4S;(3OTP<8g-DrlZcHQQ|(HpQ1Ca zx^n5X{ow!h^I3y2SZMz=cC>dk&u06-1ci0QrPDk8=MVZt(V$($vv%J4yQw(%2z5Ra z$gB^!5tTdm-f6gi;lE{#m>CztW|?(oKD8s729@Rg9mGK^vo)y9V~oS$P51b3Vb>|b z2v->BWO1CYiUTL61pgm~G^#jkDxeIM15PFndO3_Ex!`^QAq39xj}-X2(_;x-@~l#t zg_N2ygP#Z9PkBL9+n69aDvZYuo=d&;2c+0IT%`3=CUEdKD)qm^BeLV{eZwWo9_m;b zYD{!*JvL!OLA+ncPgj0)SezI2clV(8+!H@`2c8q<{u69ycefr)YC5P?L!@2^^KQ7h z$9JQtc8}jFes_uA=fv-B@w-?2z9@d%p>(>(%kZJXEyMpA1O8_s{AKaG7J9gQ{7UhA zgZRBl{N{<@Y2r8YX~LW)+(X6h2O<^f>F)96BIG^s`&04zpW^ph;`a;Uw^jT$iQg6C z_iFK*FMel;-}A-qSn(SWzaI(ue-*#q7QeTM@Y}?%Ojpu5NrXHpV16Wi_dZVEe)<@H zpA^4Oi{G8%cbE8mPW-+reh-M>gW~tk;`guO_iy6&AL93);`iU;_dW6Zf%yGM{C11q zB$4hQ@tZ1shlt;y;`b!+J3{=Xi{DY=_Z0CvNtDP%;x`Emw|o3Q&_=t*zb1ZvC4S$K ztyb2Q4&4c;iqn^WBVHPrFiyYo{Y?1PAU8^ELgT}n_Ht~uR!hknMabRal9Axdld!VT zdN`}8W(SvXxn{Z*b};Q!d@KGG7J4T&E!=^Ld^qMWnW32JJFrR_j(Ig4bNUW$%n4E3 z5sLAji<$MXd<>DXOyUX_3`O$oncM%N1O6KyV{F=ip<5XFHX=V}VeiV)9T*~oBdS9Y z*QGy(ov!?p`?}y~FysO3r^WYwAr5jwu-D5~8lBOzLJ?&oay^NZ1@k148{{#-8ulNf zH77Tspbbvm3`LZa$c-dY9{dmk1#nWa14=T?^}`|9N{+>qyf=qJmytpxDJ%=_kQAzL zv~p3_r}0~mbpxX-=&uUSMl=?0s&{Y;XNXjBh*ZrE3}eF)(?b!pB!c5vin3~h*T^(( z+QE%6AtGrZ*p+h0OEdb{Q?v}~NMt36)CEULBC8m56N6R--$AE{mC3C79ax4A6aG;M z-@7BjjQCP0;$~9ulFH4&ZR3OteLJ`@A;jHHA=t(pm|cYE1K9cP+xn?iA|2qrd-?y^uEUrdo8VMvhv;8IEO;vMIjNW(%%pF!{rj%GsK|0P3<;7YN@3B{VaZd$&<3uYZ=R8!sb6 zuXaP~T%7REatNJRE-7kS0hhP6D;dL{VZr%>Kk7d4DwI`l7F;+3pV}R-0Y4HIpc62o zbj!YO1VDcPP|aX>=HCB8uYopfhl9SPg7B#hceVMimalBZ=OptrsXaqPxz~izKHq^= z4^*<6jaX_S!R}5SiRG6X2!V_pz|LH?J`bM;zAYQ1Ae{e=XBT2m#6Qbv!Gp#!9VGU8-8;5nT23xX|PQIs0|BC!&^Nnn0kdH(+mSye#Ad$ z4@SR5fj!*F16XmMp)Awx%yAH1sG5kPO+m4V-KF6hhL(nf_2G{I&m*98#d3*{M7&{$ zWKeWZR1!3}Z01)nUh>fF;9)CiYKnq~B=8_^!Q8@W?PfFvf8;$#8`D&E;Y(0)4E%v}@X|XM zjaJ^>r5cVJUlnjk-mwXYO^GalPzNlzQxw~al|VY1*fv&yhSG61z|MB>eg>5eEXyMwf;As~2#>s#?Z5%m4w;qi%&U=m z*0<~k4w;y!>@@(g4q``&ynqV#`6~DtRxwffvT;~A7((Td#gv01&TAU6W~y(@U{ISI zSej55-ix4plKrO~*n(KmB4JwxeoP;j zauL~m-~o8iT1znZ50}ND*29DRbs!ur8fL z3zc9s^+eMWnI`&fsZ`Oae#t2g3STa{yO$p1ZV!OH>lkkx&XunB$o!~9rzF%w%Su4 zo`sKHaBoEFci~WJ;9E(aX5>s{BXEP`f7@Fk?)G+W848~Jhd!WqNmj7!Q14L4dChNn z2Wj8N^k(cx=?pZ#P1;IHi`{Q3Qa|98Lm&kp8<+nbr)=?i!+`z8O1;OYBA{6y{I@U=g>`h|T#@#E-Vez+{7(|Vmy$$in{ zY0|-8ztRU^O9}N@Ur-wfwW%+tZG^h1FDM+%g=1X9^&@&7$6yzD@XWqwxe0Z6UmtmF zCe;4Epu}MHmKfz8QRzQHPgLPxl>2B{ei7V6gAZ)z;z$-p4pg~P+<|uCliSwYHgGhY z*O8>YgA{{f+~0K@cWJk$`tSKTsul_-87;kbjBiFCE&e(q`J=ZxbNQ{3zYD&2Wc)1+ zp4K0I+%BrdDba>VQSx6Dm~XoS_qTvk2wR|D_R@#=0A@bErC8F8qkimQBf7I``*F_U z2RnISZF}lvTl&h0lzEi&Fh$7?KJcqh8weK;T+!eI8}tLseM9lcPkb_4JvP4ZFLt@t@8o%j-I*V+ z#TPDe)*K_f|o@ZW5!4?I4-RH!_37f@Al&9 z!P8L#qp=o^KId=G^k+QX&KeZF^6e-|M5W(OX$SLOj2nInJG_I@y71`qYY6|&3-Qxu zmlWs+eZsGgpMC|-V+kJk8Ti5Xg8Pw9+lDd(q1|8f_{VZdAq$RZ1r$-b(c3fi?e$UW z4pF1Z2kqrlG83hyM~Bbq&b+=kYTb?wpB=2;8$bQP{rgZ}Xo%UtnW%PDhcEs?iDeTU z(fHJ2iIzU}8|Vc_xF5u)Xgr1MW=(BgAJn4DD=^>9&NEQ#?k>R^m(-r^_Y1YzE*H|i z%(~yUfrC7#eqpt@cd(X=p;i0+bi3PS#eZX$0B#Y$8-&%iVWUKql4z$dIO$cuDVzx$ z6RO1SLShusJ^!PGv=WZqgrtQPhaFX(?6{c!8kzqwWVQW%X|>yA^~-EKZ5uYpObDyJ z{jkVChh6RW(+&Q+7&MA&8EqRj3t(xrZD13J=v4mc3r_ll$g{vHWPNIviQR?7nEyL} zuk#PbvCRJlcH%+=qC1yTs361b_e-l?Xq+#w5w>mEDzhT2_I4Mu((y6|0PP#-4rIRc zPa%tJMx0AM+qR)Yphyd2GU3y*pf7kf^?iH0z}YV4Qp50OBuQD^^gAVsaySMci>T#a zToSzRIiw$ZJvq=GWXax*Z1w`hj%8LK!*J-b2l|-*Kz*(63#2*t{O%Ahan$=*AkpN( zY}~}Q8Mpq#dT2lNA@%vfey#WA2G8sVyc(4LFkGbJ+(*lApuL5KdrNe9zIwo$2b*sm zcF)fSB$`F)r&o5%e(fqaASgS=p_Hx=Zd)<5tkARpt8IQ(V1Cw|RLjCQ(!1x|sOi?h zA#jj7hu1L)hUld~hGF(tw3JPoVngGsK%KsTKY&&if)7OnUqGD=1&3utmjJ?d?SFK) z72^}+`7jpw$Jg-la|6ZG#fRGBoAdS&hj8dLW&ss}CHN{9ZR)!ivy-`;7_te`7qSf` zP20OZm-j2D-?vj7j~7d(ksKy`2?-=5kdQz^0tpEuB#@9mLIMd1BqWfKKtciu2_z(t zkiaJ^0dZfXjcYBca6EZ%)*iV6628YG6#reu=RM|(2WH`iN!Ee;J3s92vGAX4MiZ$e zB#@9mLIMd1BqWfKKtciu2_z(tkU&BL2?-=5kdVMplK?k_B%ZJY4i{N1+%1!MlmxDb zlt-64(tpSiUvgz+yzTJsbI8|K7zv-_pyzeOZ*llL9P~RK_#F;@_c`#r4t_Em@m&u3 z84mbOj`X%T;#(Z}E=PQY1HZ(e}uC4MlG&U^v+t<`KHdWTyi~LR1 zwGCxcbEZx$%Wqh~iTFmqEVulKzXoBA zRuiD>;9G>SCaW4@4S=6&<-kmZD5tZ ztpm1g&B32-U27Fu3$0RcR1Q}$BvAxcG4SeuQ)|sb3Vy(o<7?q32l-Zwpq_1AY|Q|V zIo1?Vx)3Hu{H_L^$6AA2R0GZrlWkofVj6{ybgPg`6=E1N8-8*)4N@zIJs0p{8k%F$ zkW_r2PU(}wXlckCULf*2UPw7y=zW#9jkKE~`HPWOnaFWHa?ya?G$Majf>XaJwJFGJ z86*~2G7Oz83~Ru}FjIuT40-k9Z;i;Q4>ZaQ_eIt+Aqfv)YZR;b6DMADZEbz&Vtcj6 z*I3(7KXdBjoXJzC=1iHAGkHchKUzA8@{HbokC*b)C3Xz;TU|p{Wt|5J<#;CMel=4GKYi)D5*2(c-&$h|3D!uB4D$C>ZHTYz(Ro~#RU0z!yCOr(QuJl)`C}C~IZyL-#g}+hxJC%Pa z{9|AqRsLKR-=X~Qp3o0YNfJ+&%1>YN)AF&l$nw_m`G4P%el4Fv%745G2PYMZCqwb? zuUrvPCzv1Bhu^Dsou4k{m)OVdSJ&r5D!r^lQ$=F4mw8sy*3Zo<60=Xsn(e8t4*M`( za@2Fp;Wjv!pRM1P<-_vJfZ444%rE^Lm0#z#MfnkRLO;5GcdGobyt5WfvyN1ruR|J? z=i%>2d2WT9@(U_|v+}>L{FE=@UsZlB-+juDsQ+I*IC4fj8Q+!i!#Y&$YAh}Fw>(&m zFpK+-5lencmpUL-JB z#dRood7-Cj>sK80^1b!@k>%U0{JMN=lwX(cQsvj>>sEeUzAokOC2NT&NI;kGCM7R# zWo<+Yq&(T4?5(nV`YSXvU-9G|A%1_;OTZ^2kdQz^0tpEWOadRQ%KK=@goC9I{_ZzF zA3ds05A&=aJ~-j2&umPa{kM}!w%@ZY_hM%%*I)dzM>cMq@YL)}(q`|OJ8!!)9{#7l z`{0B*4-S27$1{((s0 zal;&E`~&Czc=o3*88!Pr?aEm{e&wl|&iIobI(_E$r`BCM{IVBw_db=y?k(bZGkx!6 zFFbwyQ{SBV*qnP?i=T{$gy(R^N3)lI7fS+aNYNM@K^WXgHQd>PmAX6zGKu= zt#jV&!JmK1SD*ZN=S6dp>+gEvieJr|<)okc?>jCtjWa&wj2}Gp+ts;qFaGqyPnJLY z;B067TkoCeI`<;?Q-3&chO70x@62_^KQLnNWp_=#?y2)n-8?6!{wq&7#-W+q`L<@od|samKUFo5mRr|8$3OfVA8J`5ML% zZFM5fG?DS$-FKjkgiT~D?i@g*X`Jy$8$qUtgaA75!zMCT& z-*#EEU@nH4f_}DV`Xa~Cb(2V_Mf~>mv%9xQD+!zRlCb42#>13l>S0#|XbXHau^*-% zn+KV`AoeZ&)2!9>MZvzF`ke1J8c;&f1PpkYg`T26Gq6d$G9>Kr{&%P7@DY0OVrTF zz=6OJw=ewOU^K3_E&cRUKWMaAb`SjkKvRE2!4vYuxVr_rT3dRwV_ETt7Iv9LTIdP? z(C2)1A%N}oMQ4QalV{UJfT7+jG)+V#Jm?#)GL17Hev@_&`IK&3n5PXga+wqV;ma%M zXyc5df6lPaUZzbn{5t?+8YhiAkvY>i<4;3?X`Jy=_$>(5#tG4af-{XXz7z4LamHi2 zIkrt2#sP3D%GWfJ(I3HUn#fqZ+x1O(+BDAiKY!FT*EG)f7p|>((lpNa#dn`E+cZ7m z$2`1av0UW%_LrH)8Q=WX z#V*r0ZcU*3n zM#VRO-m!lF?^piyOyR?&>lfeI@RhVRp>>4vLTu(P2yd#$2EJJT?{wfh(~aQ|t^db~ zUtZKuwbG!K1$PR}mN@YZRN>U=cf^SwD}8(C0P`Owe);81mA>k5E<*W_6F-b8U7`HP ziC;diX}R37De>K~Y?@~VnEyEOvlc}T$vD|$9=61Z573Awl>a#K%k%2+-9`DT%4SK1 z^`EmTd)EN-A18i!ab@F5Pq+f;^tZ%`ACY-@LivvqKWkCslCGA&{p2I&X$*b4 zMIPKR)K8sp;uFVww0_tZCqDM`HxQpTce2%aR#aBu<1fwEW`vQSrF+8lgZ>ibr@ZKQ zD?jTC{VwH)^MrmlR^d}|l=7qHEvZ^q9Fy|u*gLTL))^;$tn_nU7+CtQIPsnBEnDY5 zPJAslZR`5p87F?M^mASuVE*I8$Jg~u{8F>F7T=?*w&$&_Zxl)z;d@Zw*nXZ>^qS$% zf&U5Rcf(KpyiNHT#{qRmoaqz%cz%!-@$5TF`H5BD_Rj`Z-Z^pN$4Y-oocPYN()GO~ zPJC1?e`S3YtDJard$WH&!2HLFzanz_6Vi8E;>6c{Yde(xIPn{n-Q=mlxJUEh1|hbu z?3ZQ!L-J65ozAhclNI5dIMdVmLi3~f?TQm$<7r#V?@*lhvC^;DH?aESjT1js`{{}k zU*}ufq5Q{*A1nQeR|c5>IPqi2zbj6Boo{W2@*gLD%>3^kVE*I8kD32C@pZnn9m;>4 z__5Nj_{9M8A18h+`FF*Muk)?#Q2yh@ccz~OLhPd}UL9cmopRLW*AXXv ztn$nLRb=_u%I{3?X!~{f<;0m@tn%xM6W__dmfxW`@ne-=#p?sBKi)X;W0hY};p_U` z6(>EXJo@X`<##B~^kS9YmNy0_zm7QZW0jx%=D^a=i4#9o`E6DBy8O1pNw2@<<%HMe z*AZuWvE*0r*1+WFjT1kX{DKN!%daa=dQN%t*RSPwD9-d^*8g7*Onx14;;*jttYIEC zA2o2N!DPQJ)8}}C>nXPK!+A^}Ex(*N(_2$p@AH?}uJYEE>nud@7b^ca=|v#Jqx%Ev zzvC;9zWCAOLvNh)V)3&nPW-<3(ekl=6JP#f@ngq{-xohxKAYmC7mJ^cIPsm8M$5zQ z8kjtC;>3@Y{+2lLW9C0j{8;JR?+h^iapK2He@mSBG4mfMeysHE-wrVUapK2He@mSB zG4mfMe%7KXQ%Iv6r`9&(y;k``&zkb8N^fPAJS0WR{j%N_T*j5zvL+82lD5Zn|{l`^W8}KcPPJ> z|5oMK^53HTOzT8`wEXuSrTp}z@0%g4G?<(NQr=6I-&X!(ElWKga2;WKd{;Pez>S#Tp-U#)Bch zvU2%m-dpKsa}!Tp?J{2_PN!FP`EqqYa}cqtKi-2<-g}h4M)^0wpAA!{{52}RMETvy zU!eS1@UsoLlpoH-(~AW3c_%HOki3poecAHo$ojHb`E`A{TlsZ;X;FS%U;N6i>x)!=uk8a<|k@wOqsb5*&u%_PjG*@}NJYiokyR}W?jXf(m z-m*&Eg~Bc5p%W@4;#QU3jQOzvgpNuiO(O zJ}n^QtFt72{yEat3Cx4uX%LDES1MIjRbiXBh|+qa(ce%TaY>Uxj0m?In*4Uda@$u~ zudW9XP^s3CmXS$GElDn`rP8?xp2$Raz^=eMje6HsX0llTX~Nu>z;|^Y*3XFtmC5kq2QK=K{dX zd!=%80uGz0)PuSobZzTwaR-%#HG47H@|pl0`NVhMk@Kdi#b~cR@3e}2e--cbV%pJe zm*Ra^@!en*rlPs-?;TdWQ>$LwbXAAjqgV<(LwEzmkw2O<8@F)T))HvFh2k!-Vk^($ zSWQ2%?)}m6+Kzsom>aUJL=F~+TH`^RjShD?;QZ<~HrA|0{ApZ$hnT)&OiQ=-{HL`` zJvj7JQva5|Ydoi&6KXNMt%moY@je^2Ln(_=NFlPdWs7@lcn{k$)QspYax!Gy1WC&J=1kM{ zhtt<-PZg=>i}K>Vc5LVJemt+x3$pg}W}bX-%6r>9(7;S9vZh8amvDN~YrrB<^IP+g zDsNQqiC&lz$;MX-toxeF$;de&`^sTy!&SaooSa?h_Y|R*YQjBM^BqHUQydZ6&K5T(}r)zn`{*z9cswQ&>pLxMH#}em-f?Fg<3^f zlCtv|{Jnqt`?F{Ma^4dkY}+&R{l)j`_=WH8tMmM&t>BwazmOli<@=$Jex2!4gH47G zYXI$9v>cB4!Zo%Laf=XAgVd|R1$(1vakrtoi7*zP=Ax(F|Mb}x9cgNo<3_^CLSy%y z-@el;20!&MtkuS+n-Q7)`j-0dKRTr&|0i#+a|IVw{g;mX!Yfa`yZxlLyw>}stp3#H z4ZqUiuYBu+Z_FG$vT)PW+due^QzpHu!)Fv`X8tkz{L42Uc!e8)FV&pT!Fn9Fte^KJPvl5TzN%CFlM`L3T$3{|{iI3Jcnd(A)$ zyRY%>@pwO5Gc-wfL>MDuF@ozG>L;!9_!{iS+I1egzNxOxmU4<%#R?;zgPO@+W|FwI zlzoDY)b$;vTK_UE%n@~@yn*zbFd^lVcf#@RRkr;qp(RLPk6WoXs2zOJ6tQ4?5JiB< ziMOG)-tY0*Rh9L)kHHpROzrGSdl^=ER&uHok^d-!m|yCpI&kDgZmC__!}{Q-Cd18V z2<6Q#;aD4f%B2!!88E3~SHjKPYB_$4CBL512{WYUfy*izJvMGNjJz!^40r6>PxSkH z!YNDFiP(O6yPwIKKc~OvdXm07R*sEiU5I|Snn{bJy6_#kYzevruq9RFR^*wY*7*=~ zi*=icD%^g|`5V0rjh<>*%~9(es{j(^*wv5bCVy?6aT8Gw7Dd2`SU;Mf0XTZ8gW;{w z{dvv-25+&hN9j^SG@=xF8+2G(NL?af*yBfPfl7bfVqayI=W#zsUay~@2=Zr6>X3`&sOvp%+m^AqmFXLrwOp<)l%=EnrMRXC z9a^OxM}@!>`4qmJQg{saO7Vcz#2@--r9mCG{}8yP?rfXbo2|FTbu< z-J@>OEtrQ5X{&KtmtsW2FRHJ^=ip&_3mg1}*!&x!Cyui*-bH9U=cGx8FRooBmT0s; zzp|=^0ouRF@2Rh<4bxvF);q)T#g$%>MGe0MjoQq|B2QJ5&uHZuZqeF$v6!p3P>zRy?Ny8r9zUf*M<-=g<_CqjGTn8m!WzPJ9aXMdKs&)2rDf#zrL%r!0> zGghv{;5-ND^D^8ip93=$F0Q|w?bRM%Bi^UZoH{vY^3Nrh#-cfC@Htxd z#0p~gNwnurqJD6`5`Bz%w97S7o{93r@S*IQ{IbOj7*CZIFM@N*^vTn6CQqA|n{!E7 zZGBZ;;=b5bVm#mX95T^9PsILs;QIF1_m9oo1XahmLyl)+4&Oj%n~TAmeHOsXgSiIt z(gh8^Do?3rRRb@N#Ivv=>?!cndHgaMBOtw2DBnv*f9urq-7rT`&bP?1-xtf(#(}*p z)-*K7{e4#~-{$Cfw-5?``kF5sFZ(A-8!q?w^LQDkn0zi!+n+Iy;21YN`{q6cP7w1O z3hS$Uo>iFo7B9|Q5<25B7Cg&FVoJ#r)YU~z@Oa@vm*1j><^DC5K2KS3ZI!Q~5xz3H zWYAca-{kWl``5_vaal=IV-05Wq1&z-%jQ*9t!(m^2~w3_@62jX<4S*noNqVIT!uwz zkMF|9nf?as?1x?1R9)L}VU_Y%&E&?U(89#RhWf%)tC-YkPYLMaJ*uX=c&*oCE%Nxq zy2P6L#=>gEg!s;{t88raG?wLgy-ONBzT$>zPhDZPWD0NvwT<4o%C!qC)q2EQ$%5KCk6=h}XIX_@ zkjG|vr4B4NPD`dd8C|T_E*9lsjx;}iNonEY>lnW1xINi`!OW7_qw!z8!v{j{UywbC_ zM18BV(j(Vn(Vj##Yjo6mgcsu2#Sli=Z{e*Jz6Ggk#L0}|r+JXvvt4qAJhpv&@~7BKSP2OvB#@B62`7PD@Q%U!n}VG@+Hbn5RA@L%L(!gYT^wZ( z0);U%^-l%qu6?7v@iS}7-a%jZg%;4P7~zLLef_+@d0rXyrBN8?oH=5IhA*-I_}J%C zUKw@gcB}KM?e|UA;ai`aap@Zi%L+eJ(skC(-+k^%9e#BT_`sTdk9_5!rA6P{^R92< zqEmmO;k`rt)Ay~LUK#bopR9_s&lfDz;d=bpDFZ^^1?c!&tQmMy)_p_J*Rh`SW9lK?RTv*zXa^T18XEl14s81lPh%(&Zjq<;) z{BGrcRr#}(e_x#G0sOdrI5*|fb(HcG>%5Viiw0J{t~l}aSpb?3U7p(%elGIL`FTg2 z^eCHwJi2{!#+hD|{Ej}9B>7E)@#YLnel2n0Yq@C#bbUV*C%(qhw$`uKl=%3^indtF zuEoV#q`~z@eMY>uvQ}>^w7ivT>l!NYPNTA^zN)6c;eB~ z<!SmX*kV zocKCjZR_+q$X&a45}|2Xj}U*q^;e4(S-f1LPE`G)F$ocPZ3BFni61;@l_y5wgJ zjBB=(hZ}B|cdqhhD}Rpi!+Tskq56M>{5j>p`b~M1%o$kysOSYh$B^KOuwNve@3Hu0 zg#6M~f01QmvLDX>EArsJw|eX_>8~{8enA`W4QwH`@%Wk~?6WKxAKq0s9vVe#v{Eu& zF1}IWSBAEA9{y~6Pbj}W;dvSS4sk8Q)FGhp3D2 z|4RADuQGY2O@B*BOG4U5|9b{}LAPpoRcISLyz&gg$yC)bpFDgQTH|Y2Gt;i|*>mii z=JCyXD~Z4<7blMJyGnlN!Ma(EuaMYkBcJ~(Q z)ShWCsjpmC=iy-mwe_|B+R8c}&rpRgFKe+W+it`ir}zeo2La@xN59F|+Rgx=2(mhF9sV=LfQk-?!*tDz>OnGtYT-1~2j)gq+_aTCV9_8l@I1%cz z^Lsz;wB+~TYn_?t#eWXqH5O0bXu=-tMznNlcb-(n(+pw8*4R_C#PJVA!{vRI%!$a z&asL>Pkz%-iu6{1OOA)w6JH6l7U_r2(7V7yjc^y>oQE>>!aVQJFLLOE#+D;1=A>M3 z8~rS$BgHF6?pX(`g&g>eY9r*uZ|}W=j};Nl1*Jp*dS%ptfboZ)r=hi8> z@5DI2&ikd_`SB|{nxD-v_<2U1qj$pq`N{ZBeC4O*q50Bw*S80T-!VY^5`{lCQP zRlesdJ*D~9w&q*gEspdLs`CVcsvN(h^jz*}N&m;{e8QX5`Oq`edDQQz^fX;b>}Kc(yy3U9FDbDIi(MA;81Tjw*Dz4`O9eziHybKGnoY&6`lob}>FL-rZXmW@*$ z=irxpkeoY3#_~i?o8sq1UHeItSG?uLGc$TSr>L)Z7JAQi8hIk;c(o{>DDT91H;MCZ zLTVBklN!~$$0geNasxjfCDA^4Qt9z>{#>FwIeut_S&n(|iGQBpz{|6*^8~fV(5LU{ zm4n22L90-I7C?*gtp?xP|L@#-eDSx)| zKcM{Z9#>E3y;z0h|F~ikw9zO^F0{NsiyU$sVt&HbTiFy1)v4OHRO>9@p*->~c^ ztejhx=EJShKdkUW@`#h3lb`XL`;? zsQLBQ#+QF*`l0gg1HW+b{L;L|*a}uuxM;DRe^qJel9I*q@pe@`ybpCT?qw^*y?~3Y zt6ucneHgRzYE<5$u%Y6;2@D6DCat}{dv~d<8 zzN^G3iRO-1O-b6vZoN%%8kt&8---CXVl`$Gym^Uxf&2TMgSDJ6HrLDL?MQszOM0B4 z$Tjrvjx$@VpmT?^URUSdW8MxIzFCp8WVuh3?~O@;d$<~7)Z#GLj{2nPQuub9Fr;+# z#lMYIxoes?v+yn-=`-(giQcX@J|OkO?P{NcGhOZPsF!e^4bmQSv$S2xZc+9&WhW^_ z+NN3L$9ElQGq}^F+Vl=Onr<>DM)Rc-hwcMJ0j_aT%Ad!RztWt+g8~S0kND zo~GJ}9RSPm{j}KIihZtilWlu(4H&|IQ)3eX)&fGRWYQxDIW@5G;!rKx#TN?{_CM?~2f8(juwXdd`!^vg@g8u5m^4sX6WU$*@? z$keW38C43wSK|I@2n4xgGz3w&9OVi@upD``^=X9(!4>pEw|o1DtKVc*nrgGt;(53>(V#Np=%}Rwk}eL9rNUNxt%YE5f1KFK0hL{aY$CavQEpfP*SyHmW2y1F z>s8p+T#ag3?KwhuYT3&AuWo6NsOpjBln1eL2X&}<5LO6$nFsIgkR890u- z9o5{*QH&rO*}@{QENh88N5L+|okKY0*3Pf-puNub`5Jr?kfmZzD*IP?i&H*!uu{MB z{|)Rf-fhUJO6BGgcQ)2d}?{%n}K z5ozKxZ3VBbVT;gGjL>0u)zx6FaVA=(hM}$dfdS^nBq`kE8}a9bSyzo88uh0j^Fl&C zvJNDzN%ni$%C1m$i?X*VTQer}sTH>>2k3SkpRcs6*0DmpQnga^n3`21C_AEk_ati- zMQV=-&7nWkFs|zgQP<@n`e$9mLe(b(W|dr*D)w0(Qnrr43D;=rWoU1!(84ap`El|l zp%Uz^n}-wUlCETTfuA5epjLOPEz~A zCy9|5ch^sX?&02cY8Z}%)*_@%oOdVB5sXc{5~=d7AE{5nZAnf@+|zcI*He#kn&&rP z3s&I2y2;~Pdrc+A4l+nEi12(Ap9g@#zX&s(1<2#ImTTb<{-?3z<_X~XROqt@Soa}?LEdtB3h;Rl^JeCOVj zd7B3P{^#YR-r1_dv$G3-k(>AGRX_gITVFZl#)m(n!?n#BXQ`TR&V-ha)&sGoavo5N z^5&h1>mWgn=J_oOL;3X~@6qD75$27ra-NWnlH_-4^8iJNVo`$fZs;CrRu4OId@>1;l~Nb8FO(s8rb9^Oy_E}BJ**^%Wpn(yWkF?$R`>+?>FREIp)3F zPB?RZPWh~eB-?{eejg)m2$%PbX>cc9H{jTYoiv?r)Gqvzh2Mzw#wX!q-RE7uY;lni zaME1_YEm17RJLH%2`5(@3ZaiUq6t4vxIDz_uVk1jO2K@b@bj_XoAra=M)icoeTa_o zUkoUY$n@ik&j*IQ$6P~5+X+W4LrGSFUT^s!9NUYt#GUl`os;=Zjx&CV$X_@mXFRnV z?|+wVGyKI_Pq?*$Yn?l1JXfCbPy@_XQU>7LU~YloEOZXcOSj5?9!_B3oIAtM_V*{l zUjYAyhJPOX8MlS;;qCDaYmOs+0crxjW%594c~F84V!eWx@~LpZv%W=x+~|NS05!gW zWoep^E6Ma7J$Fln92fMUcu0oZ%I*<=jQHu1P9FgoioYCXyOdp`>Dq=wDi-BM}*;r^rJl{A70?T4!Er`U`_l%@EkJW z#|q`TF?bEaX! zyH82A(mt1Bz1|DF#wR~EVD*y=cQjd%rcT4mFVl(kXjsN&0CzLsYGS~e_$D$8@7TCV#I(zPW)ED-EWvhiQ};a5Zp8lNV{~G)#Fho!%bebQmz5hMRbs z#zb#Nn;me@G{|?_-Ql!txSh0#a~Sla@qH)``ZWgqmKgMN$3~}LV&FP?r=L9SF<@Rd z+>G~vu6|5;cR1iQjLswd+kl(fj`D};?O_UoVM+|RO>m36!af9JW1d)|=8C=1(s4{% zb8hlYexg0m%6}dJW0k)y&osc8^kbE$rfu!`=PGniguR)=iUT;1Y>^z_%QYtQ6FJiU`&5(s`15;$Ds<;lmq#W z_P9QUJ}C3E~Lvks~^%U zf!Slw)N!UwUb_sKw0qHC!fb}=?J?6c^BF6>oN>|HNlhQ=Fi$N8Of23iK%bAPcUo30eAsTf446ZPo3NTL-4Xe8z%ibWnNRvDMHfu0d}{i-Zfp2hd~ZTNS!O--CE90i z4`cTjFo*luo^`rR!<0jZK{HzVl&|Z)FmEM>oA6z#+~_vxW;@^*Z$7LSn+=!_xSg~K zpLRy{de>s$nsl|B^bZ>_8TUu0pBsaImw~Hknl|Y#1x@{U;f^+*YK}qQW#DQWv~_># zYzMvdry9QlX<1)Nv2ezo4vJ~B4vqn=eq3jm`Wr zAJLvofRENMZUWD-9_E*6bVPyqr1EN?l`3={c{KCshKzfAsI@B$m}uqF8@`U~MEcSC z-8}|v4QtxuuFHUN)(I0fn!Cb|UY}j4OT9hhb*TYk*2CWL4k10__GsS*Ehj$f2f5i} zz#N7<`>PlW!)%G8z3X&I+brJ>gC^tj({gt7{&*?U>+K=0UIV7Za1&3{rQ75q7TvBG zd~`YJlAigHkHZE`8gzpBXu1qD`G`eV^X=@n`{LUPAB#Tu)nf>)Cvirj$#R67@cN<7 zp?<-clsc$~ZPM59IFqsmoDMMQQ#Vi#;LJ=N-NQEF*_PREaVBREI2~ZZvkkJn;ml7R z-NTN6KPS}+0v2Z=_JA`3h{Jxw`bHSe3-=b7rQgK*7);5x(1y@=u>a_UoBhY3El02a zr~t3g`VaCS?dbq~wEknCkq0Lp^T{+0MS=OG@|tsQ^!{TbWZc_BEwjadiB>MX;p@14 zNIzQtk%ltu?P08Kz&PtfZ@7%B$co;7v=}s_(IBsz4H&Z?MuXR3T}Url|8dBmrD3(r z`oVmrp#;FHFb1Un>gGrvndK(+ILf|PkKx* zm;~>lZ-B?FN6b&Ohk4NTLgyQ2ghm4uVWynCNNY1-apq@Cct&VBbr^8j4`9B8I`v8E zam^rkjwT=GBiduiN9Px3ltu#;VWxb#kk&T9;+waa@H&Ec`wX})!%Y~{(vMlLq@Q?B z3<;DjDp)i+`;;$D{?j)Xbo}a9=l$2VhWTfK^@5pY%j=r__|&&te1uhwFU4w?<3bC5 z4b#|LTse+KsBT!(ShlRGwywIY5uc`cs>=#I%bHfigErY)y-bNN8#WD}@zP5CCM3{r z2^?1UUTDO={2DG{CnS)NKu-yrdZD~WLC5sv*Ki3tApuGtX>gJ?*bDolBkNbAx;R`$qq@w;^SM*L2Gm_M)LCpkG~@DBi!Ja`+nG<6gr zXdGUJq3QL1QfIDTC#*VM~W! zGi(#s9T2ZMDb{I8r=dL3E0WF|WQ|C&WtmMFWQ_vf!@&30xhd8uNz*Jyqk`qp!t#g` zrpqtsd=y90C~Js!aK)$@EXz|xSu+23zMEo=Rpm_o+rbpc|1wgCtVbtXqfgH8CM73X z$w)g?cXVAEpKPVydy+Rv*P(Ryr@~K~QwK}?WY{BK9c4O< zCm*_QYyO=0Dz5_mWWd|t(+7O!!v{ah4NKgB(Pg8{#I(u3rsK3tJe0jzf0G`?9JpdIe&rk7QB3>Zlf10kXD}}NJp5kB_>q=t zkcGW&-%q|NQm3%E1Ri8eyd?tfM*LO_yc~&FntVkRJe6nSwF|s2;rHtTuR`LLB~R#y zH&&Ga@tz}I#S6p{HAlDOvCNB4@^nDJ{3Q&d;zI3!_4;1O&><^P-ycEv1eB!@WyyFS z{KQ9od%(w%y>6#EK2}+q>6a>gw0$!C8eiKc{8W^!A(yv*Lb?1Fze+ATkK}!-Y7h4! zPukD=e=q94rm52)-Xlnx@ui5@yiZUv(X!As@qLPS@YI8zrmNG@={x;eW_`sw>BXX> zb)bfy0REZ(VM(H&2+3CGL#MCdHC=7%a8s9?bwbl|@=yFy$c5o28+t?2)$pn;L-Nz% z+9rRc=p!_rnr_He`OfZu_MPF+k|$4ymh)dzdAk2i8|&Kj#oyB@d{DI_(+W} zgx@J&&0jQ|`EtT5zt9zsJ9X&L9n=*g@vC&j`KVj_jqyh;+M0J2KbCcAf4ZP~gP^%A zOjFkbla>!U>nt@^t&rno3>3u^8k=Z2>GIY#$KRzGe`~*{Yu4MTMt#wAE*fN=nv`Xw zqwe!b$_CGmq1;t>vtB0NdbH=onp2x_)T6JK+?* z7)wF-v0lu0iuGb9epS8Dw6v{g$+7)+#n_(u&r+{p#$LoR|Bxh(`5B&#QNG|+<-x3j z-xg_ozh_#p>b=e{ZKGaC%`e-bIgg=V%SY4Ea&?58UGl5bWw_ZVs&X1dejkzL@T5rhdHlX4(ruX~()B0*t7p0<-aiH2 z$M{X@B)?lO6L?#aKOF@xXAtJsr;GItmi<&DT$yb_{ukzv z(Ysgab)V8p^hbkZJ=65H?Sxb1Ean~2($n!yJkn#jPP;ewc=%D}W7hjQ&#>Mvz;6*u z^2nB6>b+V2ngrhM_`OTubx6D|V!gy!KeT+bt@BHp?ZZ)TO#a>$>HGn|e-i0*#mQgh zF6Qe@{AR%TSV^W=X4ev4p|M|%6wkE|oEmE8*K^DrqR50QU+;Qfe`|0r_*C)3XU zn91%I?YmFD!}Vq=Fn~M@*5xoMBlof1SVt~*t;c$i^Y@r$_QUvn6b9|APkzJw^>2x5 zWgmp~ZN#l-AQn!k44mwLC{ncBUSa-Az&zF~%xePX&0b;N6EGh~!Z3ek zd70(ynA?rYiJTW)@sg;Yi(p*`gU$?-kzu&gG&%WZpB8`kINW!f*pdQGJ5f=))-p5$b#D%i=lMM>Mq|FEA4{!f857AARQM~Hu{ zdfUlsTjP-@@X*8GSDpqKZjFEh_y#jlZP zjv=*ZrHfAaf8QU>&*c7t{7_xX5Tmvx7^oqmUwr>Hurmo<+o7k z*?)%PA27)yXCSkfCa+CSo*y~h8!P(s@LXrX0d$5UY;jr8G zNZ|XgwLE9_n&%QN&*i=5xmwF}eXn^IYk8LSn&(L^&*on9e5B?1wAVZby{e5rk4E2n zj?v^>vu%adrHex>F8u3O&txjdvz zVsQ+QMl8Db?;I`9$ZmPG?|QK&hm4i)4o*pu?+$AJ|IInM{0-8+Q<&cQox&TW9+h<) zxEJwR(0F!d%LLAskKq^YrW{V5-$jR~$H;5c(e?R0)cHrP&fB}! zY1C!V_$>5PlgikduASRIBW1I;^4KQj4>pg}Hu)M;FhP8b@4Fc1aYntm@A2OcJN9+y z=kYx5rJsrFV4s!u9Zs!|D|XirAEQa^#qwDS-(wl1*CA!@kuq8JJT^+XgM;~6e{xKj ziTcC&n6bC7qdq;>T0d@&`S6j{=lE3Wk|`1U4ku-#t$uIFYXf+U>!XKy^u4c>;veZ+ zy?WU^t?bx6%ko-h8KVxpZ$miU$VjrPLHe3-ch1Gw`bs)5NgXKv&v1lE9Wm)pzAwqS z*Kp3gjF`#Xj{h!*(br7*yl8OecTH-wUeZtPJFz+1cVeZlUx>flr(j!(N=ErerKFna zxM?8vfCC20SB&jJ7GLsUxyYm{NLB|7&Od_73D`ALuBsTePn1--fakF$`+4%lmOoA^ zT1|X;V@TZklQySIBdYv&pbGhwDYd^c9dA00R=eiCJ(l+`;IHv{`5I}rypP55J_DYO z&wGi9^TjScd>qUB75HXvc@KY6^Y>WrbJXeYP1M`%J(zL+A!|wAM;hx~IDF6dva_UK zhlfXfp6Pjv^WLtX`Q0b^Rn~H_S_&N;<%{J6x!2G9*q^legvR-nu{QPjyT|#raUO0^ z&c}^D=<71+c+T^_U`{7`pD|eGx_+FDZ%5WZm;QCeEozdZNol;=k5rJTHnN;Kxf$!Cm(QD4{Cb$xE+)B73wF2{<_?`fYK zNl~Y1-{+_)z`O}9L@FcoU>k>?lil(V;Ncp1I5{j2io^Of@^FsH%H!z{9zHS2V{vQ; zp$muqiCRoaK0m1({QVsC+xw3n;7%KFJ}h~_(3G~p;}3tiA8);WrSL4lu^?{U^K$pk z9RJPNyY4F^-;p(1J`tRcI`@ifoa;8je5I(;1t z*ZUpM|6kYJDY4H;B-TsclfJLyQSZC4XLXIEF~20;LoB-Pr$&4Y#+RSO|Lse8&cme5 zA?-50$MJYFN;%k)FN>N7`AJ5-X==*GeOOyY{l?y6Fy2oYt6y?XvoZGj85!Hy_hw`N z_cg|%)Ji<~v%H|P9>9DE@nw_Pgxh3V)_sjjJ2K~gCFlKvwex-{ZJcLzey4w^Ne%r% z{~T&7)MFmM?v8z5Vt+i3dhCtkz0n7QdS8c}DCc2EcK*NJG%2rg+@J>CC{Ozz>>I;R zrmQA@B;_N4?Jh_H?~YzhHjPY~I6yuW6o2=&R~hj%;+%+#Yv;aVye@r^&tWN05N;=9up{n+BOuJ~`0zNj4O=Q8IE zKaBSE^9C&_3C1**o^{KIDe7*-LgP*I$#x!OJ{)K=m8C>Yd^Fi@F6jO5tk>vQ*Z#=! z-uyP=WAw*(-Zc&xyw_ZpzI#tG`q!0mT#<7~NMs?=ku=hXfNaxQqfd26R}RBjA3k@lu8RpJ_~2>jOm|bjlXjri^v&qILG68@hySON*sur6nlbO-8el_s_!;7~?tE=<(@kqA z6hAix9V)+38KV6^gOtmcau@J$u~x1sR_=JyVa%63%4x^Pp`FJ^pOm|ahx<_K!S_(^ zAzSv!V$9uC(WU0r9l`*g4*UR9z z{a*erO2$Z!m39t6%#y5pPV655W@=YSY z?&lug|NMkLWIe^>7GyfuL9|$jd;k8oR_;e0Q$Ce)(O9`&zBjr1`((o@L8E7b-yptE zM&Xk~Jp7yA|2E{dB~>c9f&;xU6!D%QAg6?BsG{iG>+8C z^8+RQ`rUrKNIg%!z?`Bkq5NreOfuO!%StRLW5jd_*RMS&`zEe0af9|*y8J(qB{mJz zkjw+r;L=;v;Ov`u?{`DzXYie`|Exdda#jCGiaXh1vP;>B{<;6LsV0@0eXg=?HL1nO z@#XHh)io_CJyoTb4p8ZZ2deaJY>p3_?|R&a=;QaQK3C?yW>Sk#gnkM;`#GF`O5S?C ze{hIvpmLSIoOEc?D@ke50g=>j{}A5VWlf5|etzAfl{fd3>xx7Ngi}NP9kH}yk4{xb z^QQbT??LLI!Zg({dw_DkY*K$g!&t}iC+ogzTIW0+l%xht(|s8Uu=ZRM;*xj z!5*#-i5wIj7&_3M>NvpK->!v*YtjrS$cuW%(kn{r1R8aye_eInDl&H z-+y4z^Gch&`h0M3q9K1nc)xc*(k)tko4y|Osjcl-`r5Sa;di3`#6D}ZCLNZ_JVK=h zsKe+N*W-vtJKg$eBzRr2%Dr6WFTR~WAEhOAKA5qzue@~0H~rAg zES<(9$Hh!LZFc>W)26Z>e35&A%3r)6!36!i=I2^A6--dS^fyd;M-OdCN%@wJ{KdEP z^6^^I9uIO4SNV(27v)(?{$5L#@MI7QXNPl~Z{d@YwTxxnK=O?{6!D}@M{2DYY zu2J2TZ!5`Py#M~#r3dsJF?`6E3ADeYSbqJP2Jt_G^hAC{6V!iH0zVEM6sytEL;W>9 z)Nf7i?tlIX@!rMkUmp=;sE7L7dZ^!>p#Dh->OWjhvzQXcCx`Sz@d+lVzuvfjPh2-X z(*5`KQ2)Xn>W?O<|Mv;ji~X=zW04-}@93d^>tWsFKQ6&K`AVykGa?5B=I!_gV20R0y6DITBgCoBGEhySNMfd6VbkS|0GRK485!?;Pu z$)zrrDk-igak}z4t@Gk;^`S~U&SJ0A?y8)$@<9`=}j_3ydgTDDGa{pqK!<&8a0eD5mBE3eC+ye97fweJIH z)St#|t*mbLAwdL!v-TX{lXLSfa@LeM-Et2mTa~xeX4|7wqWsf}ON)wSb1tfI&M9&^ zD`t4hx)$xRoQqpOb{>|94mGe6_=DatE#${OjNGCVs^2sTzs5SJddEvlSeG_A6_qO|H? zh+9QvQ8|u#rWcnudnn#(|4UrORaJ5mvZCVIoNyFRbM;uwKGx%L&M2?KqaK2pc;0o6 zM@d<6#SCX@QI(gokRCeJTRh8MF6RuE$}Fe1tg^JIq}b)^p_bm~qfw9MM^VhHo+{cG ze@dO=m)BWDxXWi$XiGA|RMGaG-s+2;QznjWf^rv)%JaI$bnb0=g#~e|GEshgK6+#n zx@`cOcYP0CBak4Dlf84@PFrqf@w9T=!?rsj!WeRj%FX&vtR_W`4$$nqrep z$avPZ0sVQRcE(L|R(U<;CG1^eWYZp3F}<>t<+H)4q>ZQHCSDz>@UHU_>Xyt#Z zOTG1{yt4PU5_PB7`u4xNUgLp(J%u5WYubcK=oBIregB*rQ{K`jeaK_OM}>I6eBj->Xk- zLEdUkj5&Vo)j91wQ*TpXKm02ysjQgo0uv`*c{{!#8=-d;I?I`<2)qPi6dD zm;aeb7QgVig{3oLpZsAMXYrXP2je2nDOu;4);!z3O_BYnU*52A`cOdwdz~-)>OrFR zdtU;H_gf=iiR#;*{h_n=uDkm_ZVHLp-@pB!(}%ciVb4QN?7+BhBWy$_#@_wg*ZrW= z!>&=FN%aNNrLHHbM;?SR+JCbrmq z=&d2eCGGW z7ds5o<}0-bKh5w0luf@Jl7_Z%J%fM{&7+SI==lRI$5YUE0b0r(0EkS7WU-^$eE#>Eym618C4;Y}!l9f0dlCiz3~bF>Zr)ot_( zwPM@gl_>BKJ_ykcnLjYi&s@S!Gdvbq7z-<`NAvJG1poRc<`{7cz_(C?j1j!{cH&I^ zKDZin;C~Pvc85~K@W%>gAv5*(;BsW4zd`t_q+>^5_5%9&0{wywD1;q?Pu@w~$Pz^H-&gMrG7#ffp=hPms1? z16shG48pgOgFF#rSumdoPQn^&M@Sq0T zz)v%DAVs?Hcc@AHhcBUKi52_-)!>_2%2=Zu85g)2)v;HH;o-{|FYyg7M`q?p82)@Y z`w8hb=tJA^(+?j(^Qk`!zd}Q${ziO6TgYRE8E6x>1)hnr@Y4o=k20_W@Ew#&{b~hm zqAY9+{57&kyU>Ahu-)+cHH`Z@@~@>kDDoD4ftLrBTF;zuKp*nsk00K*j`+}*037@P zdmZ!H3eSCzI>jHj5shYV3B&J@owm$>BOWMhMMp*0{cHhU+B9TZbvTsv213)LG9FUeO9R>w$OKcu){N+WB(!D4KIG4JnUV5 zSpEXzE8`A7Mj6;D%)W<4Z>Ao20cyu~z~3P&d3^9e)I@BX31V&!H`}6^3u3EXFhfKS3GTQTPM0VXK!I zca(~4hDW1(YzsUIrBkOBo`X`bZEzwAW82}a$j<&BfM>RHoWgd%Z&9Jd_GP}Chth~) z1kQbhc(#xqp7|<%lJ0O*`+rNqz-k3u2pvA`2ip~M`X zjYdn%;RMt|9y`1YWs~lJGf|uP39mw7Y#+P@Ws=7a??o2c4Zu~%OL`DKf`a4^!K**Q ze~#%s_zD`qI*P)xdgL2cMRSdUuB6NHETlbCG4e|RUdGnaz!JJibD zHt*oFCMc6}bi=FBLgu*-E<>Z4GeP(YN|8AO`+veXVw>UVXddg`1`q#?b;>z|1zv~j zoG1I?<7gZ6Fbsb}JF(4CrE<}NP52M5MFq6whp(VIe2&7QpR+%Z-wIzwer)vxa~5Th z#|Ga=R&4W^#0jNf+u>Iz4coGly3q#u=z#sc!hdWloQf7o-{E0jbN+;FgD&L7_Q79# zLm#p2LS)DG!(VqWC$Sx{4h_Q&z%k!4AF$nU5sF|3;pFewh1t89_t|z*f!`u z^ReB~k2YWjU{9wZOf@;w^)NVmX~kc)IH zoPsjwqZ_7j&7HROq{DxpH0&rGcL42TJK&#DRO*L6qIslSxQ5B4$VR#w4(A#XQ^bb1 zp?2nZ06vDAu)}aCS}*gTYkD4o);~hKun=v)cED>;0dw9D*P}x05PStB9fTjEc5E(|rp`dw zw6~-6%YQ>l`&;TVOF-FaE&4qbBSy{0=qz1Am4P6Eq*&2Irs!*napls>6=J0Y}mo zYzw>??Z9@!Kcjh@@Bw~^T-aRfL|uw9nR{+{H}c|V0KSUmVMpQdLunV=1}{Mc%tHtC zp)hOK4}++Q{UHQjLn-8mz_g?B=YITwC!y%yh!32Gnu(zsu0ajt3BeAu9@~6OlA3@P zVLRYL)QTO1@1srF>Sv4tYNJj&yau&k`{AHt@sqV_fetkPZDI>wL-Qybh1VWO9HdV8 zE%IWUf1afBP${+@UWIl(N}aF?DSQsWk5L1A3m1V=!_fk48@vK-BEJu=L+i0a@MBbp zt$sm%RETYZSD{O9_T@3 zZ{ah17&TEg3{As`C$<@$h2~+~;q}Oc?S~Je1(F|{P9koSAD)GF&__GG9&N++!-vsO z$q!8@Gv1OPo`qT^KfE4olKk*tlp^_|=@ep1x*48@Hb{PWJ!+Etl7`&ulVNzYHA&TA zTj31kW?%Ker%@Sp1P(hjN!4Q8;9TUu_QRKv4?7AE9nP@^+X62|8Pw^9jVQ?c3Be8& zz&4-89)SGV4!98cu!HbXWRdv*-#{ZcMnvJjY~mxw3wSP?%DS+_3RFW`A6$nTIUfkY zH&82keiRP;HTkhE@N87b+O)%&Xea4DcrTiV9fZ%KQu0LL_o$t8^XWx~)l!6_AkDxm8gyBDti*$8Hk~#`)BHaonpaN_Myh?2H_~9nhM0y0K=P-vQKP*L~ z$>W3dXuBNu;a0SXI7Hx_Gw}gC0M9s!JlGC+71|)jHMk0G;y4?EZ=k8nhbSC)HtUph z3p^XOyhwcDOq5N!58jJ1Srl7pf3UV z2=bC1hW|uE@lBmWd{BUKx55c%KF2Hvyb5i=_QO?Z5q1c^fm*SnaNxPD5sohwcs9z! zw!@id2YGz(UNnOAAbb|pVMpNis1)0r%lM*A*fw|>YQlEIThLIB9Rc_V+DUpC{u9~p znUhg<6iR15x55c%7`6jmg)%AYhpW&A>=1kdE#f#5g#+`bQ;sk2Y~u|3EFW*TJMw#F;#1n2C0= zhgjhRw1~OkfLEanr2FA2G!;7p-$28#qj2D8<|noVo{yTCGY)taYLK#U74k}1_y($z zvT)!S))#GA;MpjhbUU1hwvg_F_o7|cLHI0Mj~#(OqEzOac`S1eS!M3Q%TOczb;Daw zEp`B|LsRK*2!4RfvUfrEZ-_tV&wkjB=Hb7}XP-eW0bKfuS) z7Wy8BKcQ0MY(Ag-Xg=w7cmo=Z9e}T(Z01rF9&b-l5&XBo*{JbF>VyxWMZ_lzzeOXM z=jIEN)R|}=wjItz?bv?!5Go};VfZa_;j?)X{-ad-ZiVL{pNuu!jQlc=P+iELfNy4K zK`#8aLK~_R|6x5UB~~FgxR7xnR#tdEno7C@UW;}~JmF)gg?7X6#L289Y&+bFI{2(Z zU6iCIp+)%Yh7X}uVjG4RUd){2b37k>0?m`pOX0CoSljf=25&-Bc`qD*@1S(bs!JFj z6lGmoVFfD1_QA(cn#2bld@23Hw!q6#w(PfX4cZ{{9R7&5$r`^bNu7rxtOWU7eS<#2&vY_p=&ub{ zqnuXSg5}ehtBjo+u0^{(pe+2^4301OZ-XD9W^79tYYzFa{ct4;Vh7=~s1Z8?A1r52 zqunSRcm-=#%EFIPJ3d=xvUj0{q}$<;OFDcf?2LD*Og*z_xgj)L+psIljOANP?$Y71iwP-vCZ@F2W_FO1Fk{&XL~2>#gMQiMvNre?vXgGDB@e2hemk6kGS-qG&O%w(Zg@4aVEf?h z$ci0+hup**?T_^3Me;x&vQobvHlZBs5c~|KVyjzOcgT!wg~h0YV}~2wgECo1 zLHHWlgdK$^-p0Nm$2Is{lqPY8e?(?{2*6j+LOBM(VSZx89I(Nw&Q9`%k>3huq5%H;;Js)&$F(3l^mg*d@f}V;`Q&%NtI#&ik^Inl2mUi39Pkcgp+r2ta~^=p&kdk1x4s@0KR|}Wh3yAKeN6_x4?PGhCc!L z25Q8P!Xft(L&n7lFF{+d-EcX|psf)60_`9^YEhCp>@U<${2lNVzlU z$G9_BZSV%vNPGe?h_+ES1jDERI|8F<1mmdwN*vI3>ajp83gW*F+L4d89MFx-^w9@z zL1}WX1@A>c(gW~*G@oNx2tI|Pq=(@vC?fHJA0WH<48KP0(#HVD9h61i?XV6h>;PPY z3h$>*_ySr;dIWxf<}(&*F+QN7*kL%co^h1<46Bwf?({JTKSEJrq8gIaAC|I?WIn?P z+R1v1!lRbsgTw)rpe^q(F7P(gPW}LV9EFKb7=DHdNmq>=m(gh2vcmIGlzJTSI#f#x z{ct7PLH;1zjM}6gc;O0sCch6ZLtBV#5Wa$Tl0O3buVn7Yc`Q5~Eh60pr=uwKxZz!> zjJ5(WZ54eX4pulHStTZL9SX=CgD;{C(j)M5lux>9;`1@&z_!3|P>0mBI!RTc2=)8m z)$8z+vGc({JwRVxr%w1Tvf+Oe4qngkh?ra8UC4zUfm-h&cG+W>j}SdEiNpa{fWTY;Yqg#E!xP zALG0Q+YHN50r`D!z8z`?L#vt*kHd3$tG)opJQRz-!b`9^o;s z6Cctoa4~9<^D&tHCNalP8*D@EticEz_7?k<_yZf!PV6xJ25tJ7aete0A>^X086J&F zrN8hLVv!M-3q!#|>Q>h#0E zpaAIsxEi&~dky#risG~SlDUUUzo9R11=>YhA$Z_U#)+~PI2nzWI^mOO1a<@-_7&rb zZG}!$NPK+oHng64qVOl=rylh+doK#pM>o6{?U4BfKSSFjKfL@K#)38CfVZM{>I}lS z&@hQVv~-Xk+X@{h6Wa~1Mj_e?!bef5^aVbLcG6ZDzKJ%F9)X{rEu=@`52zJeean19 zVd^o%qfr6*E$}3?o;+4)LqqAia01#vY#neWsw3SEuR@#fEdov7(HH75!|PDiyNnCm zh{Cc?;TLEj?OMO5AIM9cQFzuae3P+*k0P7+_5;45pyY=apIB zggo#y)WO~of!rfkMX~+xj1&`JgrqNUE7~S&6MleNrSJW?uLr87Ej#=zN}(+WoP}Dk zeemy+PTxcDPY0OP2KpF=yHFu*Ik=ZY9crU3KeTf%Dlhwk8?Hz5r7S#~o2wL%-v;NS z1*8XH2zl9C)IgK6h_J1294f$ez`3ZSjr?#k%6OT+zyWF8_XOJl$D=ZA2fP{8VF%!5 z6qNjM0QX(Xe}(*TJZi&sz?+f#Rr15ls2w{32OLZsq<%OaxgrPG2Mi)6k{^yo zyV!pMumzj(00xjG_t&BM!oQ`T3UpKrOxtTLQ zn8*Ev%>G_dDP)1bqw;+mIXeDEYxG=e%^(smUIW4kJ{d2p1}2J z9zKNNTPRD)a=*QiD8xP%grA~CvVOVW!ucqTId6x5Mf0d91l!Si>alXq@XJvleRsfH z(I$Kdz~|9a(j)NTqfBZG=@xh%DwX{3R@6#*1ZHxN%oOSmz#q{<>EqG(gUqtW9>YB} zQB;nJ@YbI(H;9iPK7y2-U%@;Jv0~q|Lk|j&-w#75h%JnuEsP`g6jv5Bj5T6~c2r88 z4mb;KlCg$wp=NxG!Xu7j{>yO(wxR{(vHzU8hML45_v+=XV-yZNf&Su~88)I8@^H^Ubrc#+x)n}9h1d>w6;jxKxJuHc?=XrM5>MffUlM=z z94mAnH}lO6m!TTw5*wO23i+7>LHI0+(8mb;9?fTb%_o}FiO5PG8@vp)Fcxlj3u==6 z1U`Zqm@{Gc1xjbms9`2`9GXv^R`^@ANX8xh5v566u>VQSXZ-iWzac9z48azZN;>yu zQ%4~Ov9iJmC`4HYydI5~Jg1n{X()uBA@~8xmbR=Wm2@imCv{rjX5?nPqEHPdKEzPC z1#Kfw7)DSk=~4Itn!;XWKFy?#Mp4<9;7KThJ;4fXXgx7^z}wJ5d+dmqA$Cy-D<~Cd~7SUE1p8Ff(kIxZ!U_Sc*^|;|ZC_~zUThI>b35>%Z zlubQ>@h0^u@={L}stL>o(rs`&+8}cVUV(;@$5CKXOOTm7K{)w5=7x*~+=Avaj<)lO zKMIoH4qvb{e)wj+z@(<32I^FkOlmkvCyx&%UC0<=TcHbWAx}io3fWsE5424tR>ac{ zKSOP@CtPe&$D(cIx5DeDut#AB;1j5Tvg#7bqMeitK=Y*te=P7%s7CsG8GS?r;{R0E z78)jfgr8haOc-~wgSm-PDJy)dnE5Z`KFy>)LOY3Vu!I;QAb-GZQcoiv>E_?j zN7Ra~Je=pCFyrEf527g&|0t?BHdnopcAh1r?GWfI$?+jtEg5?W#G91B&3M4PJ}3h;J}} zvSoa0@CB7hS(r4JIEW8d;sbJ#9)(F)u`ftHa4aeyPXMk%wZz0X&!iqeR{CYRnsZL% zB99N=fVN3I;eu<}a~RX$b?l93o9y|&H>p!lJ~6bxBD9^h-0)^(Cp`oQ{Q;lx#|`g7 zb<}UU5r5Dw>bJw$sFD0WxD0iWKL}qz3uG){|61mQv<2V3nKemUkvfhe$WLFae#Q}{ z5i9$jOzQWj44=cdbAP@&m~XUeg-(=7{b9Hb+334(0e+!;?BJay^*IXCmgO$?I;5z_ z2cJNT#21#VK@F5OuOSU>k#^y=D2x1l_%L#jKMcP?Q?b=r-j|_LY&*OX z)nWVL-;ozP1iwM^u+2f{Fq)6;fH$CBjD;VrLIsi^zJbXevQ(wZEz{d#16r64-zkIH*7@dk`9mh8*@hT z!?XU*9**sU_n{5=6NEdOi6^%0A(Ofj)k!++_b~p5&(Mw*VMiZfer}+w#2;RRHc4z@ zBie!;gwLQ>>@ZAvl=X%ke}783cjZ`Q`8-AA1neB_`1^6ny*gh+h0zp^?_j&Iar~IoNjiIw$1rxEEEcsk0E{P1SfMxFp%+QPn#9femt z!}v-%d;x`}9{97(9FMTAa5bvK4#T6KWo}41EJ5?J-SAdqr>y`yVGDi1w!>SWV-LiR zz}e4}U-H0LUZ8$#YnVL~wMjbMjKY#19`Pb`{!8+}$tWG$0snx8V*BA!XeV|AW^QE+ zu;cGpDfhFy@+HQFbm84-0k)%+W7*5ZA3Oe@kaEAsBVHkWXNtyCu}iW2@Z&b-47Tqz z?vsgHC>w;;uhX{JF#QeI1GW`TMj>p=Hj}ytHB#0MN4?4MOX`6OP%U-4AF@xPP1tHXb$&!l#QxZ%E$peQ(8Aog@oQvjT`{B2!6x;H- zNnMOGv3>B|FNg)U8@8i5Z1b0_WwZ#}0q39w$q(;BP1r&B6FPU^GJVfFCEX4W|A{!0p01KrF3S2QMNNfCNy(~}JUOrgZNhfL z+mV$#izE%rCp`>POv$PpI~|^jwqWPOO0)sH2A-Ontm?2Guo3OR-T+lfvMR++hkrn3 z>fZ$4MLF0z;19@4o|JycY8YCNodZ8W4N|s$vbr1X!fuAG$VS<1@Dnso%5o3oi;)Z4 z1s_FIv8~(-_6B4pzhz*u`iy&8Z{qz$6lSF-tG0IXz&DOaRvp;ktYozn*(n=^U!y75 z>da*IbJV&vMdLW^2)3{m?Z6hUK^@q_R+JJ<(fA#<8C!VtS;@+WEgXgFu!Yy7h1kMI z)POB~9tE-OXD6#FaE ze3JPC|ADfx!|+Qq^=bNh4*f+gY%9DFwUfsIe~)%z`{4s9t%YL{d=Cx9j>1FF#s8;> zIUI%7W7}aBni|4?xCFVdgYYHPx(WYba&EF(K>cQT8cM?l8!SN^u-)(uR7jlx_z#pu zJz@AI+Vl+m+wdP{ug8CQA#!0m;O|ibwjVwq>Holg_#R5zjQ{Y^ykymcZGoduD|zg& z3QZ;EKDY!GZp4515~{Yg3bBR# zMkOl;w(vyc!WLeDYOsZK(0pv+UyvVL_z$!QTlf)b#16{f8l_!)QN3@qmb)E;sa-+25cW}MQME27lnuZhP{RNDHeDo znuqO&4O$gz9V|&!&mfC@4hcU(Q@hywiofr#F9LlL=!-yK1o|S-7lFPA z{M!-O`cglwj!in-b^hBG_fWy}TVw6*(3zsQccf0M&d2oEE$y-V!*!0;>Coxb>DReN zFL$5Lv3h;A`tvn#^6%bo$z2&(q$BF<<93 zIydMvZ;QQNsB@vtls9APHl4TXd|v0!w_>lC>TJ@vL+9|fW3PL4uGhIq_iKdCuV0Pj zTd&it^JJYLzY=@BQD>>np*mlGIrjQ}I#XI>&!_3nZ8|MFmClHs?=hVXI#y?wom@&x{(R5qjNw1`=+^g%|5OX9k=VEo);Uz?@jB1d zIYsAuoj>aHvQdBjmd;%|hrSc5H%I3roh|z7ex3iRe)Uvu#ecY6zp=0D`{P2L_5a~< zJm%e4ybsrzrt>>J?(^S^y&ln@x9NOd=SH2&bozCEs+W69=ao83b!O@9oU1=SPG^6e z?fUDRb#B$uoAu{SIs-cI(s{E^m(EE#|E8B;sq-S8ew~YSeysbSt(WU*KKyT`XKap* zhqOP9x#14=Q^DtVr_CoV^~ru*LvVN6!=yDo)lW4(vpel;nOBsJKD#?D%G%7|*iV^X z*qwF-@hGD{6<6Op(xy;fTU^?Y#LxP4Kd$w-yKFOa*tco-^Z1zcou|d((s!N~KP&d% z?mJJ5&7;2awEdeqedlShdDC~Ewtw@c?>sFwZ~D&DdY?Dn^}ERFxzOnyRasG5F87X} zSX?zz4NV&DEU7GY+AFmi^Qx;&zj1mCimSZZoi@3WX+Mg@J-;z|Tm_}MTl1C9Q68+4 zV%LSv5~*)YjruvYxt!jvuc#lT?egh!d8{I*-1hTgengZqk()7>kDTju7FJ$dUg{jh z4KURi{SC9Qvh#IylK3^M+T-EI$98VJSvETE z$&)LLyZVlj+Y9D#9oY-VOfp_CR--C+_W`HXY*Hra_W{;QoImXfx}+*yPQB0tCSD)q zs;qKOaL(n+=JJetTzci{n3_CwpQI3|i;7*P}N`(UfktY}W2xcS+t9`=$27S~`0ks1^D;gh-!HeiqNKQbMwz#$%U#DW$tyaK9M2`^&Zw*` zwe5M2$_vJfjJ>QXlXIuLif2?M$dFHQHMU>wobpocN1mX9@njg&)9ZP8MdORc6M|DD zx^cHu-IrUOj+6LP`%M!4{VY3KFZ)4CZk4;*TVmVK@-p`M+Sq**tD)jwsiA1R%m6I_ zYI1U}a~i8+f9n~?C~?!t(J~ItGmdUAcevbrZ`yqeWM>s)Q*zzhM^x@43}Uyp-Ono2 z`PA|6c`=1p{F@z7Rm5Ui^(^=P_arZGingjZGO|wZjDM*D84+$x%$O9@mwz)H>S@iB zf2}K;Cphxdzgvgan@Q|4_KNa~{a!G#SoLXob^Ll|$FTKqc`~Va&B3^d;RRaQEdz-$_maKGx8$2eL6P=9Ic7RiDocT|@@S;)nQ?sq3wYMW@t69!jC9~YzTSBwkbNTYKShFf;JJq>jah|}B z@>Nk0({}ecHg_nMyvKD*FLg2~m2(wWn;BJ8Q&{GyoHM4T#Oao|VDb*INRr2Tcw>}b zTv6(Bda}9ah$?bddz`ETmr9i!MOAKRN%?fn(aK_(lsYhBdQnAX?6hAU#p@-pH(%dw3UivASr~q{-(Mj+;29Gy3r5}qs;nrQT{Xw;;T`()qAKnp)cw`ii>kEyD;2xTmG+xcQdQ!fD|O5en|F;|vOxUT zQx~fuaj?QUr%Y`$6;(OQia62}pdxu|{Wo5%te9RtgQ-+hQS4#n6%~7C%;w%u5flNECY|K4sk_*%`KQYf9=pd`C84=7Wm0i@6`vY(uHmmzCV5?>irrq~%%?TlO8P<43MwmS zR=daQu^Ch0^{_*v^y8I_%01rdV%Ow~Ipr0l+={H<OPp0zYE!>LqrY}NiC^K2tJ0rTddBim2)-IU`dsR`&|8d4?$}2?cS=fZfg6ih zxl=Bvc6#R8ou28H9*!6#^tGDKdemv%eHd4&&WKB8*K-z^lBfnGOIvf9JF^PQXF2n# zWCIaPWhad;uaXHC%dGDcqnO@R>gKplu`VoN=H*kJawP+Ip0+^Unmtxqqpi}i-dV|^ z)B*kVfNNdg-i%3-Q*S~&XXMW-EoG3I6=$WKU+v{MuZ{hMURLr9c7t(A458E0YT`6rUbO60r~X@k$hNb%DzYLr8#v((NBqBcjg9;$Pb#*G_2 z)>Anv*1q~BpL)eU#1h|QL2{N(EcR5Dak}JD=NF90n_Osog4O$Hi+!*5MW8PNi6am- zD<4-_8b{I0|HeEp*I-k*0?q#KJ+5~b|6hs#*H*MF0C{phtFx0l*Z zJ5DP{$tq5F`Jc79`g2k|&fitj3Ws!0`E1T_W;m-lUyOfwjCQ8X`@jO{Y^TfOlE-sS zD6Se;F}rf6({qBQx;(E$PU+4$VS2Hv%6Wq2l+JoaoYHeEBTm`FrxB-gc8=mBPBG%f zlfGHl7lFPA>=^+czr`lSyx3B2t+&mBv(dSAW2K2RU557me3BlXdGwZy!{vc$T? zw#2@~vBbT^x5U3Bu!IwO?Rb&3CTC6Kn(b>k)}*e@Si5~KH|kUc)R_^;3giUZ0v&1@|R3mGNqxpp{1d%VS7VIL+a9;Web-zE^A)avaD^{ z_GKB3IgK@qD#&kfsjFix4g{OASz5ESd1=Se{AE*?l`TtMK4tmBnYt=} zRqd*_Roho}tV(TaYsy(&v$}b8$LgFlHEWvbSz3#A3svp(ZeJN)*|Acs%2;Jtm9@&cDrc2#6*q5H znbgHhR%quch zSXN}M$X{Wn1&7v#Z$<42|B8hx0xKF<1ergf6>TdbE4HtQGLuqQnpbA5v@oL9l{qVI z%x^nUapRQ_zx*p3R|c7Bu~9KbBz2X!b42o2*@>Q;dFZ1De&$1fSO)1!m^l$4s!@7n zZpvt~G-Wken{t|LP5DiBdg-R0zNXqHf78OI0C5iz`A}0!Q<(J>X-ZvfUfr=qtxa8P zW|zoXYh9bO*2b!`ubr~i!Mt`eCjPYx*9O)$t_?CeVa6xIZW5JM$4#2~eLYZt)POkv z9bAV6w6MQy57aI;R(tBQ{4S%Dp^c8r^jcY=|[], this.inputs = const [], this.images = const [], - this.groups = const [], + this.groups = const [], this.progressBars = const [], this.bindings = const {}, this.header, @@ -103,7 +103,7 @@ class WindowsNotificationDetails { final List images; /// A list of groups to show. - final List groups; + final List groups; /// A list of progress bars to show. final List progressBars; diff --git a/flutter_local_notifications_windows/lib/src/details/notification_group.dart b/flutter_local_notifications_windows/lib/src/details/notification_row.dart similarity index 94% rename from flutter_local_notifications_windows/lib/src/details/notification_group.dart rename to flutter_local_notifications_windows/lib/src/details/notification_row.dart index b8541b1c9..0d51d465b 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_group.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_row.dart @@ -2,12 +2,12 @@ import "package:xml/xml.dart"; import "notification_part.dart"; -/// A group of notification content that must be displayed as a whole. +/// A group of notification content that must be displayed as a whole row. /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-group -class WindowsGroup { +class WindowsRow { /// Makes a group of multiple columns. - const WindowsGroup(this.columns); + const WindowsRow(this.columns); /// The different columns being grouped together. final List columns; diff --git a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart index 0d2f7a1d9..1d0c68303 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart @@ -28,6 +28,7 @@ class NotificationsPluginBindings { lookup) : _lookup = lookup; + /// Allocates a new plugin that must be released with [disposePlugin]. ffi.Pointer createPlugin() { return _createPlugin(); } @@ -38,6 +39,7 @@ class NotificationsPluginBindings { late final _createPlugin = _createPluginPtr.asFunction Function()>(); + /// Releases the plugin and any resources it was holding onto. void disposePlugin( ffi.Pointer ptr, ) { @@ -52,6 +54,7 @@ class NotificationsPluginBindings { late final _disposePlugin = _disposePluginPtr.asFunction)>(); + /// Initializes the plugin and registers the callback to be run when a notification is pressed. int init( ffi.Pointer plugin, ffi.Pointer appName, @@ -88,6 +91,7 @@ class NotificationsPluginBindings { ffi.Pointer, NativeNotificationCallback)>(); + /// Shows the XML as a notification with the given ID. See [updateNotification] for details on bindings. int showNotification( ffi.Pointer plugin, int id, @@ -110,6 +114,7 @@ class NotificationsPluginBindings { int Function(ffi.Pointer, int, ffi.Pointer, NativeStringMap)>(); + /// Schedules the notification to be shown at the given time (as a [time_t]). int scheduleNotification( ffi.Pointer plugin, int id, @@ -132,6 +137,11 @@ class NotificationsPluginBindings { int Function( ffi.Pointer, int, ffi.Pointer, int)>(); + /// Updates a notification with the provided bindings after it's been shown. + /// + /// String values in the `` element of the XML can be placeholders instead of values, + /// for example, `{name}` and then call this function with a map with a `name` key, + /// and any string value, and the notification will be updated with that value where `name` was. int updateNotification( ffi.Pointer plugin, int id, @@ -151,6 +161,7 @@ class NotificationsPluginBindings { late final _updateNotification = _updateNotificationPtr.asFunction< int Function(ffi.Pointer, int, NativeStringMap)>(); + /// Cancels all notifications. void cancelAll( ffi.Pointer plugin, ) { @@ -165,6 +176,9 @@ class NotificationsPluginBindings { late final _cancelAll = _cancelAllPtr.asFunction)>(); + /// Cancels a notification with the given ID. + /// + /// Only applications with "package identity" (ie, installed with an MSIX installer), can use this. void cancelNotification( ffi.Pointer plugin, int id, @@ -182,6 +196,10 @@ class NotificationsPluginBindings { late final _cancelNotification = _cancelNotificationPtr .asFunction, int)>(); + /// Gets all notifications that have already been shown but are still in the Action center. + /// + /// Only applications with "package identity" (ie, installed with an MSIX installer), can use this. + /// When your app does not have identity, such as in debug mode, this will return an empty array. ffi.Pointer getActiveNotifications( ffi.Pointer plugin, ffi.Pointer size, @@ -201,6 +219,7 @@ class NotificationsPluginBindings { ffi.Pointer Function( ffi.Pointer, ffi.Pointer)>(); + /// Gets all notifications that have been scheduled but not yet shown. ffi.Pointer getPendingNotifications( ffi.Pointer plugin, ffi.Pointer size, @@ -220,6 +239,7 @@ class NotificationsPluginBindings { ffi.Pointer Function( ffi.Pointer, ffi.Pointer)>(); + /// Releases the memory associated with a [NativeNotificationDetails] array. void freeDetailsArray( ffi.Pointer ptr, ) { @@ -235,6 +255,7 @@ class NotificationsPluginBindings { late final _freeDetailsArray = _freeDetailsArrayPtr .asFunction)>(); + /// Releases the memory associated with a [NativeLaunchDetails]. void freeLaunchDetails( NativeLaunchDetails details, ) { @@ -252,12 +273,14 @@ class NotificationsPluginBindings { final class NativePlugin extends ffi.Opaque {} +/// A key-value pair in a map where both the keys and values are strings. final class StringMapEntry extends ffi.Struct { external ffi.Pointer key; external ffi.Pointer value; } +/// A map where the keys and values are all strings. final class NativeStringMap extends ffi.Struct { external ffi.Pointer entries; @@ -265,25 +288,32 @@ final class NativeStringMap extends ffi.Struct { external int size; } +/// Details about a notification. final class NativeNotificationDetails extends ffi.Struct { @ffi.Int() external int id; } +/// How the app was launched, either by pressing on the notification or an action within it. abstract class NativeLaunchType { static const int notification = 0; static const int action = 1; } +/// Details about how the app was launched. final class NativeLaunchDetails extends ffi.Struct { + /// Whether the app was launched by a notification @ffi.Int() external int didLaunch; + /// What part of the notification launched the app. @ffi.Int32() external int launchType; + /// The payload sent to the app by the notification. Usually the action that was pressed. external ffi.Pointer payload; + /// The IDs and values of any text inputs in the notification. external NativeStringMap data; } @@ -294,6 +324,9 @@ abstract class NativeUpdateResult { static const int notFound = 2; } +/// A callback that is run with [NativeLaunchDetails] when a notification is pressed. +/// +/// This may be called at app launch or even while the app is running. typedef NativeNotificationCallback = ffi.Pointer>; typedef NativeNotificationCallbackFunction = ffi.Void Function( diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart index 63de30dff..148912bf5 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/base.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -14,6 +14,9 @@ abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatfor DidReceiveNotificationResponseCallback? onNotificationReceived, }); + /// Releases any resources used by this plugin. + void dispose(); + /// The raw XML passed to the Windows API. /// /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index a17bca01e..931ea1dd8 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -25,11 +25,15 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { static FlutterLocalNotificationsWindows? instance; /// The FFI generated bindings to the native code. - late final NotificationsPluginBindings _bindings; + late final NotificationsPluginBindings _bindings = NotificationsPluginBindings(_library); + + final DynamicLibrary _library = DynamicLibrary.open("flutter_local_notifications_windows.dll"); /// A pointer to the C++ handler class. late final Pointer _plugin; + bool _isReady = false; + /// The last recorded launch details, if any. /// /// If the app is opened with a notification, this can be read with [getNotificationAppLaunchDetails]. @@ -40,22 +44,19 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { DidReceiveNotificationResponseCallback? userCallback; /// Creates an instance of the native plugin. - FlutterLocalNotificationsWindows() { - final library = DynamicLibrary.open("flutter_local_notifications_windows.dll"); - _bindings = NotificationsPluginBindings(library); - _plugin = _bindings.createPlugin(); - } + FlutterLocalNotificationsWindows(); @override Future initialize( WindowsInitializationSettings settings, { DidReceiveNotificationResponseCallback? onNotificationReceived, }) async => using((arena) { + if (_isReady) return true; + _plugin = _bindings.createPlugin(); // The C++ code will crash if there's an invalid GUID, so check it here first. if (!settings.guid.isValidGuid) { throw ArgumentError.value(settings.guid, "GUID", "Invalid GUID. Please use xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format\nYou can get one by searching GUID generators online"); } - if (instance != null) return false; instance = this; userCallback = onNotificationReceived; final appName = settings.appName.toNativeUtf8(allocator: arena); @@ -63,11 +64,21 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { final guid = settings.guid.toNativeUtf8(allocator: arena); final iconPath = settings.iconPath?.toNativeUtf8(allocator: arena) ?? nullptr; final callback = NativeCallable.listener(_globalLaunchCallback).nativeFunction; - final result = _bindings.init(_plugin, appName, aumId, guid, iconPath, callback); - return result.toBool(); + final result = _bindings.init(_plugin, appName, aumId, guid, iconPath, callback).toBool(); + _isReady = result; + return result; }); + @override + void dispose() { + if (!_isReady) return; + _bindings.disposePlugin(_plugin); + instance = null; + _isReady = false; + } + void _onNotificationReceived(NativeLaunchDetails details) { + if (!_isReady) return; if (_details != null) _bindings.freeLaunchDetails(_details!); _details = details; final data = details.data.toMap(); @@ -81,13 +92,20 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { } @override - Future cancel(int id) async => _bindings.cancelNotification(_plugin, id); + Future cancel(int id) async { + if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); + _bindings.cancelNotification(_plugin, id); + } @override - Future cancelAll() async => _bindings.cancelAll(_plugin); + Future cancelAll() async { + if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); + _bindings.cancelAll(_plugin); + } @override Future> getActiveNotifications() async => using((arena) { + if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); final length = arena(); final array = _bindings.getActiveNotifications(_plugin, length); final result = array.asActiveNotifications(length.value); @@ -97,6 +115,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { @override Future> pendingNotificationRequests() async => using((arena) { + if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); final length = arena(); final array = _bindings.getPendingNotifications(_plugin, length); final result = array.asPendingRequests(length.value); @@ -106,6 +125,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { @override Future getNotificationAppLaunchDetails() async { + if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); final details = _details; if (details == null) return null; final data = details.data.toMap(); @@ -132,6 +152,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { @override Future show(int id, String? title, String? body, {String? payload, WindowsNotificationDetails? details}) async => using((arena) { + if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); final bindings = { if (details != null) ...details.bindings, for (final progressBar in details?.progressBars ?? []) @@ -139,12 +160,15 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { }; final nativeMap = bindings.toNativeMap(arena); final xml = notificationToXml(title: title, body: body, payload: payload, details: details); - _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), nativeMap); + final result = _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), nativeMap).toBool(); + if (!result) throw Exception("Flutter Local Notifications (Windows) could not show notification"); }); @override Future showRawXml({required int id, required String xml, Map bindings = const {}}) async => using((arena) { - _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), bindings.toNativeMap(arena)); + if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); + final result = _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), bindings.toNativeMap(arena)).toBool(); + if (!result) throw ArgumentError("Flutter Local Notifications (Windows): Invalid XML"); }); @override @@ -156,6 +180,8 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { WindowsNotificationDetails? details, { String? payload, }) async => using((arena) { + if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); + if (scheduledDate.isBefore(DateTime.now())) throw ArgumentError("Flutter Local Notifications (Windows) cannot schedule notifications in the past"); final xml = notificationToXml(title: title, body: body, payload: payload, details: details); final secondsSinceEpoch = scheduledDate.millisecondsSinceEpoch ~/ 1000; _bindings.scheduleNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), secondsSinceEpoch); @@ -163,6 +189,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { @override Future updateBindings({required int id, required Map bindings}) async => using((arena) { + if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); final result = _bindings.updateNotification(_plugin, id, bindings.toNativeMap(arena)); return getUpdateResult(result); }); diff --git a/flutter_local_notifications_windows/lib/src/plugin/stub.dart b/flutter_local_notifications_windows/lib/src/plugin/stub.dart index 8dc06bf53..f13475b29 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/stub.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/stub.dart @@ -11,6 +11,9 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { throw UnsupportedError("This platform does not support Windows notifications"); } + @override + void dispose() { } + @override Future cancel(int id) async { } diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index 0bd72dc1c..f940ed192 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -20,6 +20,7 @@ dev_dependencies: very_good_analysis: ^6.0.0 flutter_test: sdk: flutter + test: ^1.25.2 flutter: plugin: diff --git a/flutter_local_notifications_windows/src/ffi_api.cpp b/flutter_local_notifications_windows/src/ffi_api.cpp index c750d7119..52fae6bca 100644 --- a/flutter_local_notifications_windows/src/ffi_api.cpp +++ b/flutter_local_notifications_windows/src/ffi_api.cpp @@ -6,6 +6,8 @@ #include "plugin.hpp" #include "utils.hpp" +#include + using winrt::Windows::Data::Xml::Dom::XmlDocument; NativePlugin* createPlugin() { @@ -39,8 +41,11 @@ int showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bi const auto ptr = reinterpret_cast(plugin); if (!ptr->isReady) return false; XmlDocument doc; - try { doc.LoadXml(winrt::to_hstring(xml)); } - catch (winrt::hresult_error error) { return false; } + try { + doc.LoadXml(winrt::to_hstring(xml)); + } catch (winrt::hresult_error error) { + return false; + } ToastNotification notification(doc); const auto data = dataFromMap(bindings); notification.Tag(winrt::to_hstring(id)); diff --git a/flutter_local_notifications_windows/src/plugin.cpp b/flutter_local_notifications_windows/src/plugin.cpp index 5e9abba42..e9508fc72 100644 --- a/flutter_local_notifications_windows/src/plugin.cpp +++ b/flutter_local_notifications_windows/src/plugin.cpp @@ -9,6 +9,8 @@ #include "plugin.hpp" #include "utils.hpp" +#include + struct RegistryHandle { using type = HKEY; @@ -187,13 +189,11 @@ void UpdateRegistry( /// and the guid of the callback. bool RegisterCallback(const std::string& guid, NativeNotificationCallback callback) { DWORD registration{}; - + winrt::guid rclsid(guid); + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); const auto factory_ref = winrt::make_self(); const auto factory = factory_ref.get(); - - winrt::guid rclsid(guid); factory->callback = callback; - winrt::check_hresult(CoRegisterClassObject( rclsid, factory, diff --git a/flutter_local_notifications_windows/test/bindings_test.dart b/flutter_local_notifications_windows/test/bindings_test.dart new file mode 100644 index 000000000..64f46a47e --- /dev/null +++ b/flutter_local_notifications_windows/test/bindings_test.dart @@ -0,0 +1,37 @@ +import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; +import "package:test/test.dart"; + +const settings = WindowsInitializationSettings(appName: "Test app", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); +const bindings = {"title": "Bindings title", "body": "Bindings body"}; + +void main() => group("Bindings", () { + final plugin = FlutterLocalNotificationsWindows(); + setUpAll(() => plugin.initialize(settings)); + tearDownAll(() async { await plugin.cancelAll(); plugin.dispose(); }); + + test("work in simple cases", () async { + await plugin.show(500, "{title}", "{body}"); + final result = await plugin.updateBindings(id: 500, bindings: bindings); + expect(result, NotificationUpdateResult.success); + }); + + test("fail when ID is not found in simple cases", () async { + await plugin.show(501, "{title}", "{body}"); + final result = await plugin.updateBindings(id: 599, bindings: bindings); + expect(result, NotificationUpdateResult.notFound); + }); + + test("are included in show()", () async { + await plugin.show(502, "{title}", "{body}", details: const WindowsNotificationDetails(bindings: bindings)); + }); + + test("fail when notification has been cancelled", retry: 5, () async { + await Future.delayed(const Duration(milliseconds: 200)); + await plugin.show(503, "{title}", "{body}"); + final result = await plugin.updateBindings(id: 503, bindings: bindings); + expect(result, NotificationUpdateResult.success); + await plugin.cancelAll(); + final result2 = await plugin.updateBindings(id: 503, bindings: bindings); + expect(result2, NotificationUpdateResult.notFound); + }); +}); diff --git a/flutter_local_notifications_windows/test/details_test.dart b/flutter_local_notifications_windows/test/details_test.dart new file mode 100644 index 000000000..00c67ef2b --- /dev/null +++ b/flutter_local_notifications_windows/test/details_test.dart @@ -0,0 +1,129 @@ +import "dart:io"; + +import "package:test/test.dart"; +import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; + +const settings = WindowsInitializationSettings(appName: "Test app", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); + +extension PluginUtils on FlutterLocalNotificationsWindows { + static int id = 15; + + Future showDetails(WindowsNotificationDetails details) => + show(id++, "Title", "Body", details: details); + + void testDetails(WindowsNotificationDetails details) => + expect(showDetails(details), completes); +} + +void main() => group("Details:", () { + final plugin = FlutterLocalNotificationsWindows(); + setUpAll(() => plugin.initialize(settings)); + tearDownAll(() async { await plugin.cancelAll(); plugin.dispose(); }); + + test("No details", () async { + expect(plugin.show(100, null, null), completes); + expect(plugin.show(101, "Title", null), completes); + expect(plugin.show(102, null, "Body"), completes); + expect(plugin.show(103, "Title", "Body"), completes); + expect(plugin.show(-1, "Negative ID", "Body"), completes); + }); + + test("Simple details", () async { + plugin.testDetails(const WindowsNotificationDetails()); + plugin.testDetails(const WindowsNotificationDetails(subtitle: "Subtitle")); + plugin.testDetails(const WindowsNotificationDetails(duration: WindowsNotificationDuration.long)); + plugin.testDetails(const WindowsNotificationDetails(scenario: WindowsNotificationScenario.reminder)); + plugin.testDetails(WindowsNotificationDetails(timestamp: DateTime.now())); + plugin.testDetails(const WindowsNotificationDetails(subtitle: "{message}", bindings: {"message": "Hello, Mr. Person"})); + }); + + test("Actions", () { + const simpleAction = WindowsAction(content: "Press me", arguments: "123"); + final complexAction = WindowsAction( + content: "content", + arguments: "args", + activationBehavior: WindowsNotificationBehavior.pendingUpdate, + activationType: WindowsActivationType.background, + buttonStyle: WindowsButtonStyle.success, + inputId: "input-id", + tooltip: "tooltip", + image: File("test/icon.png").absolute, + ); + plugin.testDetails(const WindowsNotificationDetails(actions: [simpleAction])); + plugin.testDetails(WindowsNotificationDetails(actions: [complexAction])); + plugin.testDetails(WindowsNotificationDetails(actions: List.filled(5, simpleAction))); + expect(plugin.showDetails(WindowsNotificationDetails(actions: List.filled(6, simpleAction))), throwsArgumentError); + }); + + test("Audio", () { + plugin.testDetails(WindowsNotificationDetails(audio: WindowsNotificationAudio.silent())); + plugin.testDetails(WindowsNotificationDetails(audio: WindowsNotificationAudio.preset(sound: WindowsNotificationSound.call10))); + }); + + test("Rows", () { + const emptyColumn = WindowsColumn([]); + final image = WindowsImage.file(File("test/icon.png").absolute, altText: "an icon"); + const text = WindowsNotificationText(text: "Text"); + final simpleColumn = WindowsColumn([image, text]); + final bigRow = WindowsRow(List.filled(5, simpleColumn)); + plugin.testDetails(const WindowsNotificationDetails()); + plugin.testDetails(const WindowsNotificationDetails(groups: [WindowsRow([])])); + plugin.testDetails(const WindowsNotificationDetails(groups: [WindowsRow([emptyColumn])])); + plugin.testDetails(WindowsNotificationDetails(groups: [WindowsRow([simpleColumn])])); + plugin.testDetails(WindowsNotificationDetails(groups: [bigRow])); + plugin.testDetails(WindowsNotificationDetails(groups: List.filled(5, bigRow))); + }); + + test("Header", () async { + const header = WindowsHeader(id: "header1", title: "Header 1", arguments: "args1", activation: WindowsHeaderActivation.foreground); + plugin.testDetails(const WindowsNotificationDetails(header: header)); + plugin.testDetails(const WindowsNotificationDetails(header: header)); + }); + + test("Images", () async { + final simpleImage = WindowsImage.file(File("test/icon.png").absolute, altText: "an icon"); + final complexImage = WindowsImage.file( + File("test/icon.png").absolute, + altText: "an icon", + addQueryParams: true, + crop: WindowsImageCrop.circle, + placement: WindowsImagePlacement.appLogoOverride, + ); + plugin.testDetails(WindowsNotificationDetails(images: [simpleImage])); + plugin.testDetails(WindowsNotificationDetails(images: [simpleImage, complexImage])); + plugin.testDetails(WindowsNotificationDetails(images: List.filled(6, simpleImage))); + }); + + test("Inputs", () async { + const textInput = WindowsTextInput(id: "input", hintText: "Text hint", title: "Text title"); + const selection = WindowsSelectionInput(id: "input", items: [ + WindowsSelection(id: "item1", content: "Item 1"), + WindowsSelection(id: "item2", content: "Item 2"), + WindowsSelection(id: "item3", content: "Item 3"), + ],); + const action = WindowsAction(content: "Submit", arguments: "submit", inputId: "input"); + plugin.testDetails(const WindowsNotificationDetails(inputs: [textInput])); + plugin.testDetails(const WindowsNotificationDetails(inputs: [selection])); + plugin.testDetails(WindowsNotificationDetails(inputs: List.filled(5, textInput))); + plugin.testDetails(const WindowsNotificationDetails(inputs: [textInput], actions: [action])); + expect(plugin.showDetails(WindowsNotificationDetails(inputs: List.filled(6, textInput))), throwsArgumentError); + plugin.testDetails(const WindowsNotificationDetails(inputs: [selection, textInput], actions: [action])); + }); + + test("Progress", () async { + final simple = WindowsProgressBar(id: "simple", status: "Testing...", value: 0.25); + final complex = WindowsProgressBar(id: "complex", status: "Testing...", value: 0.75, label: "Progress label", title: "Progress title"); + final dynamic = WindowsProgressBar(id: "dynamic", status: "Testing...", value: 0); + plugin.testDetails(WindowsNotificationDetails(progressBars: [simple])); + plugin.testDetails(WindowsNotificationDetails(progressBars: [complex])); + plugin.testDetails(WindowsNotificationDetails(progressBars: [simple, complex])); + plugin.testDetails(WindowsNotificationDetails(progressBars: List.filled(6, simple))); + await plugin.show(201, null, null, details: WindowsNotificationDetails(progressBars: [dynamic])); + for (var i = 0.0; i <= 1.5; i += 0.05) { + dynamic.value = i; + final result = await plugin.updateProgressBar(notificationId: 201, progressBar: dynamic); + expect(result, NotificationUpdateResult.success); + await Future.delayed(const Duration(milliseconds: 10)); + } + }); +}); diff --git a/flutter_local_notifications_windows/test/icon.png b/flutter_local_notifications_windows/test/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/flutter_local_notifications_windows/test/plugin_test.dart b/flutter_local_notifications_windows/test/plugin_test.dart new file mode 100644 index 000000000..399edbbe8 --- /dev/null +++ b/flutter_local_notifications_windows/test/plugin_test.dart @@ -0,0 +1,71 @@ +import "package:test/test.dart"; +import "package:timezone/standalone.dart"; +import "package:timezone/data/latest_all.dart"; + + +import "package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart"; +import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; + +const goodSettings = WindowsInitializationSettings(appName: "test", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); +const badSettings = WindowsInitializationSettings(appName: "test", appUserModelId: "com.test.test", guid: "123"); + +void main() => group("Plugin", () { + setUpAll(initializeTimeZones); + + test("initializes safely", () async { + final plugin = FlutterLocalNotificationsWindows(); + final result = await plugin.initialize(goodSettings); + expect(result, isTrue); + plugin.dispose(); + }); + + test("catches bad GUIDs", () async { + final plugin = FlutterLocalNotificationsWindows(); + expect(plugin.initialize(badSettings), throwsArgumentError); + plugin.dispose(); + }); + + test("cannot be used before initializing", () async { + final plugin = FlutterLocalNotificationsWindows(); + final progress = WindowsProgressBar(id: "progress", status: "Testing", value: 0); + final now = TZDateTime.local(2024, 7, 18); + expect(plugin.cancel(0), throwsStateError); + expect(plugin.cancelAll(), throwsStateError); + expect(plugin.getActiveNotifications(), throwsStateError); + expect(plugin.getNotificationAppLaunchDetails(), throwsStateError); + expect(plugin.pendingNotificationRequests(), throwsStateError); + expect(plugin.show(0, "Title", "Body"), throwsStateError); + expect(plugin.showRawXml(id: 0, xml: ""), throwsStateError); + expect(plugin.updateBindings(id: 0, bindings: {}), throwsStateError); + expect(plugin.updateProgressBar(progressBar: progress, notificationId: 0), throwsStateError); + expect(plugin.zonedSchedule(0, null, null, now, null), throwsStateError); + plugin.dispose(); + }); + + test("cannot be used after disposed", () async { + final plugin = FlutterLocalNotificationsWindows(); + final progress = WindowsProgressBar(id: "progress", status: "Testing", value: 0); + final now = TZDateTime.local(2024, 7, 18); + await plugin.initialize(goodSettings); + plugin.dispose(); + expect(plugin.cancel(0), throwsStateError); + expect(plugin.cancelAll(), throwsStateError); + expect(plugin.getActiveNotifications(), throwsStateError); + expect(plugin.getNotificationAppLaunchDetails(), throwsStateError); + expect(plugin.pendingNotificationRequests(), throwsStateError); + expect(plugin.show(0, "Title", "Body"), throwsStateError); + expect(plugin.showRawXml(id: 0, xml: ""), throwsStateError); + expect(plugin.updateBindings(id: 0, bindings: {}), throwsStateError); + expect(plugin.updateProgressBar(progressBar: progress, notificationId: 0), throwsStateError); + expect(plugin.zonedSchedule(0, null, null, now, null), throwsStateError); + plugin.dispose(); + }); + + test("does not support repeating notifications", () async { + final plugin = FlutterLocalNotificationsWindows(); + await plugin.initialize(goodSettings); + expect(plugin.periodicallyShow(0, null, null, RepeatInterval.everyMinute), throwsUnsupportedError); + expect(plugin.periodicallyShowWithDuration(0, null, null, Duration.zero), throwsUnsupportedError); + plugin.dispose(); + }); +}); diff --git a/flutter_local_notifications_windows/test/scheduled_test.dart b/flutter_local_notifications_windows/test/scheduled_test.dart new file mode 100644 index 000000000..ff45aa477 --- /dev/null +++ b/flutter_local_notifications_windows/test/scheduled_test.dart @@ -0,0 +1,38 @@ +import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; +import "package:test/test.dart"; +import "package:timezone/standalone.dart"; +import "package:timezone/data/latest_all.dart"; + +const settings = WindowsInitializationSettings(appName: "Test app", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); + +void main() => group("Schedules", () { + final plugin = FlutterLocalNotificationsWindows(); + setUpAll(initializeTimeZones); + setUpAll(() => plugin.initialize(settings)); + tearDownAll(() async { await plugin.cancelAll(); plugin.dispose(); }); + + Future countPending() async => (await plugin.pendingNotificationRequests()).length; + late final location = getLocation("US/Eastern"); + + test("work with basic times", () async { + await plugin.cancelAll(); + expect(await countPending(), 0); + final now = TZDateTime.now(location); + final later = now.add(const Duration(days: 1)); + expect(plugin.zonedSchedule(300, null, null, later, null), completes); + expect(await countPending(), 1); + expect(plugin.zonedSchedule(301, null, null, later, null), completes); + expect(await countPending(), 2); + expect(plugin.zonedSchedule(302, null, null, later, null), completes); + expect(await countPending(), 3); + }); + + test("do not work with earlier time", () async { + final now = TZDateTime.now(location); + final earlier = now.subtract(const Duration(days: 1)); + await plugin.cancelAll(); + expect(await countPending(), 0); + expect(plugin.zonedSchedule(302, null, null, now, null), throwsArgumentError); + expect(plugin.zonedSchedule(302, null, null, earlier, null), throwsArgumentError); + }); +}); diff --git a/flutter_local_notifications_windows/test/sound.mp3 b/flutter_local_notifications_windows/test/sound.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..60dbf9794d2d1270b43308ab84425261ba2f5b19 GIT binary patch literal 37616 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 literal 0 HcmV?d00001 diff --git a/flutter_local_notifications_windows/test/xml_test.dart b/flutter_local_notifications_windows/test/xml_test.dart new file mode 100644 index 000000000..ca921cb7a --- /dev/null +++ b/flutter_local_notifications_windows/test/xml_test.dart @@ -0,0 +1,69 @@ +import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; +import "package:test/test.dart"; + +const settings = WindowsInitializationSettings(appName: "test", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); +const emptyXml = ""; +const invalidXml = "Blah blah blah"; +const notWindowsXml = "Hi"; +const unmatchedXml = "Hi"; +const validXml = """ + + + + + Hello World + This is a simple toast message + + + + +"""; + +const complexXml = """ + + + + + Surface Launch Party + Studio S / Ballroom + 4:00 PM, 10/26/2015 + + + + + + + + + + + + + + + + + + +"""; + +void main() => group("XML", () { + final plugin = FlutterLocalNotificationsWindows(); + setUpAll(() => plugin.initialize(settings)); + tearDownAll(() async { await plugin.cancelAll(); plugin.dispose(); }); + + test("catches invalid XML", () async { + expect(plugin.showRawXml(id: 0, xml: emptyXml), throwsArgumentError); + expect(plugin.showRawXml(id: 1, xml: invalidXml), throwsArgumentError); + expect(plugin.showRawXml(id: 2, xml: notWindowsXml), throwsArgumentError); + expect(plugin.showRawXml(id: 3, xml: unmatchedXml), throwsArgumentError); + expect(plugin.showRawXml(id: 4, xml: validXml), completes); + expect(plugin.showRawXml(id: 5, xml: complexXml), completes); + }); +}); diff --git a/melos.yaml b/melos.yaml index 16853911a..e2eb24feb 100644 --- a/melos.yaml +++ b/melos.yaml @@ -3,6 +3,7 @@ repository: https://github.com/MaikuB/flutter_local_notifications packages: - flutter_local_notifications/* - flutter_local_notifications_linux/* + - flutter_local_notifications_windows/* - flutter_local_notifications_platform_interface/* - "**/example/*" @@ -71,6 +72,15 @@ scripts: dir-exists: - linux scope: "*example*" + build:example_windows: + run: | + melos exec -c 1 -- \ + "dart run msix:create" + description: Build a specific example app for Windows. + select-package: + dir-exists: + - linux + scope: "*example*" clean: run: git clean -x -d -f -q From 56f037e2b8b0b8154f91c4fce4a01b2848ced415 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 16 Jul 2024 19:45:02 -0400 Subject: [PATCH 059/112] Configured exampleand Melos for Windows --- .../example/lib/main.dart | 20 ++++--------------- .../example/pubspec.yaml | 14 ++++++++++--- .../src/ffi_api.cpp | 2 -- .../src/plugin.cpp | 3 --- melos.yaml | 2 +- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index c43a31e86..38c96afa6 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -172,24 +172,12 @@ Future main() async { await flutterLocalNotificationsPlugin.initialize( initializationSettings, - onDidReceiveNotificationResponse: - (NotificationResponse notificationResponse) { - switch (notificationResponse.notificationResponseType) { - case NotificationResponseType.selectedNotification: - selectNotificationStream.add(notificationResponse); - break; - case NotificationResponseType.selectedNotificationAction: - if (notificationResponse.actionId == navigationActionId) { - selectNotificationStream.add(notificationResponse); - } - break; - } - }, + onDidReceiveNotificationResponse: selectNotificationStream.add, onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); - final NotificationAppLaunchDetails? notificationAppLaunchDetails = !kIsWeb && - Platform.isLinux + final NotificationAppLaunchDetails? notificationAppLaunchDetails = + !kIsWeb && Platform.isLinux ? null : await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); String initialRoute = HomePage.routeName; @@ -2804,7 +2792,7 @@ Future _showLinuxNotificationWithByteDataIcon() async { 'icons/app_icon_density.png', ); final image.Image? iconData = image.decodePng( - assetIcon.buffer.asUint8List().toList(), + assetIcon.buffer.asUint8List(), ); final Uint8List iconBytes = iconData!.getBytes(); final LinuxNotificationDetails linuxPlatformChannelSpecifics = diff --git a/flutter_local_notifications/example/pubspec.yaml b/flutter_local_notifications/example/pubspec.yaml index 4c130af22..f88ae03f9 100644 --- a/flutter_local_notifications/example/pubspec.yaml +++ b/flutter_local_notifications/example/pubspec.yaml @@ -11,8 +11,9 @@ dependencies: path: ../ flutter_timezone: ^1.0.4 http: ^1.2.1 - image: ^3.0.8 + image: ^4.2.0 path_provider: ^2.0.0 + timezone: ^0.9.4 dev_dependencies: flutter_driver: @@ -21,7 +22,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - msix: ^2.8.17 + msix: ^3.16.7 # The following overrides exist to ensure the example app builds with the latest code # of these packages as part of CI @@ -46,4 +47,11 @@ environment: msix_config: display_name: Flutter Local Notifications Example identity_name: Com.Example.FlutterLocalNotificationsExample - debug: true + msix_version: 1.0.0.0 + store: false + install_certificate: false + output_name: example + toast_activator: + clsid: "d49b0314-ee7a-4626-bf79-97cdb8a991bb" + arguments: "msix-args" + display_name: "Flutter Local Notifications" diff --git a/flutter_local_notifications_windows/src/ffi_api.cpp b/flutter_local_notifications_windows/src/ffi_api.cpp index 52fae6bca..0544263b9 100644 --- a/flutter_local_notifications_windows/src/ffi_api.cpp +++ b/flutter_local_notifications_windows/src/ffi_api.cpp @@ -6,8 +6,6 @@ #include "plugin.hpp" #include "utils.hpp" -#include - using winrt::Windows::Data::Xml::Dom::XmlDocument; NativePlugin* createPlugin() { diff --git a/flutter_local_notifications_windows/src/plugin.cpp b/flutter_local_notifications_windows/src/plugin.cpp index e9508fc72..fb6a929f6 100644 --- a/flutter_local_notifications_windows/src/plugin.cpp +++ b/flutter_local_notifications_windows/src/plugin.cpp @@ -9,8 +9,6 @@ #include "plugin.hpp" #include "utils.hpp" -#include - struct RegistryHandle { using type = HKEY; @@ -190,7 +188,6 @@ void UpdateRegistry( bool RegisterCallback(const std::string& guid, NativeNotificationCallback callback) { DWORD registration{}; winrt::guid rclsid(guid); - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); const auto factory_ref = winrt::make_self(); const auto factory = factory_ref.get(); factory->callback = callback; diff --git a/melos.yaml b/melos.yaml index e2eb24feb..22a0d3cea 100644 --- a/melos.yaml +++ b/melos.yaml @@ -75,7 +75,7 @@ scripts: build:example_windows: run: | melos exec -c 1 -- \ - "dart run msix:create" + "flutter build windows && dart run msix:create" description: Build a specific example app for Windows. select-package: dir-exists: From 6470448cf2a7fdfc0603cb36d2274b4a7cb4d358 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 17 Jul 2024 12:24:14 -0400 Subject: [PATCH 060/112] Documented everything and removed Flutter dependency from _windows --- flutter_local_notifications/README.md | 40 ++++++-- flutter_local_notifications/pubspec.yaml | 1 - flutter_local_notifications_windows/README.md | 92 +++++------------- .../pubspec.yaml | 8 +- images/windows_notification.png | Bin 0 -> 67902 bytes melos.yaml | 2 +- 6 files changed, 57 insertions(+), 86 deletions(-) create mode 100644 images/windows_notification.png diff --git a/flutter_local_notifications/README.md b/flutter_local_notifications/README.md index 2619472e8..f91ff6b9e 100644 --- a/flutter_local_notifications/README.md +++ b/flutter_local_notifications/README.md @@ -5,7 +5,7 @@ A cross platform plugin for displaying local notifications. ->[!IMPORTANT] +>[!IMPORTANT] > Given how both quickly both Flutter ecosystem and Android ecosystem evolves, the minimum Flutter SDK version will be bumped to make it easier to maintain the plugin. Note that official plugins already follow a similar approach e.g. have a minimum Flutter SDK version of 3.13. This is being called out as if this affects your applications (e.g. supported OS versions) then you may need to consider maintaining your own fork in the future ## Table of contents @@ -59,8 +59,9 @@ A cross platform plugin for displaying local notifications. * **iOS**. Uses the [UserNotification APIs](https://developer.apple.com/documentation/usernotifications) (aka the User Notifications Framework) * **macOS**. 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. Notification actions only work on macOS 10.14 or newer * **Linux**. Uses the [Desktop Notifications Specification](https://specifications.freedesktop.org/notification-spec/) +* **Windows** Uses the [C++/WinRT](https://learn.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/) implementation of [Toast Notifications](*https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-notifications-overview) -Note: the plugin has a requires Flutter SDK 3.13 at a minimum. The list of support platforms for Flutter 3.1.3 itself can be found [here](https://github.com/flutter/website/blob/3d18ab48218101493af84953b71eac0cc6781fdd/src/reference/supported-platforms.md) +Note: the plugin has a requires Flutter SDK 3.13 at a minimum. The list of support platforms for Flutter 3.13 itself can be found [here](https://github.com/flutter/website/blob/3d18ab48218101493af84953b71eac0cc6781fdd/src/reference/supported-platforms.md) ## ✨ Features @@ -109,6 +110,10 @@ Note: the plugin has a requires Flutter SDK 3.13 at a minimum. The list of suppo * [Linux] Ability to set custom hints * [Linux] Ability to suppress sound * [Linux] Resident and transient notifications +* [Windows] Can show raw XML (see the [Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer)) +* [Windows] A full Dart API for all the options supported by toast notifications +* [Windows] Can configure images, buttons, dropdowns, text input, and launch behavior +* [Windows] Can dynamically update notifications after they've been shown ## ⚠ Caveats and limitations @@ -154,6 +159,11 @@ Scheduled/pending notifications is currently not supported due to the lack of a The `onDidReceiveNotificationResponse` callback runs on the main isolate of the running application and cannot be launched in the background if the application is not running. To respond to notification after the application is terminated, your application should be registered as DBus activatable (please see [DBusApplicationLaunching](https://wiki.gnome.org/HowDoI/DBusApplicationLaunching) for more information), and register action before activating the application. This is difficult to do in a plugin because plugins instantiate during application activation, so `getNotificationAppLaunchDetails` can't be implemented without changing the main user application. +### Windows limitations + +- Windows does not support repeating notifications, so [`periodicallyShow`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/periodicallyShow.html) and [`periodicallyShowWithDuration`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/periodicallyShowWithDuration.html) will throw `UnsupportedError`s. +- Windows only allows apps with package identity to retrieve previously shown notifications. This means that on an app that was not packaged as an [MSIX](https://learn.microsoft.com/en-us/windows/msix/overview) installer, [`cancel`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/cancel.html) does nothing and [`getActiveNotifications`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/getActiveNotifications.html) will return an empty list. To package your app as an MSIX, see [`package:msix`](https://pub.dev/packages/msix) and the `msix` section in [the example's `pubspec.yaml`](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/pubspec.yaml). + ### Notification payload Due to some limitations on iOS with how it treats null values in dictionaries, a null notification payload is coalesced to an empty string behind the scenes on all platforms for consistency. @@ -166,6 +176,7 @@ Due to some limitations on iOS with how it treats null values in dictionaries, a | iOS | | | macOS | | | Linux | | +| Windows | | ## 👠Acknowledgements @@ -174,6 +185,7 @@ Due to some limitations on iOS with how it treats null values in dictionaries, a * [Jeff Scaturro](https://github.com/JeffScaturro) for submitting the PR to fix the iOS issue around showing daily and weekly notifications and migrating the plugin to AndroidX * [Ian Cavanaugh](https://github.com/icavanaugh95) for helping create a sample to reproduce the problem reported in [issue #88](https://github.com/MaikuB/flutter_local_notifications/issues/88) * [Zhang Jing](https://github.com/byrdkm17) for adding 'ticker' support for Android notifications +* [Kenneth](https://github.com/kennethnym), [lightrabbit](https://github.com/lightrabbit), and [Levi Lesches](https://github.com/Levi-Lesches) for adding Windows support * ...and everyone else for their contributions. They are greatly appreciated ## 🔧 Android Setup @@ -427,7 +439,7 @@ then extend `didFinishLaunchingWithOptions` and register the callback: - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [GeneratedPluginRegistrant registerWithRegistry:self]; - // Add this method + // Add this method [FlutterLocalNotificationsPlugin setPluginRegistrantCallback:registerPlugins]; } ``` @@ -571,11 +583,18 @@ final DarwinInitializationSettings initializationSettingsDarwin = final LinuxInitializationSettings initializationSettingsLinux = LinuxInitializationSettings( defaultActionName: 'Open notification'); +final WindowsInitializationSettings initializationSettingsWindows = + WindowsInitializationSettings( + appName: 'Flutter Local Notifications Example', + appUserModelId: 'Com.Dexterous.FlutterLocalNotificationsExample', + // Search online for GUID generators to make your own + guid: 'd49b0314-ee7a-4626-bf79-97cdb8a991bb') final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsDarwin, macOS: initializationSettingsDarwin, - linux: initializationSettingsLinux); + linux: initializationSettingsLinux, + windows: initializationSettingsWindows); await flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: onDidReceiveNotificationResponse); ``` @@ -599,9 +618,9 @@ void onDidReceiveNotificationResponse(NotificationResponse notificationResponse) In the real world, this payload could represent the id of the item you want to display the details of. Once the initialisation is complete, then you can manage the displaying of notifications. Note that this callback is only intended to work when the app is running. For scenarios where your application needs to handle when a notification launched the app refer to [here](#getting-details-on-if-the-app-was-launched-via-a-notification-created-by-this-plugin) -The `DarwinInitializationSettings` class provides default settings on how the notification be presented when it is triggered and the application is in the foreground on iOS/macOS. There are optional named parameters that can be modified to suit your application's purposes. Here, it is omitted and the default values for these named properties is set such that all presentation options (alert, sound, badge) are enabled. +The `DarwinInitializationSettings` class provides default settings on how the notification be presented when it is triggered and the application is in the foreground on iOS/macOS. There are optional named parameters that can be modified to suit your application's purposes. Here, it is omitted and the default values for these named properties is set such that all presentation options (alert, sound, badge) are enabled. -The `LinuxInitializationSettings` class requires a name for the default action that calls the `onDidReceiveNotificationResponse` callback when the notification is clicked. +The `LinuxInitializationSettings` class requires a name for the default action that calls the `onDidReceiveNotificationResponse` callback when the notification is clicked. On iOS and macOS, initialisation may show a prompt to requires users to give the application permission to display notifications (note: permissions don't need to be requested on Android). Depending on when this happens, this may not be the ideal user experience for your application. If so, please refer to the next section on how to work around this. @@ -692,7 +711,7 @@ The details specific to the Android platform are also specified. This includes t ### Scheduling a notification -Starting in version 2.0 of the plugin, scheduling notifications now requires developers to specify a date and time relative to a specific time zone. This is to solve issues with daylight saving time that existed in the `schedule` method that is now deprecated. A new `zonedSchedule` method is provided that expects an instance `TZDateTime` class provided by the [`timezone`](https://pub.dev/packages/timezone) package. Even though the `timezone` package is be a transitive dependency via this plugin, it is recommended based on [this lint rule](https://dart-lang.github.io/linter/lints/depend_on_referenced_packages.html) that you also add the `timezone` package as a direct dependency. +Starting in version 2.0 of the plugin, scheduling notifications now requires developers to specify a date and time relative to a specific time zone. This is to solve issues with daylight saving time that existed in the `schedule` method that is now deprecated. A new `zonedSchedule` method is provided that expects an instance `TZDateTime` class provided by the [`timezone`](https://pub.dev/packages/timezone) package. Even though the `timezone` package is be a transitive dependency via this plugin, it is recommended based on [this lint rule](https://dart-lang.github.io/linter/lints/depend_on_referenced_packages.html) that you also add the `timezone` package as a direct dependency. Once the depdendency as been added, usage of the `timezone` package requires initialisation that is covered in the package's readme. For convenience the following are code snippets used by the example app. @@ -744,6 +763,8 @@ If you are trying to update your code so it doesn't use the deprecated methods f ### Periodically show a notification with a specified interval +**Note** This is not supported on Windows + ```dart const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( @@ -765,8 +786,7 @@ final List pendingNotificationRequests = ### Retrieving active notifications - - +**Note** On Windows, your app must be packaged as an MSIX to do this. See the limitations section. ```dart final List activeNotifications = @@ -850,6 +870,8 @@ await flutterLocalNotificationsPlugin.show( ### Cancelling/deleting a notification +**Note** On Windows, your app must be packaged as an MSIX to do this. See the limitations section. + ```dart // cancel the notification with id value of zero await flutterLocalNotificationsPlugin.cancel(0); diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index 5f801f069..52b5d21a0 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -39,7 +39,6 @@ flutter: windows: default_package: flutter_local_notifications_windows - environment: sdk: ^3.1.0 flutter: ">=3.1.3" diff --git a/flutter_local_notifications_windows/README.md b/flutter_local_notifications_windows/README.md index 02bf2fda4..09b1d0c21 100644 --- a/flutter_local_notifications_windows/README.md +++ b/flutter_local_notifications_windows/README.md @@ -1,92 +1,48 @@ # flutter_local_notifications_windows -A new Flutter FFI plugin project. +The Windows implementation of `package:flutter_local_notifications` as an FFI package that can be run in plain Dart or as a Flutter plugin. See [the docs on FFI](https://dart.dev/interop/c-interop). -## Getting Started +## Limitations -This project is a starting point for a Flutter -[FFI plugin](https://docs.flutter.dev/development/platform-integration/c-interop), -a specialized package that includes native code directly invoked with Dart FFI. +- Windows does not support repeating notifications, so [`periodicallyShow`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/periodicallyShow.html) and [`periodicallyShowWithDuration`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/periodicallyShowWithDuration.html) will throw `UnsupportedError`s. +- Windows only allows apps with package identity to retrieve previously shown notifications. This means that on an app that was not packaged as an [MSIX](https://learn.microsoft.com/en-us/windows/msix/overview) installer, [`cancel`](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/cancel.html) does nothing and [getActiveNotifications](https://pub.dev/documentation/flutter_local_notifications/latest/flutter_local_notifications/FlutterLocalNotificationsPlugin/getActiveNotifications.html) will return an empty list. To package your app as an MSIX, see [`package:msix`](https://pub.dev/packages/msix) and the `msix` section in [the example's `pubspec.yaml`](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/pubspec.yaml). ## Project structure This template uses the following structure: -* `src`: Contains the native source code, and a CmakeFile.txt file for building - that source code into a dynamic library. +- `src`: Contains the native source code, and a CmakeFile.txt file for building + that source code into a dynamic library. Within this folder, there are three C++ files: + - `ffi_api.h`/`ffi_api.cpp`: A C-compatible header file with the API that will be used by Dart, and the C++ implementation of that API + - `plugin.hpp`/`plugin.cpp`: A C++ class holding handles to the [C++/WinRT](https://learn.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/) SDK, along with some Windows-heavy logic. `ffi_api.cpp` implements its features using this class. + - `utils.hpp`/`utils.cpp` handle copying and allocating data from C structs to WinRT classes and vice-versa. Since FFI is done over C-based APIs, C++ types like strings, maps, and vectors need to be translated. -* `lib`: Contains the Dart code that defines the API of the plugin, and which +- `lib`: Contains the Dart code that defines the API of the plugin, and which calls into the native code using `dart:ffi`. + - The `details` folder holds all the Windows-specific notification configurations such as `WindowsAction`, `WindowsImage`, etc. + - The `ffi` folder holds the generated bindings (see below) and other FFI utilities. + - The `plugin` folder implements `package:flutter_local_notifications_platform_interface` in two ways: a stub for platforms that don't support FFI, and an FFI-based implementation. -* platform folders (`android`, `ios`, `windows`, etc.): Contains the build files - for building and bundling the native code library with the platform application. +- The `windows` folder contains the build files for building and bundling the native code library with the platform application. ## Building and bundling native code -The `pubspec.yaml` specifies FFI plugins as follows: +The code in `src` can be built with CMake. A `build.bat` file is included, which has the following code: -```yaml - plugin: - platforms: - some_platform: - ffiPlugin: true +```batch +@echo off +cd build +cmake ../windows +cmake --build . +cd .. +copy build\shared\Debug\flutter_local_notifications_windows.dll . ``` -This configuration invokes the native build for the various target platforms -and bundles the binaries in Flutter applications using these FFI plugins. - -This can be combined with dartPluginClass, such as when FFI is used for the -implementation of one platform in a federated plugin: - -```yaml - plugin: - implements: some_other_plugin - platforms: - some_platform: - dartPluginClass: SomeClass - ffiPlugin: true -``` - -A plugin can have both FFI and method channels: - -```yaml - plugin: - platforms: - some_platform: - pluginClass: SomeName - ffiPlugin: true -``` - -The native build systems that are invoked by FFI (and method channel) plugins are: - -* For Android: Gradle, which invokes the Android NDK for native builds. - * See the documentation in android/build.gradle. -* For iOS and MacOS: Xcode, via CocoaPods. - * See the documentation in ios/flutter_local_notifications_windows.podspec. - * See the documentation in macos/flutter_local_notifications_windows.podspec. -* For Linux and Windows: CMake. - * See the documentation in linux/CMakeLists.txt. - * See the documentation in windows/CMakeLists.txt. +This generates a DLL from the native code and copies it to the current directory. This is useful for testing locally without Flutter. When using Flutter, this step is unnecessary as Flutter will build and bundle the assets for you. ## Binding to native code To use the native code, bindings in Dart are needed. To avoid writing these by hand, they are generated from the header file -(`src/flutter_local_notifications_windows.h`) by `package:ffigen`. +`src/ffi_api.h` by `package:ffigen`. Regenerate the bindings by running `dart run ffigen --config ffigen.yaml`. - -## Invoking native code - -Very short-running native functions can be directly invoked from any isolate. -For example, see `sum` in `lib/flutter_local_notifications_windows.dart`. - -Longer-running functions should be invoked on a helper isolate to avoid -dropping frames in Flutter applications. -For example, see `sumAsync` in `lib/flutter_local_notifications_windows.dart`. - -## Flutter help - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. - diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index f940ed192..024d42985 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -4,22 +4,16 @@ version: 1.0.0 environment: sdk: ">=3.1.0 <4.0.0" - flutter: ">=3.0.0" dependencies: - flutter: - sdk: flutter ffi: ^2.1.2 flutter_local_notifications_platform_interface: ^7.2.0 - plugin_platform_interface: ^2.0.2 timezone: ^0.9.4 xml: ^6.5.0 dev_dependencies: - ffigen: ^12.0.0 # using such a low version to avoid Dart 3.0 + ffigen: ^12.0.0 very_good_analysis: ^6.0.0 - flutter_test: - sdk: flutter test: ^1.25.2 flutter: diff --git a/images/windows_notification.png b/images/windows_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..44067cd5de540ef32c9e977e77c36ec80d08675e GIT binary patch literal 67902 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 Date: Wed, 17 Jul 2024 12:28:23 -0400 Subject: [PATCH 061/112] Added retry to flaky test --- flutter_local_notifications_windows/test/details_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_local_notifications_windows/test/details_test.dart b/flutter_local_notifications_windows/test/details_test.dart index 00c67ef2b..e95e51964 100644 --- a/flutter_local_notifications_windows/test/details_test.dart +++ b/flutter_local_notifications_windows/test/details_test.dart @@ -110,7 +110,7 @@ void main() => group("Details:", () { plugin.testDetails(const WindowsNotificationDetails(inputs: [selection, textInput], actions: [action])); }); - test("Progress", () async { + test("Progress", retry: 5, () async { final simple = WindowsProgressBar(id: "simple", status: "Testing...", value: 0.25); final complex = WindowsProgressBar(id: "complex", status: "Testing...", value: 0.75, label: "Progress label", title: "Progress title"); final dynamic = WindowsProgressBar(id: "dynamic", status: "Testing...", value: 0); From 5b37ea77ecb796bee3f36d4164244a7f3a4f63d7 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 17 Jul 2024 13:24:31 -0400 Subject: [PATCH 062/112] Documented the crash test --- .../bin/{test.dart => crash.dart} | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) rename flutter_local_notifications_windows/bin/{test.dart => crash.dart} (59%) diff --git a/flutter_local_notifications_windows/bin/test.dart b/flutter_local_notifications_windows/bin/crash.dart similarity index 59% rename from flutter_local_notifications_windows/bin/test.dart rename to flutter_local_notifications_windows/bin/crash.dart index 5a5a33372..cc64d3ad5 100644 --- a/flutter_local_notifications_windows/bin/test.dart +++ b/flutter_local_notifications_windows/bin/crash.dart @@ -1,3 +1,16 @@ +// This file demonstrates how the WinRT APIs are _not_ thread safe. +// +// If you debug this code into the C++, you'll see that the crash happens when +// declaring a local variable. A quick google shows that dynamic libraries are +// only loaded *once* into the Dart VM. This leads me to believe that it is an +// issue with sharing address spaces, and the two local variables exist at the +// same time, causing the crash. +// +// This crash can happen when running `dart test -j 1`, which would otherwise +// fix other concurrency issues with the tests. This crash is not significant +// for users as it depends on having two plugins instantiated at the same time, +// which is not recommended, but I left it here as a demonstration if needed. + import "dart:isolate"; import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; @@ -6,19 +19,17 @@ import "package:timezone/standalone.dart"; const settings = WindowsInitializationSettings(appName: "Test app", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); void main() async { - await Isolate.spawn(main2, null); - await Isolate.spawn(main3, null); + await Isolate.spawn(bindingsTest, null); + await Isolate.spawn(scheduledTest, null); await Future.delayed(const Duration(seconds: 5)); } -Future main3(_) async { +Future scheduledTest(_) async { await Future.delayed(const Duration(seconds: 4)); - // Scheduled: final plugin = FlutterLocalNotificationsWindows(); await plugin.initialize(settings); await initializeTimeZone(); - final location = getLocation("US/Eastern"); final now = TZDateTime.now(location); final later = now.add(const Duration(days: 1)); @@ -27,10 +38,9 @@ Future main3(_) async { await plugin.zonedSchedule(302, null, null, later, null); } -Future main2(_) async { +Future bindingsTest(_) async { final bindings = {"title": "Bindings title", "body": "Bindings body"}; await Future.delayed(const Duration(seconds: 1)); - // Bindings: final plugin = FlutterLocalNotificationsWindows(); await plugin.initialize(settings); await plugin.show(503, "{title}", "{body}"); From af82c0cd4214f5deefc6cfcd14bbccd28c04cda5 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 17 Jul 2024 13:29:17 -0400 Subject: [PATCH 063/112] Removed flutter from _platform_interface and bumped --- flutter_local_notifications_platform_interface/pubspec.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/flutter_local_notifications_platform_interface/pubspec.yaml b/flutter_local_notifications_platform_interface/pubspec.yaml index 5807c29bc..42707c52b 100644 --- a/flutter_local_notifications_platform_interface/pubspec.yaml +++ b/flutter_local_notifications_platform_interface/pubspec.yaml @@ -1,15 +1,12 @@ name: flutter_local_notifications_platform_interface description: A common platform interface for the flutter_local_notifications plugin. -version: 7.2.0 +version: 7.3.0 homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_platform_interface environment: sdk: ^3.1.0 - flutter: ">=3.1.3" dependencies: - flutter: - sdk: flutter plugin_platform_interface: ^2.0.0 dev_dependencies: From 68c03adce820d39b98ffd9791a4279459e744190 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 17 Jul 2024 13:32:14 -0400 Subject: [PATCH 064/112] Removed XML from main plugin --- flutter_local_notifications/pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index 52b5d21a0..f43c65f41 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -14,7 +14,6 @@ dependencies: flutter_local_notifications_windows: ^1.0.0 flutter_local_notifications_platform_interface: ^7.2.0 timezone: ^0.9.0 - xml: ^6.5.0 dev_dependencies: flutter_driver: From ed7feadbc60865e86b1d4276dba1dc4858dfcabb Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 8 Aug 2024 15:54:23 -0400 Subject: [PATCH 065/112] Simplify C++ code and use ffigen 13.0 for Dart enums --- .../lib/src/ffi/bindings.dart | 44 ++++++++---- .../lib/src/ffi/utils.dart | 12 ++-- .../pubspec.yaml | 2 +- .../src/ffi_api.cpp | 69 ++++++++----------- .../src/plugin.cpp | 4 +- .../src/plugin.hpp | 6 +- 6 files changed, 73 insertions(+), 64 deletions(-) diff --git a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart index 1d0c68303..d5b9634ee 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart @@ -142,21 +142,21 @@ class NotificationsPluginBindings { /// String values in the `` element of the XML can be placeholders instead of values, /// for example, `{name}` and then call this function with a map with a `name` key, /// and any string value, and the notification will be updated with that value where `name` was. - int updateNotification( + NativeUpdateResult updateNotification( ffi.Pointer plugin, int id, NativeStringMap bindings, ) { - return _updateNotification( + return NativeUpdateResult.fromValue(_updateNotification( plugin, id, bindings, - ); + )); } late final _updateNotificationPtr = _lookup< ffi.NativeFunction< - ffi.Int32 Function(ffi.Pointer, ffi.Int, + ffi.UnsignedInt Function(ffi.Pointer, ffi.Int, NativeStringMap)>>('updateNotification'); late final _updateNotification = _updateNotificationPtr.asFunction< int Function(ffi.Pointer, int, NativeStringMap)>(); @@ -295,9 +295,18 @@ final class NativeNotificationDetails extends ffi.Struct { } /// How the app was launched, either by pressing on the notification or an action within it. -abstract class NativeLaunchType { - static const int notification = 0; - static const int action = 1; +enum NativeLaunchType { + notification(0), + action(1); + + final int value; + const NativeLaunchType(this.value); + + static NativeLaunchType fromValue(int value) => switch (value) { + 0 => notification, + 1 => action, + _ => throw ArgumentError("Unknown value for NativeLaunchType: $value"), + }; } /// Details about how the app was launched. @@ -307,7 +316,7 @@ final class NativeLaunchDetails extends ffi.Struct { external int didLaunch; /// What part of the notification launched the app. - @ffi.Int32() + @ffi.UnsignedInt() external int launchType; /// The payload sent to the app by the notification. Usually the action that was pressed. @@ -318,10 +327,21 @@ final class NativeLaunchDetails extends ffi.Struct { } /// See: https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.notificationupdateresult -abstract class NativeUpdateResult { - static const int success = 0; - static const int failed = 1; - static const int notFound = 2; +enum NativeUpdateResult { + success(0), + failed(1), + notFound(2); + + final int value; + const NativeUpdateResult(this.value); + + static NativeUpdateResult fromValue(int value) => switch (value) { + 0 => success, + 1 => failed, + 2 => notFound, + _ => + throw ArgumentError("Unknown value for NativeUpdateResult: $value"), + }; } /// A callback that is run with [NativeLaunchDetails] when a notification is pressed. diff --git a/flutter_local_notifications_windows/lib/src/ffi/utils.dart b/flutter_local_notifications_windows/lib/src/ffi/utils.dart index 31b0d791a..6a77a386e 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/utils.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/utils.dart @@ -24,20 +24,18 @@ extension IntUtils on int { /// Gets the [NotificationResponseType] from a [NativeLaunchType]. NotificationResponseType getResponseType(int launchType) { - switch (launchType) { + switch (NativeLaunchType.fromValue(launchType)) { case NativeLaunchType.notification: return NotificationResponseType.selectedNotification; case NativeLaunchType.action: return NotificationResponseType.selectedNotificationAction; - default: throw ArgumentError("Invalid launch type: $launchType"); } } /// Gets the [NotificationUpdateResult] from a [NativeUpdateResult]. -NotificationUpdateResult getUpdateResult(int result) { +NotificationUpdateResult getUpdateResult(NativeUpdateResult result) { switch (result) { - case 0: return NotificationUpdateResult.success; - case 1: return NotificationUpdateResult.error; - case 2: return NotificationUpdateResult.notFound; - default: throw ArgumentError("Invalid update result: $result"); + case NativeUpdateResult.success: return NotificationUpdateResult.success; + case NativeUpdateResult.failed: return NotificationUpdateResult.error; + case NativeUpdateResult.notFound: return NotificationUpdateResult.notFound; } } diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index 024d42985..19515e2da 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: xml: ^6.5.0 dev_dependencies: - ffigen: ^12.0.0 + ffigen: ^13.0.0 very_good_analysis: ^6.0.0 test: ^1.25.2 diff --git a/flutter_local_notifications_windows/src/ffi_api.cpp b/flutter_local_notifications_windows/src/ffi_api.cpp index 0544263b9..c1491f258 100644 --- a/flutter_local_notifications_windows/src/ffi_api.cpp +++ b/flutter_local_notifications_windows/src/ffi_api.cpp @@ -9,35 +9,32 @@ using winrt::Windows::Data::Xml::Dom::XmlDocument; NativePlugin* createPlugin() { - return reinterpret_cast(new WinRTPlugin()); + return new NativePlugin(); } void disposePlugin(NativePlugin* plugin) { - delete reinterpret_cast(plugin); + delete plugin; } int init(NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, NativeNotificationCallback callback) { - const auto ptr = reinterpret_cast(plugin); - // TODO: Register the callback here string icon; if (iconPath != nullptr) icon = string(iconPath); - const auto didRegister = ptr->registerApp(aumId, appName, guid, icon, callback); + const auto didRegister = plugin->registerApp(aumId, appName, guid, icon, callback); if (!didRegister) return false; - const auto identity = ptr->checkIdentity(); + const auto identity = plugin->checkIdentity(); if (!identity.has_value()) return false; - ptr->hasIdentity = identity.value(); - ptr->aumid = winrt::to_hstring(aumId); - ptr->notifier = ptr->hasIdentity + plugin->hasIdentity = identity.value(); + plugin->aumid = winrt::to_hstring(aumId); + plugin->notifier = plugin->hasIdentity ? ToastNotificationManager::CreateToastNotifier() - : ToastNotificationManager::CreateToastNotifier(ptr->aumid); - ptr->history = ToastNotificationManager::History(); - ptr->isReady = true; + : ToastNotificationManager::CreateToastNotifier(plugin->aumid); + plugin->history = ToastNotificationManager::History(); + plugin->isReady = true; return true; } int showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bindings) { - const auto ptr = reinterpret_cast(plugin); - if (!ptr->isReady) return false; + if (!plugin->isReady) return false; XmlDocument doc; try { doc.LoadXml(winrt::to_hstring(xml)); @@ -48,52 +45,48 @@ int showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bi const auto data = dataFromMap(bindings); notification.Tag(winrt::to_hstring(id)); notification.Data(data); - ptr->notifier.value().Show(notification); + plugin->notifier.value().Show(notification); return true; } int scheduleNotification(NativePlugin* plugin, int id, char* xml, int time) { - const auto ptr = reinterpret_cast(plugin); - if (!ptr->isReady) return false; + if (!plugin->isReady) return false; XmlDocument doc; try { doc.LoadXml(winrt::to_hstring(xml)); } catch (winrt::hresult_error error) { return false; } ScheduledToastNotification notification(doc, winrt::clock::from_time_t(time)); notification.Tag(winrt::to_hstring(id)); - ptr->notifier.value().AddToSchedule(notification); + plugin->notifier.value().AddToSchedule(notification); return true; } NativeUpdateResult updateNotification(NativePlugin* plugin, int id, NativeStringMap bindings) { - const auto ptr = reinterpret_cast(plugin); - if (!ptr->isReady) return NativeUpdateResult::failed; + if (!plugin->isReady) return NativeUpdateResult::failed; const auto tag = winrt::to_hstring(id); const auto data = dataFromMap(bindings); - const auto result = ptr->notifier.value().Update(data, tag); + const auto result = plugin->notifier.value().Update(data, tag); return (NativeUpdateResult) result; } void cancelAll(NativePlugin* plugin) { - const auto ptr = reinterpret_cast(plugin); - if (!ptr->isReady) return; - if (ptr->hasIdentity) { - ptr->history.value().Clear(); + if (!plugin->isReady) return; + if (plugin->hasIdentity) { + plugin->history.value().Clear(); } else { - ptr->history.value().Clear(ptr->aumid); + plugin->history.value().Clear(plugin->aumid); } - for (const auto notification : ptr->notifier.value().GetScheduledToastNotifications()) { - ptr->notifier.value().RemoveFromSchedule(notification); + for (const auto notification : plugin->notifier.value().GetScheduledToastNotifications()) { + plugin->notifier.value().RemoveFromSchedule(notification); } } void cancelNotification(NativePlugin* plugin, int id) { - const auto ptr = reinterpret_cast(plugin); - if (!ptr->isReady) return; + if (!plugin->isReady) return; const auto tag = winrt::to_hstring(id); - if (ptr->hasIdentity) ptr->history.value().Remove(tag); - for (const auto notification : ptr->notifier.value().GetScheduledToastNotifications()) { + if (plugin->hasIdentity) plugin->history.value().Remove(tag); + for (const auto notification : plugin->notifier.value().GetScheduledToastNotifications()) { if (notification.Tag() == tag) { - ptr->notifier.value().RemoveFromSchedule(notification); + plugin->notifier.value().RemoveFromSchedule(notification); return; } } @@ -101,9 +94,8 @@ void cancelNotification(NativePlugin* plugin, int id) { NativeNotificationDetails* getActiveNotifications(NativePlugin* plugin, int* size) { // TODO: Get more details here - const auto ptr = reinterpret_cast(plugin); - if (!ptr->isReady || !ptr->hasIdentity) { *size = 0; return nullptr; } - const auto active = ptr->history.value().GetHistory(); + if (!plugin->isReady || !plugin->hasIdentity) { *size = 0; return nullptr; } + const auto active = plugin->history.value().GetHistory(); *size = active.Size(); const auto result = new NativeNotificationDetails[*size]; int index = 0; @@ -118,9 +110,8 @@ NativeNotificationDetails* getActiveNotifications(NativePlugin* plugin, int* siz NativeNotificationDetails* getPendingNotifications(NativePlugin* plugin, int* size) { // TODO: Get more details here - const auto ptr = reinterpret_cast(plugin); - if (!ptr->isReady) { *size = 0; return nullptr; } - const auto pending = ptr->notifier.value().GetScheduledToastNotifications(); + if (!plugin->isReady) { *size = 0; return nullptr; } + const auto pending = plugin->notifier.value().GetScheduledToastNotifications(); *size = pending.Size(); const auto result = new NativeNotificationDetails[*size]; int index = 0; diff --git a/flutter_local_notifications_windows/src/plugin.cpp b/flutter_local_notifications_windows/src/plugin.cpp index fb6a929f6..a4a28590f 100644 --- a/flutter_local_notifications_windows/src/plugin.cpp +++ b/flutter_local_notifications_windows/src/plugin.cpp @@ -201,7 +201,7 @@ bool RegisterCallback(const std::string& guid, NativeNotificationCallback callba return true; } -bool WinRTPlugin::registerApp( +bool NativePlugin::registerApp( const string& aumid, const string& appName, const string& guid, @@ -212,7 +212,7 @@ bool WinRTPlugin::registerApp( return RegisterCallback(guid, callback); } -std::optional WinRTPlugin::checkIdentity() { +std::optional NativePlugin::checkIdentity() { if (!IsWindows8OrGreater()) return false; uint32_t length = 0; auto error = GetCurrentPackageFullName(&length, nullptr); diff --git a/flutter_local_notifications_windows/src/plugin.hpp b/flutter_local_notifications_windows/src/plugin.hpp index f67bcc0ed..5e2c0b692 100644 --- a/flutter_local_notifications_windows/src/plugin.hpp +++ b/flutter_local_notifications_windows/src/plugin.hpp @@ -12,7 +12,7 @@ using std::optional; using std::string; using namespace winrt::Windows::UI::Notifications; -class WinRTPlugin { +class NativePlugin { public: /// Whether the plugin has been properly initialized. bool isReady = false; @@ -37,8 +37,8 @@ class WinRTPlugin { /// A callback to run when a notification is pressed, when the app is or is not running. NativeNotificationCallback callback; - WinRTPlugin() { } - ~WinRTPlugin() { } + NativePlugin() { } + ~NativePlugin() { } /// Checks whether the current application has package identity. See [hasIdentity] for details. /// From 94b0f58770d54c3d8bc94e00c49e67f4461b07cb Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 8 Aug 2024 16:03:34 -0400 Subject: [PATCH 066/112] Made attributes and notifToXml() private --- .../lib/src/details.dart | 1 - .../lib/src/details/notification_details.dart | 21 ----------------- .../lib/src/details/notification_to_xml.dart | 23 +++++++++++++++++++ .../lib/src/plugin/ffi.dart | 1 + 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/flutter_local_notifications_windows/lib/src/details.dart b/flutter_local_notifications_windows/lib/src/details.dart index 68bd51ab2..6a33441bb 100644 --- a/flutter_local_notifications_windows/lib/src/details.dart +++ b/flutter_local_notifications_windows/lib/src/details.dart @@ -9,7 +9,6 @@ export "details/notification_input.dart"; export "details/notification_part.dart"; export "details/notification_progress.dart"; export "details/notification_text.dart"; -export "details/notification_to_xml.dart"; /// The result of updating a notification. enum NotificationUpdateResult { diff --git a/flutter_local_notifications_windows/lib/src/details/notification_details.dart b/flutter_local_notifications_windows/lib/src/details/notification_details.dart index 942877042..518759411 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_details.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_details.dart @@ -41,20 +41,6 @@ enum WindowsNotificationScenario { urgent, } -extension on DateTime { - String toIso8601StringTz() { - // Get offset - final offset = timeZoneOffset; - final sign = offset.isNegative ? "-" : "+"; - final hours = offset.inHours.abs().toString().padLeft(2, "0"); - final minutes = offset.inMinutes.abs().remainder(60).toString().padLeft(2, "0"); - final offsetString = "$sign$hours:$minutes"; - // Get first part of properly formatted ISO 8601 date - final formattedDate = toIso8601String().split(".").first; - return "$formattedDate$offsetString"; - } -} - /// Contains notification details specific to Windows. /// /// See: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts @@ -115,13 +101,6 @@ class WindowsNotificationDetails { /// using the binding name as the key here, and the value as any string you want final Map bindings; - /// XML attributes for the toast notification as a whole. - Map get attributes => { - if (duration != null) "duration": duration!.name, - if (timestamp != null) "displayTimestamp": timestamp!.toIso8601StringTz(), - if (scenario != null) "scenario": scenario!.name, - }; - /// Builds all relevant XML parts under the root `` element. void toXml(XmlBuilder builder) { if (actions.length > 5) { diff --git a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart index 5adfb84a1..e6887e0e7 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart @@ -2,6 +2,29 @@ import "package:xml/xml.dart"; import "notification_details.dart"; +extension on DateTime { + String toIso8601StringTz() { + // Get offset + final offset = timeZoneOffset; + final sign = offset.isNegative ? "-" : "+"; + final hours = offset.inHours.abs().toString().padLeft(2, "0"); + final minutes = offset.inMinutes.abs().remainder(60).toString().padLeft(2, "0"); + final offsetString = "$sign$hours:$minutes"; + // Get first part of properly formatted ISO 8601 date + final formattedDate = toIso8601String().split(".").first; + return "$formattedDate$offsetString"; + } +} + +extension on WindowsNotificationDetails { + /// XML attributes for the toast notification as a whole. + Map get attributes => { + if (duration != null) "duration": duration!.name, + if (timestamp != null) "displayTimestamp": timestamp!.toIso8601StringTz(), + if (scenario != null) "scenario": scenario!.name, + }; +} + /// Converts a notification with [WindowsNotificationDetails] into well-formed XML. /// /// For more details, refer to the [Toast Notification XML schema](https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root). diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index 931ea1dd8..407941d5c 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -2,6 +2,7 @@ import "dart:ffi"; import "package:ffi/ffi.dart"; import "../details.dart"; +import "../details/notification_to_xml.dart"; import "../ffi/bindings.dart"; import "../ffi/utils.dart"; From 05848562bf38a947a75a0934e8c962bc86050489 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 8 Aug 2024 16:09:48 -0400 Subject: [PATCH 067/112] Fixed readme --- flutter_local_notifications/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_local_notifications/README.md b/flutter_local_notifications/README.md index f91ff6b9e..46a36ac2f 100644 --- a/flutter_local_notifications/README.md +++ b/flutter_local_notifications/README.md @@ -59,7 +59,7 @@ A cross platform plugin for displaying local notifications. * **iOS**. Uses the [UserNotification APIs](https://developer.apple.com/documentation/usernotifications) (aka the User Notifications Framework) * **macOS**. 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. Notification actions only work on macOS 10.14 or newer * **Linux**. Uses the [Desktop Notifications Specification](https://specifications.freedesktop.org/notification-spec/) -* **Windows** Uses the [C++/WinRT](https://learn.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/) implementation of [Toast Notifications](*https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-notifications-overview) +* **Windows** Uses the [C++/WinRT](https://learn.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/) implementation of [Toast Notifications](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-notifications-overview) Note: the plugin has a requires Flutter SDK 3.13 at a minimum. The list of support platforms for Flutter 3.13 itself can be found [here](https://github.com/flutter/website/blob/3d18ab48218101493af84953b71eac0cc6781fdd/src/reference/supported-platforms.md) From aa7f45f3cfd62b1c0f4d8e75fb8a5e5b11dcce2d Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 17:31:32 -0400 Subject: [PATCH 068/112] Fixed typo --- flutter_local_notifications/example/lib/windows.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index d3afd4cb2..e6c176d34 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -63,19 +63,19 @@ List examples({ }, ), PaddedElevatedButton( - buttonText: 'Show notitification with activation', + buttonText: 'Show notification with activation', onPressed: () async { await _showWindowsNotificationWithActivation(); }, ), PaddedElevatedButton( - buttonText: 'Show notitification with button styles', + buttonText: 'Show notification with button styles', onPressed: () async { await _showWindowsNotificationWithButtonStyle(); }, ), PaddedElevatedButton( - buttonText: 'Show notitifications in a group', + buttonText: 'Show notifications in a group', onPressed: () async { await _showWindowsNotificationWithHeader(); }, From 801cdb0c42fb9558c591098fb7f3cd07fb012f38 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 19:07:29 -0400 Subject: [PATCH 069/112] WindowsTextInput.hintText -> .placeHolderContent; --- flutter_local_notifications/example/lib/main.dart | 2 +- .../lib/src/details/notification_input.dart | 8 ++++---- .../test/details_test.dart | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 38c96afa6..20a5a6f9a 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -1163,7 +1163,7 @@ class _HomePageState extends State { WindowsAction(content: 'Send', arguments: 'send-reply', inputId: 'text'), ], inputs: [ - WindowsTextInput(id: 'text', title: 'Send a reply?', hintText: 'Message'), + WindowsTextInput(id: 'text', title: 'Send a reply?', placeHolderContent: 'Message'), ], ); diff --git a/flutter_local_notifications_windows/lib/src/details/notification_input.dart b/flutter_local_notifications_windows/lib/src/details/notification_input.dart index 8f9368498..ef5929a0a 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_input.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_input.dart @@ -37,12 +37,12 @@ class WindowsTextInput extends WindowsInput { /// Creates an input field in a notification. const WindowsTextInput({ required super.id, - this.hintText, + this.placeHolderContent, super.title, }) : super(type: WindowsInputType.text); - /// The hint text. - final String? hintText; + /// A placeholder shown before the user enters input, like a hint text. + final String? placeHolderContent; @override void toXml(XmlBuilder builder) => builder.element( @@ -51,7 +51,7 @@ class WindowsTextInput extends WindowsInput { "id": id, "type": type.name, if (title != null) "title": title!, - if (hintText != null) "placeHolderContent": hintText!, + if (placeHolderContent != null) "placeHolderContent": placeHolderContent!, }, ); } diff --git a/flutter_local_notifications_windows/test/details_test.dart b/flutter_local_notifications_windows/test/details_test.dart index e95e51964..776845067 100644 --- a/flutter_local_notifications_windows/test/details_test.dart +++ b/flutter_local_notifications_windows/test/details_test.dart @@ -95,7 +95,7 @@ void main() => group("Details:", () { }); test("Inputs", () async { - const textInput = WindowsTextInput(id: "input", hintText: "Text hint", title: "Text title"); + const textInput = WindowsTextInput(id: "input", placeHolderContent: "Text hint", title: "Text title"); const selection = WindowsSelectionInput(id: "input", items: [ WindowsSelection(id: "item1", content: "Item 1"), WindowsSelection(id: "item2", content: "Item 2"), From bfb2350a45e13b46ac88a47cc59c92210698cef0 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 19:09:14 -0400 Subject: [PATCH 070/112] Improved docs on WindowsNotificationText.placement --- .../lib/src/details/notification_text.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flutter_local_notifications_windows/lib/src/details/notification_text.dart b/flutter_local_notifications_windows/lib/src/details/notification_text.dart index 324316617..32c607a64 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_text.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_text.dart @@ -30,7 +30,9 @@ class WindowsNotificationText extends WindowsNotificationPart { /// Whether the text should be smaller like a caption. final bool isCaption; - /// The placement of this text. Null indicates default. + /// The placement of this text. + /// + /// The default placement (null) is in the main body of the notification. final WindowsTextPlacement? placement; /// The language of this text. From f100f5308ab219cc0dd8981d22bea6e656c2562b Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 19:18:34 -0400 Subject: [PATCH 071/112] Removed WindowsActivationType.background as it's only supported on UWP --- flutter_local_notifications/example/lib/windows.dart | 1 - .../lib/src/details/notification_action.dart | 4 ---- flutter_local_notifications_windows/test/details_test.dart | 1 - 3 files changed, 6 deletions(-) diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index e6c176d34..69781eb47 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -330,7 +330,6 @@ Future _showWindowsNotificationWithActivation() => flutterLocalNotificatio WindowsAction( content: 'Loading', arguments: 'loading', - activationType: WindowsActivationType.background, activationBehavior: WindowsNotificationBehavior.pendingUpdate, ), WindowsAction( diff --git a/flutter_local_notifications_windows/lib/src/details/notification_action.dart b/flutter_local_notifications_windows/lib/src/details/notification_action.dart index 1aa18d18b..ccc8ffa9e 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_action.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_action.dart @@ -6,13 +6,9 @@ import "package:xml/xml.dart"; // If you change their Dart names, be sure to override [Enum.name]. /// Decides how the [WindowsAction] will launch the app. -/// -/// On desktop platforms, [foreground] and [background] are treated the same. enum WindowsActivationType { /// The application will launch in the foreground (the default). foreground, - /// The application will launch as a background task. - background, /// Any application can be launched using its protocol. protocol, } diff --git a/flutter_local_notifications_windows/test/details_test.dart b/flutter_local_notifications_windows/test/details_test.dart index 776845067..3b9819459 100644 --- a/flutter_local_notifications_windows/test/details_test.dart +++ b/flutter_local_notifications_windows/test/details_test.dart @@ -43,7 +43,6 @@ void main() => group("Details:", () { content: "content", arguments: "args", activationBehavior: WindowsNotificationBehavior.pendingUpdate, - activationType: WindowsActivationType.background, buttonStyle: WindowsButtonStyle.success, inputId: "input-id", tooltip: "tooltip", From be53677e42e4dc0bc5a42d3f933a9950346003cf Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 19:40:10 -0400 Subject: [PATCH 072/112] Fixed warning about mixing struct and class --- .../src/plugin.hpp | 85 ++++++++++--------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/flutter_local_notifications_windows/src/plugin.hpp b/flutter_local_notifications_windows/src/plugin.hpp index 5e2c0b692..662bd59a0 100644 --- a/flutter_local_notifications_windows/src/plugin.hpp +++ b/flutter_local_notifications_windows/src/plugin.hpp @@ -12,45 +12,48 @@ using std::optional; using std::string; using namespace winrt::Windows::UI::Notifications; -class NativePlugin { - public: - /// Whether the plugin has been properly initialized. - bool isReady = false; - - /// Whether the current application has package identity (ie, was packaged with an MSIX). - /// - /// This impacts whether apps can query active notifications or cancel them. - /// For more details, see https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/package-identity-overview. - bool hasIdentity = false; - - /// The app user model ID. Used instead of package identity when [hasIdentity] is false. - /// - /// For more details, see https://learn.microsoft.com/en-us/windows/win32/shell/appids - winrt::hstring aumid; - - /// The API responsible for showing notifications. Null if [isReady] is false. - optional notifier; - - /// The API responsible for querying shown notifications. Null if [isReady] is false. - optional history; - - /// A callback to run when a notification is pressed, when the app is or is not running. - NativeNotificationCallback callback; - - NativePlugin() { } - ~NativePlugin() { } - - /// Checks whether the current application has package identity. See [hasIdentity] for details. - /// - /// Returns true or false if the package has identity, or null if an error occurred. - std::optional checkIdentity(); - - /// Registers the given [callback] to run when a notification is pressed. - bool registerApp( - const string& aumid, - const string& appName, - const string& guid, - const optional& iconPath, - NativeNotificationCallback callback - ); +/// The C++ container object for WinRT handles. +/// +/// Note that this must be a struct as it was forward-declared as a struct in +/// `ffi_api.h`, which cannot use classes as it must be C-compatible. +struct NativePlugin { + /// Whether the plugin has been properly initialized. + bool isReady = false; + + /// Whether the current application has package identity (ie, was packaged with an MSIX). + /// + /// This impacts whether apps can query active notifications or cancel them. + /// For more details, see https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/package-identity-overview. + bool hasIdentity = false; + + /// The app user model ID. Used instead of package identity when [hasIdentity] is false. + /// + /// For more details, see https://learn.microsoft.com/en-us/windows/win32/shell/appids + winrt::hstring aumid; + + /// The API responsible for showing notifications. Null if [isReady] is false. + optional notifier; + + /// The API responsible for querying shown notifications. Null if [isReady] is false. + optional history; + + /// A callback to run when a notification is pressed, when the app is or is not running. + NativeNotificationCallback callback; + + NativePlugin() { } + ~NativePlugin() { } + + /// Checks whether the current application has package identity. See [hasIdentity] for details. + /// + /// Returns true or false if the package has identity, or null if an error occurred. + std::optional checkIdentity(); + + /// Registers the given [callback] to run when a notification is pressed. + bool registerApp( + const string& aumid, + const string& appName, + const string& guid, + const optional& iconPath, + NativeNotificationCallback callback + ); }; From 0febd6a660a8e85bf3162e3084c25c6e03ba4cf9 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 19:40:47 -0400 Subject: [PATCH 073/112] Changed docs for ffi plugin --- flutter_local_notifications_windows/lib/src/plugin/ffi.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index 02223016c..5716f9703 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -20,7 +20,7 @@ extension on String { && this[23] == "-"; } -/// The FFI implementation of `package:flutter_local_notifications`. +/// The Windows implementation of `package:flutter_local_notifications`. class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { /// Registers the Windows implementation with Flutter. static void registerWith() { From 2d5a9153f35e202bf4779326ab155a458eee5146 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 19:44:16 -0400 Subject: [PATCH 074/112] Added zonedScheduleRawXml --- .../lib/src/plugin/base.dart | 11 +++++++++++ .../lib/src/plugin/ffi.dart | 13 +++++++++++++ .../lib/src/plugin/stub.dart | 8 ++++++++ 3 files changed, 32 insertions(+) diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart index 148912bf5..c46d5d4ae 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/base.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -46,6 +46,17 @@ abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatfor String? payload, }); + /// Schedules a notification to appear using raw XML at the given date and time. + /// + /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. + /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). + Future zonedScheduleRawXml( + int id, + String xml, + TZDateTime scheduledDate, + WindowsNotificationDetails? details, + ); + /// Updates the progress bar in the notification with the given ID. /// /// Note that in order to update [WindowsProgressBar.label], it must diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index 5716f9703..53ba4c6b7 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -193,6 +193,19 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { _bindings.scheduleNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), secondsSinceEpoch); }); + @override + Future zonedScheduleRawXml( + int id, + String xml, + TZDateTime scheduledDate, + WindowsNotificationDetails? details, + ) async => using((arena) { + if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); + if (scheduledDate.isBefore(DateTime.now())) throw ArgumentError("Flutter Local Notifications (Windows) cannot schedule notifications in the past"); + final secondsSinceEpoch = scheduledDate.millisecondsSinceEpoch ~/ 1000; + _bindings.scheduleNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), secondsSinceEpoch); + }); + @override Future updateBindings({required int id, required Map bindings}) async => using((arena) { if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); diff --git a/flutter_local_notifications_windows/lib/src/plugin/stub.dart b/flutter_local_notifications_windows/lib/src/plugin/stub.dart index f13475b29..cb1f4637a 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/stub.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/stub.dart @@ -51,6 +51,14 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { String? payload, }) async { } + @override + Future zonedScheduleRawXml( + int id, + String xml, + TZDateTime scheduledDate, + WindowsNotificationDetails? details, + ) async { } + @override Future updateBindings({ required int id, From d79c4dcac4923dadb3f37848f6ab5d47dfd74c2f Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 19:47:07 -0400 Subject: [PATCH 075/112] Doc comment tweak --- flutter_local_notifications_windows/lib/src/plugin/base.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart index c46d5d4ae..0aa6c2a17 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/base.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -17,7 +17,7 @@ abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatfor /// Releases any resources used by this plugin. void dispose(); - /// The raw XML passed to the Windows API. + /// Shows a notification using raw XML passed to the Windows APIs. /// /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). From 18c73ec7024a0f67def1679822093307680f48c9 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 19:49:48 -0400 Subject: [PATCH 076/112] Tweaked another doc comment --- flutter_local_notifications_windows/lib/src/plugin/base.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart index 0aa6c2a17..1c770c5dc 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/base.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -60,7 +60,7 @@ abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatfor /// Updates the progress bar in the notification with the given ID. /// /// Note that in order to update [WindowsProgressBar.label], it must - /// not have been set to null when [show] was called. + /// not have been set to `null` when the notification was created Future updateProgressBar({ required int notificationId, required WindowsProgressBar progressBar, From 9146d65df97de8c552aa6cda3837c298febb8a33 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 20:30:51 -0400 Subject: [PATCH 077/112] Removed custom lints, using 80 chars per line and standard lints --- .../analysis_options.yaml | 52 --- .../bin/crash.dart | 31 +- .../flutter_local_notifications_windows.dart | 5 +- .../lib/src/details.dart | 24 +- .../lib/src/details/notification_action.dart | 45 ++- .../lib/src/details/notification_audio.dart | 121 ++++--- .../lib/src/details/notification_details.dart | 65 ++-- .../lib/src/details/notification_header.dart | 14 +- .../lib/src/details/notification_image.dart | 28 +- .../lib/src/details/notification_input.dart | 38 +- .../lib/src/details/notification_part.dart | 6 +- .../src/details/notification_progress.dart | 25 +- .../lib/src/details/notification_row.dart | 14 +- .../lib/src/details/notification_text.dart | 18 +- .../lib/src/details/notification_to_xml.dart | 83 +++-- .../lib/src/ffi/utils.dart | 67 ++-- .../lib/src/plugin/base.dart | 24 +- .../lib/src/plugin/ffi.dart | 331 +++++++++++++----- .../lib/src/plugin/stub.dart | 55 ++- .../pubspec.yaml | 1 - .../test/bindings_test.dart | 59 +++- .../test/details_test.dart | 290 ++++++++++----- .../test/plugin_test.dart | 150 ++++---- .../test/scheduled_test.dart | 76 ++-- .../test/xml_test.dart | 57 +-- 25 files changed, 1040 insertions(+), 639 deletions(-) delete mode 100644 flutter_local_notifications_windows/analysis_options.yaml diff --git a/flutter_local_notifications_windows/analysis_options.yaml b/flutter_local_notifications_windows/analysis_options.yaml deleted file mode 100644 index 54e222c9d..000000000 --- a/flutter_local_notifications_windows/analysis_options.yaml +++ /dev/null @@ -1,52 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. See the following for docs: -# https://dart.dev/guides/language/analysis-options -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. -include: package:very_good_analysis/analysis_options.yaml # has more lints - -analyzer: - language: - # Strict casts isn't helpful with null safety. It only notifies you on `dynamic`, - # which happens all the time in JSON. - # - # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-casts.md - strict-casts: false - - # Don't let any types be inferred as `dynamic`. - # - # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-inference.md - strict-inference: true - - # Don't let Dart infer the wrong type on the left side of an assignment. - # - # See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-raw-types.md - strict-raw-types: true - - # These are only temporary while these files are in development. - # exclude: - -linter: - rules: - # Rules NOT in package:very_good_analysis - prefer_double_quotes: true - prefer_expression_function_bodies: true - avoid_types_on_closure_parameters: true - - # Rules to be disabled from package:very_good_analysis - prefer_single_quotes: false # prefer_double_quotes - lines_longer_than_80_chars: false # lines should be at most 100 chars - sort_pub_dependencies: false # Sort dependencies by function - use_key_in_widget_constructors: false # not in Flutter apps - directives_ordering: false # sort dart, then flutter, then package imports - always_use_package_imports: false # not when importing sibling files - sort_constructors_first: false # final properties, then constructor - avoid_dynamic_calls: false # this lint takes over errors in the IDE - one_member_abstracts: false # abstract classes are good for interfaces - cascade_invocations: false # cascades are often harder to read - - # Temporarily disabled until we are ready to document - # public_member_api_docs: false - # flutter_style_todos: false diff --git a/flutter_local_notifications_windows/bin/crash.dart b/flutter_local_notifications_windows/bin/crash.dart index cc64d3ad5..b9ada9c60 100644 --- a/flutter_local_notifications_windows/bin/crash.dart +++ b/flutter_local_notifications_windows/bin/crash.dart @@ -11,12 +11,16 @@ // for users as it depends on having two plugins instantiated at the same time, // which is not recommended, but I left it here as a demonstration if needed. -import "dart:isolate"; +import 'dart:isolate'; -import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; -import "package:timezone/standalone.dart"; +import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart'; +import 'package:timezone/standalone.dart'; -const settings = WindowsInitializationSettings(appName: "Test app", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); +const WindowsInitializationSettings settings = WindowsInitializationSettings( + appName: 'Test app', + appUserModelId: 'com.test.test', + guid: 'a8c22b55-049e-422f-b30f-863694de08c8', +); void main() async { await Isolate.spawn(bindingsTest, null); @@ -27,23 +31,28 @@ void main() async { Future scheduledTest(_) async { await Future.delayed(const Duration(seconds: 4)); - final plugin = FlutterLocalNotificationsWindows(); + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); await plugin.initialize(settings); await initializeTimeZone(); - final location = getLocation("US/Eastern"); - final now = TZDateTime.now(location); - final later = now.add(const Duration(days: 1)); + final Location location = getLocation('US/Eastern'); + final TZDateTime now = TZDateTime.now(location); + final TZDateTime later = now.add(const Duration(days: 1)); await plugin.zonedSchedule(300, null, null, later, null); await plugin.zonedSchedule(301, null, null, later, null); await plugin.zonedSchedule(302, null, null, later, null); } Future bindingsTest(_) async { - final bindings = {"title": "Bindings title", "body": "Bindings body"}; + final Map bindings = { + 'title': 'Bindings title', + 'body': 'Bindings body' + }; await Future.delayed(const Duration(seconds: 1)); - final plugin = FlutterLocalNotificationsWindows(); + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); await plugin.initialize(settings); - await plugin.show(503, "{title}", "{body}"); + await plugin.show(503, '{title}', '{body}'); await Future.delayed(const Duration(milliseconds: 100)); await plugin.updateBindings(id: 503, bindings: bindings); await plugin.updateBindings(id: 503, bindings: bindings); diff --git a/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart index 0816f1cfd..06a875848 100644 --- a/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart +++ b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart @@ -1,3 +1,2 @@ -export "src/details.dart"; -export "src/plugin/stub.dart" - if (dart.library.ffi) "src/plugin/ffi.dart"; +export 'src/details.dart'; +export 'src/plugin/stub.dart' if (dart.library.ffi) 'src/plugin/ffi.dart'; diff --git a/flutter_local_notifications_windows/lib/src/details.dart b/flutter_local_notifications_windows/lib/src/details.dart index 6a33441bb..ec48637f0 100644 --- a/flutter_local_notifications_windows/lib/src/details.dart +++ b/flutter_local_notifications_windows/lib/src/details.dart @@ -1,21 +1,23 @@ -export "details/initialization_settings.dart"; -export "details/notification_action.dart"; -export "details/notification_audio.dart"; -export "details/notification_details.dart"; -export "details/notification_row.dart"; -export "details/notification_header.dart"; -export "details/notification_image.dart"; -export "details/notification_input.dart"; -export "details/notification_part.dart"; -export "details/notification_progress.dart"; -export "details/notification_text.dart"; +export 'details/initialization_settings.dart'; +export 'details/notification_action.dart'; +export 'details/notification_audio.dart'; +export 'details/notification_details.dart'; +export 'details/notification_header.dart'; +export 'details/notification_image.dart'; +export 'details/notification_input.dart'; +export 'details/notification_part.dart'; +export 'details/notification_progress.dart'; +export 'details/notification_row.dart'; +export 'details/notification_text.dart'; /// The result of updating a notification. enum NotificationUpdateResult { /// The update was successful. success, + /// There was an unexpected error updating the notification. error, + /// No notification with the provided ID could be found. notFound, } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_action.dart b/flutter_local_notifications_windows/lib/src/details/notification_action.dart index ccc8ffa9e..457ca04f1 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_action.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_action.dart @@ -1,6 +1,6 @@ -import "dart:io"; +import 'dart:io'; -import "package:xml/xml.dart"; +import 'package:xml/xml.dart'; // NOTE: All enum values in this file have Windows RT-specific names. // If you change their Dart names, be sure to override [Enum.name]. @@ -9,6 +9,7 @@ import "package:xml/xml.dart"; enum WindowsActivationType { /// The application will launch in the foreground (the default). foreground, + /// Any application can be launched using its protocol. protocol, } @@ -16,11 +17,13 @@ enum WindowsActivationType { /// Decides how a [WindowsAction] will react to being pressed. enum WindowsNotificationBehavior { /// The notification will be dismissed. - dismiss("default"), + dismiss('default'), + /// The notification will remain on screen and show a loading status. - pendingUpdate("pendingUpdate"); + pendingUpdate('pendingUpdate'); const WindowsNotificationBehavior(this.name); + /// The Windows API name for this choice. final String name; } @@ -28,11 +31,13 @@ enum WindowsNotificationBehavior { /// Decides how a [WindowsAction] will be styled. enum WindowsButtonStyle { /// A green button. - success("Success"), + success('Success'), + /// A red button. - critical("Critical"); + critical('Critical'); const WindowsButtonStyle(this.name); + /// The Windows API name for this choice. final String name; } @@ -104,21 +109,25 @@ class WindowsAction { /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action#syntax void toXml(XmlBuilder builder) { if (image != null && !image!.isAbsolute) { - throw ArgumentError.value(image!.path, "WindowsImage.file", "File path must be absolute"); + throw ArgumentError.value( + image!.path, + 'WindowsImage.file', + 'File path must be absolute', + ); } builder.element( - "action", - attributes: { - "content": content, - "arguments": arguments, - "activationType": activationType.name, - "afterActivationBehavior": activationBehavior.name, - if (placement != null) "placement": placement!.name, - if (image != null) "imageUri": + 'action', + attributes: { + 'content': content, + 'arguments': arguments, + 'activationType': activationType.name, + 'afterActivationBehavior': activationBehavior.name, + if (placement != null) 'placement': placement!.name, + if (image != null) 'imageUri': Uri.file(image!.absolute.path, windows: true).toFilePath(), - if (inputId != null) "hint-inputId": inputId!, - if (buttonStyle != null) "hint-buttonStyle": buttonStyle!.name, - if (tooltip != null) "hint-toolTip": tooltip!, + if (inputId != null) 'hint-inputId': inputId!, + if (buttonStyle != null) 'hint-buttonStyle': buttonStyle!.name, + if (tooltip != null) 'hint-toolTip': tooltip!, }, ); } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart index 2bb3ade74..bdcdbf4e6 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart @@ -1,64 +1,89 @@ -import "package:xml/xml.dart"; +import 'package:xml/xml.dart'; extension on Uri { String get filename => pathSegments.last; - String get extension => pathSegments.last.split(".").last; + String get extension => pathSegments.last.split('.').last; } /// A preset sound for a Windows notification. enum WindowsNotificationSound { /// The default sound. - defaultSound("ms-winsoundevent:Notification.Default"), + defaultSound('ms-winsoundevent:Notification.Default'), + /// The IM sound. - im("ms-winsoundevent:Notification.IM"), + im('ms-winsoundevent:Notification.IM'), + /// The Mail sound. - mail("ms-winsoundevent:Notification.Mail"), + mail('ms-winsoundevent:Notification.Mail'), + /// The Reminder sound. - reminder("ms-winsoundevent:Notification.Reminder"), + reminder('ms-winsoundevent:Notification.Reminder'), + /// The SMS sound. - sms("ms-winsoundevent:Notification.SMS"), + sms('ms-winsoundevent:Notification.SMS'), + /// Alarm sound 1. - alarm1("ms-winsoundevent:Notification.Looping.Alarm1"), + alarm1('ms-winsoundevent:Notification.Looping.Alarm1'), + /// Alarm sound 2. - alarm2("ms-winsoundevent:Notification.Looping.Alarm2"), + alarm2('ms-winsoundevent:Notification.Looping.Alarm2'), + /// Alarm sound 3. - alarm3("ms-winsoundevent:Notification.Looping.Alarm3"), + alarm3('ms-winsoundevent:Notification.Looping.Alarm3'), + /// Alarm sound 4. - alarm4("ms-winsoundevent:Notification.Looping.Alarm4"), + alarm4('ms-winsoundevent:Notification.Looping.Alarm4'), + /// Alarm sound 5. - alarm5("ms-winsoundevent:Notification.Looping.Alarm5"), + alarm5('ms-winsoundevent:Notification.Looping.Alarm5'), + /// Alarm sound 6. - alarm6("ms-winsoundevent:Notification.Looping.Alarm6"), + alarm6('ms-winsoundevent:Notification.Looping.Alarm6'), + /// Alarm sound 7. - alarm7("ms-winsoundevent:Notification.Looping.Alarm7"), + alarm7('ms-winsoundevent:Notification.Looping.Alarm7'), + /// Alarm sound 8. - alarm8("ms-winsoundevent:Notification.Looping.Alarm8"), + alarm8('ms-winsoundevent:Notification.Looping.Alarm8'), + /// Alarm sound 9. - alarm9("ms-winsoundevent:Notification.Looping.Alarm9"), + alarm9('ms-winsoundevent:Notification.Looping.Alarm9'), + /// Alarm sound 10. - alarm10("ms-winsoundevent:Notification.Looping.Alarm10"), + alarm10('ms-winsoundevent:Notification.Looping.Alarm10'), + /// Call sound 1. - call1("ms-winsoundevent:Notification.Looping.Call1"), + call1('ms-winsoundevent:Notification.Looping.Call1'), + /// Call sound 2. - call2("ms-winsoundevent:Notification.Looping.Call2"), + call2('ms-winsoundevent:Notification.Looping.Call2'), + /// Call sound 3. - call3("ms-winsoundevent:Notification.Looping.Call3"), + call3('ms-winsoundevent:Notification.Looping.Call3'), + /// Call sound 4. - call4("ms-winsoundevent:Notification.Looping.Call4"), + call4('ms-winsoundevent:Notification.Looping.Call4'), + /// Call sound 5. - call5("ms-winsoundevent:Notification.Looping.Call5"), + call5('ms-winsoundevent:Notification.Looping.Call5'), + /// Call sound 6. - call6("ms-winsoundevent:Notification.Looping.Call6"), + call6('ms-winsoundevent:Notification.Looping.Call6'), + /// Call sound 7. - call7("ms-winsoundevent:Notification.Looping.Call7"), + call7('ms-winsoundevent:Notification.Looping.Call7'), + /// Call sound 8. - call8("ms-winsoundevent:Notification.Looping.Call8"), + call8('ms-winsoundevent:Notification.Looping.Call8'), + /// Call sound 9. - call9("ms-winsoundevent:Notification.Looping.Call9"), + call9('ms-winsoundevent:Notification.Looping.Call9'), + /// Call sound 10. - call10("ms-winsoundevent:Notification.Looping.Call10"); + call10('ms-winsoundevent:Notification.Looping.Call10'); const WindowsNotificationSound(this.name); + /// The Windows API name for this sound. final String name; } @@ -75,8 +100,7 @@ class WindowsNotificationAudio { WindowsNotificationAudio.preset({ required WindowsNotificationSound sound, this.shouldLoop = false, - }) : - isSilent = false, + }) : isSilent = false, source = sound.name; /// Audio from a file. See [allowedSchemes] and [allowedExtensions]. @@ -85,47 +109,56 @@ class WindowsNotificationAudio { this.shouldLoop = false, }) : isSilent = false, - source = file.toFilePath() { + source = file.toFilePath() + { if (!allowedSchemes.contains(file.scheme)) { throw ArgumentError.value( file.toString(), - "WindowsNotificationAudio.file", - "URI scheme must be one of the following schemes: $allowedSchemes", + 'WindowsNotificationAudio.file', + 'URI scheme must be one of the following schemes: $allowedSchemes', ); } if ( - !file.filename.contains(".") || - !allowedExtensions.contains(file.extension) + !file.filename.contains('.') + || !allowedExtensions.contains(file.extension) ) { throw ArgumentError.value( file.toString(), - "WindowsNotificationAudio.file", - "File extension must be one of the following: $allowedExtensions", + 'WindowsNotificationAudio.file', + 'File extension must be one of the following: $allowedExtensions', ); } } /// Allowed Uri schemes for [WindowsNotificationAudio.fromFile]. - static const Set allowedSchemes = {"ms-appx", "ms-resource"}; + static const Set allowedSchemes = {'ms-appx', 'ms-resource'}; /// Allowed file extensions for [WindowsNotificationAudio.fromFile]. - static const Set allowedExtensions = - {"aac", "flac", "m4a", "mp3", "wav", "wma"}; + static const Set allowedExtensions = { + 'aac', + 'flac', + 'm4a', + 'mp3', + 'wav', + 'wma' + }; /// Whether this audio should loop. final bool shouldLoop; + /// Whether this notification should be silent. final bool isSilent; + /// The source of the audio. final String source; /// Serializes this audio to Windows-compatible XML. void toXml(XmlBuilder builder) => builder.element( - "audio", - attributes: { - "src": source, - "silent": isSilent.toString(), - "loop": shouldLoop.toString(), + 'audio', + attributes: { + 'src': source, + 'silent': isSilent.toString(), + 'loop': shouldLoop.toString(), }, ); } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_details.dart b/flutter_local_notifications_windows/lib/src/details/notification_details.dart index 518759411..d7c6ab39e 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_details.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_details.dart @@ -1,20 +1,21 @@ -import "package:xml/xml.dart"; +import 'package:xml/xml.dart'; -import "notification_action.dart"; -import "notification_audio.dart"; -import "notification_row.dart"; -import "notification_header.dart"; -import "notification_image.dart"; -import "notification_input.dart"; -import "notification_progress.dart"; +import 'notification_action.dart'; +import 'notification_audio.dart'; +import 'notification_header.dart'; +import 'notification_image.dart'; +import 'notification_input.dart'; +import 'notification_progress.dart'; +import 'notification_row.dart'; -export "notification_part.dart"; -export "notification_text.dart"; +export 'notification_part.dart'; +export 'notification_text.dart'; /// The duration for a Windows notification. enum WindowsNotificationDuration { /// The notification will stay for a long time. long, + /// The notification will stay for a short time. short, } @@ -52,7 +53,7 @@ class WindowsNotificationDetails { this.images = const [], this.groups = const [], this.progressBars = const [], - this.bindings = const {}, + this.bindings = const {}, this.header, this.audio, this.duration, @@ -96,27 +97,35 @@ class WindowsNotificationDetails { /// Custom bindings in the notification. /// - /// Text elements can contains "bindings", which are entered as `{bindingName}` directly into the - /// string values. You can then update them while or after the notification is launched by - /// using the binding name as the key here, and the value as any string you want + /// Text elements can contains "bindings", which are entered as + /// `{bindingName}` directly into the string values. You can then update them + /// while or after the notification is launched by using the binding name as + /// the key here, and the value as any string you want. final Map bindings; /// Builds all relevant XML parts under the root `` element. void toXml(XmlBuilder builder) { if (actions.length > 5) { - throw ArgumentError("WindowsNotificationDetails can only have up to 5 actions"); + throw ArgumentError( + 'WindowsNotificationDetails can only have up to 5 actions', + ); } if (inputs.length > 5) { - throw ArgumentError("WindowsNotificationDetails can only have up to 5 inputs"); + throw ArgumentError( + 'WindowsNotificationDetails can only have up to 5 inputs', + ); } - builder.element("actions", nest: () { - for (final input in inputs) { - input.toXml(builder); - } - for (final action in actions) { - action.toXml(builder); - } - },); + builder.element( + 'actions', + nest: () { + for (final WindowsInput input in inputs) { + input.toXml(builder); + } + for (final WindowsAction action in actions) { + action.toXml(builder); + } + }, + ); audio?.toXml(builder); header?.toXml(builder); } @@ -126,15 +135,15 @@ class WindowsNotificationDetails { /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-binding void generateBinding(XmlBuilder builder) { if (subtitle != null) { - builder.element("text", nest: subtitle); + builder.element('text', nest: subtitle); } - for (final image in images) { + for (final WindowsImage image in images) { image.toXml(builder); } - for (final group in groups) { + for (final WindowsRow group in groups) { group.toXml(builder); } - for (final progressBar in progressBars) { + for (final WindowsProgressBar progressBar in progressBars) { progressBar.toXml(builder); } } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_header.dart b/flutter_local_notifications_windows/lib/src/details/notification_header.dart index 3b23d8ce0..4efa913bc 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_header.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_header.dart @@ -1,4 +1,4 @@ -import "package:xml/xml.dart"; +import 'package:xml/xml.dart'; /// Decides how the application will open when the header is pressed. enum WindowsHeaderActivation { @@ -33,12 +33,12 @@ class WindowsHeader { /// Serializes this header to XML. void toXml(XmlBuilder builder) => builder.element( - "header", - attributes: { - "id": id, - "title": title, - "arguments": arguments, - if (activation != null) "activationType": activation!.name, + 'header', + attributes: { + 'id': id, + 'title': title, + 'arguments': arguments, + if (activation != null) 'activationType': activation!.name, }, ); } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_image.dart b/flutter_local_notifications_windows/lib/src/details/notification_image.dart index 20cd04620..09e1bd2cb 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_image.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_image.dart @@ -1,13 +1,14 @@ -import "dart:io"; +import 'dart:io'; -import "package:xml/xml.dart"; +import 'package:xml/xml.dart'; -import "notification_part.dart"; +import 'notification_part.dart'; /// Where a Windows notification image can be placed. enum WindowsImagePlacement { /// The image replaces the app logo. appLogoOverride, + /// The image is shown on top of the notification body. hero, } @@ -49,16 +50,19 @@ class WindowsImage extends WindowsNotificationPart { if (!file.isAbsolute) { throw ArgumentError.value( file.path, - "WindowsImage.file", - "File path must be absolute", + 'WindowsImage.file', + 'File path must be absolute', ); } - builder.element("image", attributes: { - "src": Uri.file(file.absolute.path, windows: true).toFilePath(), - "alt": altText, - "addImageQuery": addQueryParams.toString(), - if (placement != null) "placement": placement!.name, - if (crop != null) "hint-crop": crop!.name, - },); + builder.element( + 'image', + attributes: { + 'src': Uri.file(file.absolute.path, windows: true).toFilePath(), + 'alt': altText, + 'addImageQuery': addQueryParams.toString(), + if (placement != null) 'placement': placement!.name, + if (crop != null) 'hint-crop': crop!.name, + }, + ); } } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_input.dart b/flutter_local_notifications_windows/lib/src/details/notification_input.dart index ef5929a0a..d7d3eeefe 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_input.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_input.dart @@ -1,9 +1,10 @@ -import "package:xml/xml.dart"; +import 'package:xml/xml.dart'; /// The type of a [WindowsInput]. enum WindowsInputType { /// A text input. text, + /// A multiple choice input. selection, } @@ -46,12 +47,13 @@ class WindowsTextInput extends WindowsInput { @override void toXml(XmlBuilder builder) => builder.element( - "input", - attributes: { - "id": id, - "type": type.name, - if (title != null) "title": title!, - if (placeHolderContent != null) "placeHolderContent": placeHolderContent!, + 'input', + attributes: { + 'id': id, + 'type': type.name, + if (title != null) 'title': title!, + if (placeHolderContent != null) + 'placeHolderContent': placeHolderContent!, }, ); } @@ -74,15 +76,15 @@ class WindowsSelectionInput extends WindowsInput { @override void toXml(XmlBuilder builder) => builder.element( - "input", - attributes: { - "id": id, - "type": type.name, - if (title != null) "title": title!, - if (defaultItem != null) "defaultInput": defaultItem!, + 'input', + attributes: { + 'id': id, + 'type': type.name, + if (title != null) 'title': title!, + if (defaultItem != null) 'defaultInput': defaultItem!, }, nest: () { - for (final item in items) { + for (final WindowsSelection item in items) { item.toXml(builder); } }, @@ -105,10 +107,10 @@ class WindowsSelection { /// Serializes this item to XML. void toXml(XmlBuilder builder) => builder.element( - "selection", - attributes: { - "id": id, - "content": content, + 'selection', + attributes: { + 'id': id, + 'content': content, }, ); } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_part.dart b/flutter_local_notifications_windows/lib/src/details/notification_part.dart index 40f42a810..c62087dc1 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_part.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_part.dart @@ -1,9 +1,13 @@ -import "package:xml/xml.dart"; +import 'package:xml/xml.dart'; /// A text or image element in a Windows notification. /// /// Note: This should not be used for anything else as notification /// groups can only contain text and images. +// This class needs to be abstract so [WindowsNotificationText] and +// [WindowsImage] can extend it. Specifically, this class is a marker +// type for classes that are valid as part of a [WindowsColumn]. +// ignore: one_member_abstracts abstract class WindowsNotificationPart { /// A const constructor. const WindowsNotificationPart(); diff --git a/flutter_local_notifications_windows/lib/src/details/notification_progress.dart b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart index ef1ca2f67..f5f3c6c15 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_progress.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart @@ -1,6 +1,6 @@ -import "package:xml/xml.dart"; +import 'package:xml/xml.dart'; -import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; +import '../../flutter_local_notifications_windows.dart'; /// A progress bar in a Windows notification. /// @@ -37,21 +37,12 @@ class WindowsProgressBar { /// Serializes this progress bar to XML. void toXml(XmlBuilder builder) => builder.element( - "progress", - attributes: { - "status": status, - "value": "{$id-progressValue}", - if (title != null) "title": title!, - if (label != null) "valueStringOverride": "{$id-progressString}", + 'progress', + attributes: { + 'status': status, + 'value': '{$id-progressValue}', + if (title != null) 'title': title!, + if (label != null) 'valueStringOverride': '{$id-progressString}', }, ); - - /// The data bindings for this progress bar. - /// - /// To support dynamic updates, [toXml] will inject placeholder strings called data bindings - /// instead of actual values. This represents the new data. - Map get data => { - "$id-progressValue": value?.toString() ?? "indeterminate", - if (label != null) "$id-progressString": label!, - }; } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_row.dart b/flutter_local_notifications_windows/lib/src/details/notification_row.dart index 0d51d465b..cb8002900 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_row.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_row.dart @@ -1,6 +1,6 @@ -import "package:xml/xml.dart"; +import 'package:xml/xml.dart'; -import "notification_part.dart"; +import 'notification_part.dart'; /// A group of notification content that must be displayed as a whole row. /// @@ -14,14 +14,14 @@ class WindowsRow { /// Serializes this group to XML. void toXml(XmlBuilder builder) => builder.element( - "group", + 'group', nest: () { - for (final column in columns) { + for (final WindowsColumn column in columns) { builder.element( - "subgroup", - attributes: {"hint-weight": "1"}, + 'subgroup', + attributes: {'hint-weight': '1'}, nest: () { - for (final part in column.parts) { + for (final WindowsNotificationPart part in column.parts) { part.toXml(builder); } }, diff --git a/flutter_local_notifications_windows/lib/src/details/notification_text.dart b/flutter_local_notifications_windows/lib/src/details/notification_text.dart index 32c607a64..cdda9be8e 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_text.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_text.dart @@ -1,6 +1,6 @@ -import "package:xml/xml.dart"; +import 'package:xml/xml.dart'; -import "notification_part.dart"; +import 'notification_part.dart'; /// Where text can be placed in a Windows notification. enum WindowsTextPlacement { @@ -40,13 +40,13 @@ class WindowsNotificationText extends WindowsNotificationPart { @override void toXml(XmlBuilder builder) => builder.element( - "text", - attributes: { - if (languageCode != null) "lang": languageCode!, - if (placement != null) "placement": placement!.name, - "hint-callScenarioCenterAlign": centerIfCall.toString(), - "hint-align": "center", - if (isCaption) "hint-style": "captionsubtle", + 'text', + attributes: { + if (languageCode != null) 'lang': languageCode!, + if (placement != null) 'placement': placement!.name, + 'hint-callScenarioCenterAlign': centerIfCall.toString(), + 'hint-align': 'center', + if (isCaption) 'hint-style': 'captionsubtle', }, nest: text, ); diff --git a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart index e6887e0e7..0ce0e87fa 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart @@ -1,31 +1,45 @@ -import "package:xml/xml.dart"; +import 'package:xml/xml.dart'; -import "notification_details.dart"; +import '../../flutter_local_notifications_windows.dart'; extension on DateTime { String toIso8601StringTz() { // Get offset - final offset = timeZoneOffset; - final sign = offset.isNegative ? "-" : "+"; - final hours = offset.inHours.abs().toString().padLeft(2, "0"); - final minutes = offset.inMinutes.abs().remainder(60).toString().padLeft(2, "0"); - final offsetString = "$sign$hours:$minutes"; + final Duration offset = timeZoneOffset; + final String sign = offset.isNegative ? '-' : '+'; + final String hours = offset.inHours.abs().toString().padLeft(2, '0'); + final String minutes = + offset.inMinutes.abs().remainder(60).toString().padLeft(2, '0'); + final String offsetString = '$sign$hours:$minutes'; // Get first part of properly formatted ISO 8601 date - final formattedDate = toIso8601String().split(".").first; - return "$formattedDate$offsetString"; + final String formattedDate = toIso8601String().split('.').first; + return '$formattedDate$offsetString'; } } extension on WindowsNotificationDetails { /// XML attributes for the toast notification as a whole. - Map get attributes => { - if (duration != null) "duration": duration!.name, - if (timestamp != null) "displayTimestamp": timestamp!.toIso8601StringTz(), - if (scenario != null) "scenario": scenario!.name, + Map get attributes => { + if (duration != null) 'duration': duration!.name, + if (timestamp != null) + 'displayTimestamp': timestamp!.toIso8601StringTz(), + if (scenario != null) 'scenario': scenario!.name, }; } -/// Converts a notification with [WindowsNotificationDetails] into well-formed XML. +/// Extensions on [WindowsProgressBar]. +extension ProgressBarXml on WindowsProgressBar { + /// The data bindings for this progress bar. + /// + /// To support dynamic updates, [toXml] will inject placeholder strings called + /// data bindings instead of actual values. This represents the new data. + Map get data => { + '$id-progressValue': value?.toString() ?? 'indeterminate', + if (label != null) '$id-progressString': label!, + }; +} + +/// Converts a notification with [WindowsNotificationDetails] into XML. /// /// For more details, refer to the [Toast Notification XML schema](https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root). String notificationToXml({ @@ -34,29 +48,34 @@ String notificationToXml({ String? payload, WindowsNotificationDetails? details, }) { - final builder = XmlBuilder(); + final XmlBuilder builder = XmlBuilder(); builder.element( - "toast", - attributes: { - ...details?.attributes ?? {}, - if (payload != null) "launch": payload, - if (details?.scenario == null) "useButtonStyle": "true", + 'toast', + attributes: { + ...details?.attributes ?? {}, + if (payload != null) 'launch': payload, + if (details?.scenario == null) 'useButtonStyle': 'true', }, nest: () { - builder.element("visual", nest: () { - builder.element( - "binding", - attributes: {"template": "ToastGeneric"}, - nest: () { - builder.element("text", nest: title); - builder.element("text", nest: body); - details?.generateBinding(builder); - }, - ); - },); + builder.element( + 'visual', + nest: () { + builder.element( + 'binding', + attributes: {'template': 'ToastGeneric'}, + nest: () { + builder + ..element('text', nest: title) + ..element('text', nest: body); + details?.generateBinding(builder); + }, + ); + }, + ); details?.toXml(builder); }, ); - return builder.buildDocument() + return builder + .buildDocument() .toXmlString(pretty: true, indentAttribute: (_) => true); } diff --git a/flutter_local_notifications_windows/lib/src/ffi/utils.dart b/flutter_local_notifications_windows/lib/src/ffi/utils.dart index 6a77a386e..1287103e0 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/utils.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/utils.dart @@ -1,55 +1,64 @@ -import "dart:ffi"; +import 'dart:ffi'; -import "package:ffi/ffi.dart"; -import "package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart"; -import "package:flutter_local_notifications_windows/src/plugin/base.dart"; +import 'package:ffi/ffi.dart'; +import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; -import "bindings.dart"; -import "../details.dart"; +import '../details.dart'; +import '../plugin/base.dart'; +import 'bindings.dart'; /// Helpful methods on native string maps. extension NativeStringMapUtils on NativeStringMap { /// Converts this map to a typical Dart map. - Map toMap() => { - for (var index = 0; index < size; index++) - entries[index].key.toDartString(): entries[index].value.toDartString(), + Map toMap() => { + for (int index = 0; index < size; index++) + entries[index].key.toDartString(): + entries[index].value.toDartString(), }; } /// Helpful methods on integers. extension IntUtils on int { - /// Converts this integer into a boolean. Useful for return types of C functions. + /// Converts this integer into a boolean. + /// + /// Useful for return types of C functions. bool toBool() => this == 1; } /// Gets the [NotificationResponseType] from a [NativeLaunchType]. NotificationResponseType getResponseType(int launchType) { switch (NativeLaunchType.fromValue(launchType)) { - case NativeLaunchType.notification: return NotificationResponseType.selectedNotification; - case NativeLaunchType.action: return NotificationResponseType.selectedNotificationAction; + case NativeLaunchType.notification: + return NotificationResponseType.selectedNotification; + case NativeLaunchType.action: + return NotificationResponseType.selectedNotificationAction; } } /// Gets the [NotificationUpdateResult] from a [NativeUpdateResult]. NotificationUpdateResult getUpdateResult(NativeUpdateResult result) { switch (result) { - case NativeUpdateResult.success: return NotificationUpdateResult.success; - case NativeUpdateResult.failed: return NotificationUpdateResult.error; - case NativeUpdateResult.notFound: return NotificationUpdateResult.notFound; + case NativeUpdateResult.success: + return NotificationUpdateResult.success; + case NativeUpdateResult.failed: + return NotificationUpdateResult.error; + case NativeUpdateResult.notFound: + return NotificationUpdateResult.notFound; } } /// Helpful methods on string maps. extension MapToNativeMap on Map { - /// Allocates and returns a pointer to a [NativeStringMap] using the provided arena. + /// Allocates a [NativeStringMap] using the provided arena. NativeStringMap toNativeMap(Arena arena) { - final pointer = arena(); + final Pointer pointer = arena(); pointer.ref.size = length; pointer.ref.entries = arena(length); - var index = 0; - for (final entry in entries) { + int index = 0; + for (final MapEntry entry in entries) { pointer.ref.entries[index].key = entry.key.toNativeUtf8(allocator: arena); - pointer.ref.entries[index].value = entry.value.toNativeUtf8(allocator: arena); + pointer.ref.entries[index].value = + entry.value.toNativeUtf8(allocator: arena); index++; } return pointer.ref; @@ -59,14 +68,16 @@ extension MapToNativeMap on Map { /// Helpful methods on native notification details. extension NativeNotificationDetailsUtils on Pointer { /// Parses this array as a list of [ActiveNotification]s. - List asActiveNotifications(int length) => [ - for (var index = 0; index < length; index++) - ActiveNotification(id: this[index].id), - ]; + List asActiveNotifications(int length) => + [ + for (int index = 0; index < length; index++) + ActiveNotification(id: this[index].id), + ]; /// Parses this array os a list of [PendingNotificationRequest]s. - List asPendingRequests(int length) => [ - for (var index = 0; index < length; index++) - PendingNotificationRequest(this[index].id, null, null, null), - ]; + List asPendingRequests(int length) => + [ + for (int index = 0; index < length; index++) + PendingNotificationRequest(this[index].id, null, null, null), + ]; } diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart index 1c770c5dc..8d4313c17 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/base.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -1,13 +1,16 @@ -import "package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart"; -import "package:timezone/timezone.dart"; +import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; +import 'package:timezone/timezone.dart'; -import "../details.dart"; +import '../details.dart'; +import '../details/notification_to_xml.dart'; -export "package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart"; -export "package:timezone/timezone.dart"; +export 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; +export 'package:timezone/timezone.dart'; /// The Windows implementation of `package:flutter_local_notifications`. -abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatform { +abstract class WindowsNotificationsBase + extends FlutterLocalNotificationsPlatform +{ /// Initializes the plugin. No other method should be called before this. Future initialize( WindowsInitializationSettings settings, { @@ -24,7 +27,7 @@ abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatfor Future showRawXml({ required int id, required String xml, - Map bindings = const {}, + Map bindings = const {}, }); @override @@ -46,7 +49,7 @@ abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatfor String? payload, }); - /// Schedules a notification to appear using raw XML at the given date and time. + /// Schedules a notification to appear using raw XML at this date and time. /// /// See https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root. /// For validation, see [the Windows Notifications Visualizer](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/notifications-visualizer). @@ -64,7 +67,10 @@ abstract class WindowsNotificationsBase extends FlutterLocalNotificationsPlatfor Future updateProgressBar({ required int notificationId, required WindowsProgressBar progressBar, - }) => updateBindings(id: notificationId, bindings: progressBar.data); + }) => updateBindings( + id: notificationId, + bindings: progressBar.data, + ); /// Updates any data binding in the given notification. /// diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index 53ba4c6b7..40b8d7038 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -1,39 +1,46 @@ -import "dart:ffi"; -import "package:ffi/ffi.dart"; +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; -import "../details.dart"; -import "../details/notification_to_xml.dart"; -import "../ffi/bindings.dart"; -import "../ffi/utils.dart"; +import '../details.dart'; +import '../details/notification_to_xml.dart'; +import '../ffi/bindings.dart'; +import '../ffi/utils.dart'; -import "base.dart"; +import 'base.dart'; void _globalLaunchCallback(NativeLaunchDetails details) { FlutterLocalNotificationsWindows.instance?._onNotificationReceived(details); } extension on String { - bool get isValidGuid => length == 36 - && this[8] == "-" - && this[13] == "-" - && this[18] == "-" - && this[23] == "-"; + bool get isValidGuid => + length == 36 && + this[8] == '-' && + this[13] == '-' && + this[18] == '-' && + this[23] == '-'; } /// The Windows implementation of `package:flutter_local_notifications`. class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { + /// Creates an instance of the native plugin. + FlutterLocalNotificationsWindows(); + /// Registers the Windows implementation with Flutter. static void registerWith() { - FlutterLocalNotificationsPlatform.instance = FlutterLocalNotificationsWindows(); + FlutterLocalNotificationsPlatform.instance = + FlutterLocalNotificationsWindows(); } /// The global instance of this plugin. Used in [_globalLaunchCallback]. static FlutterLocalNotificationsWindows? instance; /// The FFI generated bindings to the native code. - late final NotificationsPluginBindings _bindings = NotificationsPluginBindings(_library); + late final NotificationsPluginBindings _bindings = + NotificationsPluginBindings(_library); - final DynamicLibrary _library = DynamicLibrary.open("flutter_local_notifications_windows.dll"); + final DynamicLibrary _library = + DynamicLibrary.open('flutter_local_notifications_windows.dll'); /// A pointer to the C++ handler class. late final Pointer _plugin; @@ -42,53 +49,71 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { /// The last recorded launch details, if any. /// - /// If the app is opened with a notification, this can be read with [getNotificationAppLaunchDetails]. - /// If a notification is pressed while the app is running, this will be passed to [userCallback]. + /// If the app is opened with a notification, this can be read with + /// [getNotificationAppLaunchDetails]. If a notification is pressed while the + /// app is running, this will be passed to [userCallback]. NativeLaunchDetails? _details; - /// A user-provided callback from [initialize] to run when a notification is pressed. + /// A callback from [initialize] to run when a notification is pressed. DidReceiveNotificationResponseCallback? userCallback; - /// Creates an instance of the native plugin. - FlutterLocalNotificationsWindows(); - @override Future initialize( WindowsInitializationSettings settings, { DidReceiveNotificationResponseCallback? onNotificationReceived, - }) async => using((arena) { - if (_isReady) return true; + }) async => using((Arena arena) { + if (_isReady) { + return true; + } _plugin = _bindings.createPlugin(); - // The C++ code will crash if there's an invalid GUID, so check it here first. + // The C++ code will crash if there's an invalid GUID, so check it here if (!settings.guid.isValidGuid) { - throw ArgumentError.value(settings.guid, "GUID", "Invalid GUID. Please use xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format\nYou can get one by searching GUID generators online"); + throw ArgumentError.value( + settings.guid, + 'GUID', + 'Invalid GUID. Please use xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format\n' + 'You can get one by searching GUID generators online', + ); } instance = this; userCallback = onNotificationReceived; - final appName = settings.appName.toNativeUtf8(allocator: arena); - final aumId = settings.appUserModelId.toNativeUtf8(allocator: arena); - final guid = settings.guid.toNativeUtf8(allocator: arena); - final iconPath = settings.iconPath?.toNativeUtf8(allocator: arena) ?? nullptr; - final callback = NativeCallable.listener(_globalLaunchCallback).nativeFunction; - final result = _bindings.init(_plugin, appName, aumId, guid, iconPath, callback).toBool(); + final Pointer appName = + settings.appName.toNativeUtf8(allocator: arena); + final Pointer aumId = + settings.appUserModelId.toNativeUtf8(allocator: arena); + final Pointer guid = settings.guid.toNativeUtf8(allocator: arena); + final Pointer iconPath = + settings.iconPath?.toNativeUtf8(allocator: arena) ?? nullptr; + final Pointer> callback = + NativeCallable + .listener(_globalLaunchCallback) + .nativeFunction; + final bool result = _bindings + .init(_plugin, appName, aumId, guid, iconPath, callback) + .toBool(); _isReady = result; return result; }); @override void dispose() { - if (!_isReady) return; + if (!_isReady) { + return; + } _bindings.disposePlugin(_plugin); instance = null; _isReady = false; } void _onNotificationReceived(NativeLaunchDetails details) { - if (!_isReady) return; - if (_details != null) _bindings.freeLaunchDetails(_details!); + if (!_isReady) { + return; + } else if (_details != null) { + _bindings.freeLaunchDetails(_details!); + } _details = details; - final data = details.data.toMap(); - final response = NotificationResponse( + final Map data = details.data.toMap(); + final NotificationResponse response = NotificationResponse( notificationResponseType: getResponseType(details.launchType), payload: details.payload.toDartString(), actionId: details.payload.toDartString(), @@ -99,42 +124,72 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { @override Future cancel(int id) async { - if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } _bindings.cancelNotification(_plugin, id); } @override Future cancelAll() async { - if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } _bindings.cancelAll(_plugin); } @override - Future> getActiveNotifications() async => using((arena) { - if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); - final length = arena(); - final array = _bindings.getActiveNotifications(_plugin, length); - final result = array.asActiveNotifications(length.value); - _bindings.freeDetailsArray(array); - return result; - }); + Future> getActiveNotifications() async => + using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + final Pointer length = arena(); + final Pointer array = + _bindings.getActiveNotifications(_plugin, length); + final List result = + array.asActiveNotifications(length.value); + _bindings.freeDetailsArray(array); + return result; + }); @override - Future> pendingNotificationRequests() async => using((arena) { - if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); - final length = arena(); - final array = _bindings.getPendingNotifications(_plugin, length); - final result = array.asPendingRequests(length.value); - _bindings.freeDetailsArray(array); - return result; - }); + Future> + pendingNotificationRequests() async => + using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + final Pointer length = arena(); + final Pointer array = + _bindings.getPendingNotifications(_plugin, length); + final List result = + array.asPendingRequests(length.value); + _bindings.freeDetailsArray(array); + return result; + }); @override - Future getNotificationAppLaunchDetails() async { - if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); - final details = _details; - if (details == null) return null; - final data = details.data.toMap(); + Future + getNotificationAppLaunchDetails() async { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + final NativeLaunchDetails? details = _details; + if (details == null) { + return null; + } + final Map data = details.data.toMap(); return NotificationAppLaunchDetails( details.didLaunch.toBool(), notificationResponse: NotificationResponse( @@ -147,34 +202,90 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { } @override - Future periodicallyShow(int id, String? title, String? body, RepeatInterval repeatInterval) async { - throw UnsupportedError("Windows devices cannot periodically show notifications"); + Future periodicallyShow( + int id, + String? title, + String? body, + RepeatInterval repeatInterval, + ) async { + throw UnsupportedError( + 'Windows devices cannot periodically show notifications', + ); } @override - Future periodicallyShowWithDuration(int id, String? title, String? body, Duration repeatDurationInterval) async { - throw UnsupportedError("Windows devices cannot periodically show notifications"); + Future periodicallyShowWithDuration( + int id, + String? title, + String? body, + Duration repeatDurationInterval, + ) async { + throw UnsupportedError( + 'Windows devices cannot periodically show notifications', + ); } @override - Future show(int id, String? title, String? body, {String? payload, WindowsNotificationDetails? details}) async => using((arena) { - if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); - final bindings = { + Future show( + int id, + String? title, + String? body, + {String? payload, WindowsNotificationDetails? details} + ) async => using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + final Map bindings = { if (details != null) ...details.bindings, - for (final progressBar in details?.progressBars ?? []) - ...progressBar.data, + for ( + final WindowsProgressBar progressBar + in details?.progressBars ?? [] + ) ...progressBar.data, }; - final nativeMap = bindings.toNativeMap(arena); - final xml = notificationToXml(title: title, body: body, payload: payload, details: details); - final result = _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), nativeMap).toBool(); - if (!result) throw Exception("Flutter Local Notifications (Windows) could not show notification"); + final NativeStringMap nativeMap = bindings.toNativeMap(arena); + final String xml = notificationToXml( + title: title, + body: body, + payload: payload, + details: details, + ); + final bool result = _bindings + .showNotification( + _plugin, + id, + xml.toNativeUtf8(allocator: arena), + nativeMap, + ).toBool(); + if (!result) { + throw Exception( + 'Flutter Local Notifications could not show notification', + ); + } }); @override - Future showRawXml({required int id, required String xml, Map bindings = const {}}) async => using((arena) { - if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); - final result = _bindings.showNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), bindings.toNativeMap(arena)).toBool(); - if (!result) throw ArgumentError("Flutter Local Notifications (Windows): Invalid XML"); + Future showRawXml({ + required int id, + required String xml, + Map bindings = const {}, + }) async => using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + final bool result = _bindings + .showNotification( + _plugin, + id, + xml.toNativeUtf8(allocator: arena), + bindings.toNativeMap(arena) + ).toBool(); + if (!result) { + throw ArgumentError('Flutter Local Notifications: Invalid XML'); + } }); @override @@ -185,12 +296,31 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { TZDateTime scheduledDate, WindowsNotificationDetails? details, { String? payload, - }) async => using((arena) { - if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); - if (scheduledDate.isBefore(DateTime.now())) throw ArgumentError("Flutter Local Notifications (Windows) cannot schedule notifications in the past"); - final xml = notificationToXml(title: title, body: body, payload: payload, details: details); - final secondsSinceEpoch = scheduledDate.millisecondsSinceEpoch ~/ 1000; - _bindings.scheduleNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), secondsSinceEpoch); + }) async => using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + if (scheduledDate.isBefore(DateTime.now())) { + throw ArgumentError( + 'Flutter Local Notifications cannot schedule notifications in the past', + ); + } + final String xml = notificationToXml( + title: title, + body: body, + payload: payload, + details: details, + ); + final int secondsSinceEpoch = + scheduledDate.millisecondsSinceEpoch ~/ 1000; + _bindings.scheduleNotification( + _plugin, + id, + xml.toNativeUtf8(allocator: arena), + secondsSinceEpoch, + ); }); @override @@ -199,17 +329,38 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { String xml, TZDateTime scheduledDate, WindowsNotificationDetails? details, - ) async => using((arena) { - if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); - if (scheduledDate.isBefore(DateTime.now())) throw ArgumentError("Flutter Local Notifications (Windows) cannot schedule notifications in the past"); - final secondsSinceEpoch = scheduledDate.millisecondsSinceEpoch ~/ 1000; - _bindings.scheduleNotification(_plugin, id, xml.toNativeUtf8(allocator: arena), secondsSinceEpoch); + ) async => using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + if (scheduledDate.isBefore(DateTime.now())) { + throw ArgumentError( + 'Flutter Local Notifications cannot schedule notifications in the past', + ); + } + final int secondsSinceEpoch = scheduledDate.millisecondsSinceEpoch ~/ 1000; + _bindings.scheduleNotification( + _plugin, + id, + xml.toNativeUtf8(allocator: arena), + secondsSinceEpoch, + ); }); @override - Future updateBindings({required int id, required Map bindings}) async => using((arena) { - if (!_isReady) throw StateError("Flutter Local Notifications (Windows) must be initialized before use"); - final result = _bindings.updateNotification(_plugin, id, bindings.toNativeMap(arena)); + Future updateBindings({ + required int id, + required Map bindings, + }) async => using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + final NativeUpdateResult result = _bindings + .updateNotification(_plugin, id, bindings.toNativeMap(arena)); return getUpdateResult(result); }); } diff --git a/flutter_local_notifications_windows/lib/src/plugin/stub.dart b/flutter_local_notifications_windows/lib/src/plugin/stub.dart index cb1f4637a..c34653f41 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/stub.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/stub.dart @@ -1,5 +1,5 @@ -import "../details.dart"; -import "base.dart"; +import '../details.dart'; +import 'base.dart'; /// A stub implementation for platforms that don't support FFI. class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { @@ -8,38 +8,63 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { WindowsInitializationSettings settings, { DidReceiveNotificationResponseCallback? onNotificationReceived, }) async { - throw UnsupportedError("This platform does not support Windows notifications"); + throw UnsupportedError( + 'This platform does not support Windows notifications', + ); } @override - void dispose() { } + void dispose() {} @override - Future cancel(int id) async { } + Future cancel(int id) async {} @override - Future cancelAll() async { } + Future cancelAll() async {} @override - Future> getActiveNotifications() async => []; + Future> getActiveNotifications() async => + []; @override - Future getNotificationAppLaunchDetails() async => null; + Future + getNotificationAppLaunchDetails() async => null; @override - Future> pendingNotificationRequests() async => []; + Future> + pendingNotificationRequests() async => []; @override - Future periodicallyShow(int id, String? title, String? body, RepeatInterval repeatInterval) async { } + Future periodicallyShow( + int id, + String? title, + String? body, + RepeatInterval repeatInterval, + ) async {} @override - Future periodicallyShowWithDuration(int id, String? title, String? body, Duration repeatDurationInterval) async { } + Future periodicallyShowWithDuration( + int id, + String? title, + String? body, + Duration repeatDurationInterval, + ) async {} @override - Future show(int id, String? title, String? body, {String? payload, WindowsNotificationDetails? details}) async { } + Future show( + int id, + String? title, + String? body, { + String? payload, + WindowsNotificationDetails? details, + }) async {} @override - Future showRawXml({required int id, required String xml, Map bindings = const {}}) async { } + Future showRawXml({ + required int id, + required String xml, + Map bindings = const {}, + }) async {} @override Future zonedSchedule( @@ -49,7 +74,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { TZDateTime scheduledDate, WindowsNotificationDetails? details, { String? payload, - }) async { } + }) async {} @override Future zonedScheduleRawXml( @@ -57,7 +82,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { String xml, TZDateTime scheduledDate, WindowsNotificationDetails? details, - ) async { } + ) async {} @override Future updateBindings({ diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index 29aa4a366..0c46b8c61 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: dev_dependencies: ffigen: ^13.0.0 - very_good_analysis: ^6.0.0 test: ^1.25.2 flutter: diff --git a/flutter_local_notifications_windows/test/bindings_test.dart b/flutter_local_notifications_windows/test/bindings_test.dart index 64f46a47e..71a6f7832 100644 --- a/flutter_local_notifications_windows/test/bindings_test.dart +++ b/flutter_local_notifications_windows/test/bindings_test.dart @@ -1,37 +1,58 @@ -import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; -import "package:test/test.dart"; +import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart'; +import 'package:test/test.dart'; -const settings = WindowsInitializationSettings(appName: "Test app", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); -const bindings = {"title": "Bindings title", "body": "Bindings body"}; +const WindowsInitializationSettings settings = WindowsInitializationSettings( + appName: 'Test app', + appUserModelId: 'com.test.test', + guid: 'a8c22b55-049e-422f-b30f-863694de08c8', +); -void main() => group("Bindings", () { - final plugin = FlutterLocalNotificationsWindows(); +const Map bindings = { + 'title': 'Bindings title', + 'body': 'Bindings body', +}; + +void main() => group('Bindings', () { + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); setUpAll(() => plugin.initialize(settings)); - tearDownAll(() async { await plugin.cancelAll(); plugin.dispose(); }); + tearDownAll(() async { + await plugin.cancelAll(); + plugin.dispose(); + }); - test("work in simple cases", () async { - await plugin.show(500, "{title}", "{body}"); - final result = await plugin.updateBindings(id: 500, bindings: bindings); + test('work in simple cases', () async { + await plugin.show(500, '{title}', '{body}'); + final NotificationUpdateResult result = + await plugin.updateBindings(id: 500, bindings: bindings); expect(result, NotificationUpdateResult.success); }); - test("fail when ID is not found in simple cases", () async { - await plugin.show(501, "{title}", "{body}"); - final result = await plugin.updateBindings(id: 599, bindings: bindings); + test('fail when ID is not found in simple cases', () async { + await plugin.show(501, '{title}', '{body}'); + final NotificationUpdateResult result = + await plugin.updateBindings(id: 599, bindings: bindings); expect(result, NotificationUpdateResult.notFound); }); - test("are included in show()", () async { - await plugin.show(502, "{title}", "{body}", details: const WindowsNotificationDetails(bindings: bindings)); + test('are included in show()', () async { + await plugin.show( + 502, + '{title}', + '{body}', + details: const WindowsNotificationDetails(bindings: bindings), + ); }); - test("fail when notification has been cancelled", retry: 5, () async { + test('fail when notification has been cancelled', retry: 5, () async { await Future.delayed(const Duration(milliseconds: 200)); - await plugin.show(503, "{title}", "{body}"); - final result = await plugin.updateBindings(id: 503, bindings: bindings); + await plugin.show(503, '{title}', '{body}'); + final NotificationUpdateResult result = + await plugin.updateBindings(id: 503, bindings: bindings); expect(result, NotificationUpdateResult.success); await plugin.cancelAll(); - final result2 = await plugin.updateBindings(id: 503, bindings: bindings); + final NotificationUpdateResult result2 = + await plugin.updateBindings(id: 503, bindings: bindings); expect(result2, NotificationUpdateResult.notFound); }); }); diff --git a/flutter_local_notifications_windows/test/details_test.dart b/flutter_local_notifications_windows/test/details_test.dart index 3b9819459..063ca2b05 100644 --- a/flutter_local_notifications_windows/test/details_test.dart +++ b/flutter_local_notifications_windows/test/details_test.dart @@ -1,126 +1,244 @@ -import "dart:io"; +import 'dart:io'; -import "package:test/test.dart"; -import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; +import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart'; +import 'package:test/test.dart'; -const settings = WindowsInitializationSettings(appName: "Test app", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); +const WindowsInitializationSettings settings = WindowsInitializationSettings( + appName: 'Test app', + appUserModelId: 'com.test.test', + guid: 'a8c22b55-049e-422f-b30f-863694de08c8', +); extension PluginUtils on FlutterLocalNotificationsWindows { static int id = 15; Future showDetails(WindowsNotificationDetails details) => - show(id++, "Title", "Body", details: details); + show(id++, 'Title', 'Body', details: details); void testDetails(WindowsNotificationDetails details) => expect(showDetails(details), completes); } -void main() => group("Details:", () { - final plugin = FlutterLocalNotificationsWindows(); +void main() => group('Details:', () { + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); setUpAll(() => plugin.initialize(settings)); - tearDownAll(() async { await plugin.cancelAll(); plugin.dispose(); }); + tearDownAll(() async { + await plugin.cancelAll(); + plugin.dispose(); + }); - test("No details", () async { + test('No details', () async { expect(plugin.show(100, null, null), completes); - expect(plugin.show(101, "Title", null), completes); - expect(plugin.show(102, null, "Body"), completes); - expect(plugin.show(103, "Title", "Body"), completes); - expect(plugin.show(-1, "Negative ID", "Body"), completes); + expect(plugin.show(101, 'Title', null), completes); + expect(plugin.show(102, null, 'Body'), completes); + expect(plugin.show(103, 'Title', 'Body'), completes); + expect(plugin.show(-1, 'Negative ID', 'Body'), completes); }); - test("Simple details", () async { - plugin.testDetails(const WindowsNotificationDetails()); - plugin.testDetails(const WindowsNotificationDetails(subtitle: "Subtitle")); - plugin.testDetails(const WindowsNotificationDetails(duration: WindowsNotificationDuration.long)); - plugin.testDetails(const WindowsNotificationDetails(scenario: WindowsNotificationScenario.reminder)); - plugin.testDetails(WindowsNotificationDetails(timestamp: DateTime.now())); - plugin.testDetails(const WindowsNotificationDetails(subtitle: "{message}", bindings: {"message": "Hello, Mr. Person"})); - }); + test('Simple details', () async => plugin + ..testDetails(const WindowsNotificationDetails()) + ..testDetails( + const WindowsNotificationDetails(subtitle: 'Subtitle')) + ..testDetails(const WindowsNotificationDetails( + duration: WindowsNotificationDuration.long)) + ..testDetails(const WindowsNotificationDetails( + scenario: WindowsNotificationScenario.reminder)) + ..testDetails(WindowsNotificationDetails(timestamp: DateTime.now())) + ..testDetails(const WindowsNotificationDetails( + subtitle: '{message}', + bindings: {'message': 'Hello, Mr. Person'})) + ); - test("Actions", () { - const simpleAction = WindowsAction(content: "Press me", arguments: "123"); - final complexAction = WindowsAction( - content: "content", - arguments: "args", + test('Actions', () { + const WindowsAction simpleAction = + WindowsAction(content: 'Press me', arguments: '123'); + final WindowsAction complexAction = WindowsAction( + content: 'content', + arguments: 'args', activationBehavior: WindowsNotificationBehavior.pendingUpdate, buttonStyle: WindowsButtonStyle.success, - inputId: "input-id", - tooltip: "tooltip", - image: File("test/icon.png").absolute, + inputId: 'input-id', + tooltip: 'tooltip', + image: File('test/icon.png').absolute, + ); + plugin + ..testDetails(const WindowsNotificationDetails( + actions: [simpleAction])) + ..testDetails(WindowsNotificationDetails( + actions: [complexAction])) + ..testDetails( + WindowsNotificationDetails( + actions: List.filled(5, simpleAction)) + ); + expect( + plugin.showDetails( + WindowsNotificationDetails( + actions: List.filled(6, simpleAction), + ), + ), + throwsArgumentError, ); - plugin.testDetails(const WindowsNotificationDetails(actions: [simpleAction])); - plugin.testDetails(WindowsNotificationDetails(actions: [complexAction])); - plugin.testDetails(WindowsNotificationDetails(actions: List.filled(5, simpleAction))); - expect(plugin.showDetails(WindowsNotificationDetails(actions: List.filled(6, simpleAction))), throwsArgumentError); }); - test("Audio", () { - plugin.testDetails(WindowsNotificationDetails(audio: WindowsNotificationAudio.silent())); - plugin.testDetails(WindowsNotificationDetails(audio: WindowsNotificationAudio.preset(sound: WindowsNotificationSound.call10))); - }); + test('Audio', () => plugin + ..testDetails(WindowsNotificationDetails( + audio: WindowsNotificationAudio.silent())) + ..testDetails(WindowsNotificationDetails( + audio: WindowsNotificationAudio.preset( + sound: WindowsNotificationSound.call10))) + ); - test("Rows", () { - const emptyColumn = WindowsColumn([]); - final image = WindowsImage.file(File("test/icon.png").absolute, altText: "an icon"); - const text = WindowsNotificationText(text: "Text"); - final simpleColumn = WindowsColumn([image, text]); - final bigRow = WindowsRow(List.filled(5, simpleColumn)); - plugin.testDetails(const WindowsNotificationDetails()); - plugin.testDetails(const WindowsNotificationDetails(groups: [WindowsRow([])])); - plugin.testDetails(const WindowsNotificationDetails(groups: [WindowsRow([emptyColumn])])); - plugin.testDetails(WindowsNotificationDetails(groups: [WindowsRow([simpleColumn])])); - plugin.testDetails(WindowsNotificationDetails(groups: [bigRow])); - plugin.testDetails(WindowsNotificationDetails(groups: List.filled(5, bigRow))); + test('Rows', () { + const WindowsColumn emptyColumn = + WindowsColumn([]); + final WindowsImage image = WindowsImage.file( + File('test/icon.png').absolute, + altText: 'an icon'); + const WindowsNotificationText text = + WindowsNotificationText(text: 'Text'); + final WindowsColumn simpleColumn = + WindowsColumn([image, text]); + final WindowsRow bigRow = WindowsRow( + List.filled(5, simpleColumn), + ); + plugin + + ..testDetails(const WindowsNotificationDetails()) + ..testDetails(const WindowsNotificationDetails( + groups: [WindowsRow([])])) + ..testDetails(const WindowsNotificationDetails(groups: [ + WindowsRow([emptyColumn]) + ])) + ..testDetails(WindowsNotificationDetails(groups: [ + WindowsRow([simpleColumn]) + ])) + ..testDetails( + WindowsNotificationDetails(groups: [bigRow])) + ..testDetails( + WindowsNotificationDetails(groups: List.filled(5, bigRow))); }); - test("Header", () async { - const header = WindowsHeader(id: "header1", title: "Header 1", arguments: "args1", activation: WindowsHeaderActivation.foreground); - plugin.testDetails(const WindowsNotificationDetails(header: header)); - plugin.testDetails(const WindowsNotificationDetails(header: header)); + test('Header', () async { + const WindowsHeader header = WindowsHeader( + id: 'header1', + title: 'Header 1', + arguments: 'args1', + activation: WindowsHeaderActivation.foreground, + ); + plugin + ..testDetails(const WindowsNotificationDetails(header: header)) + ..testDetails(const WindowsNotificationDetails(header: header)); }); - test("Images", () async { - final simpleImage = WindowsImage.file(File("test/icon.png").absolute, altText: "an icon"); - final complexImage = WindowsImage.file( - File("test/icon.png").absolute, - altText: "an icon", + test('Images', () async { + final WindowsImage simpleImage = WindowsImage.file( + File('test/icon.png').absolute, + altText: 'an icon', + ); + final WindowsImage complexImage = WindowsImage.file( + File('test/icon.png').absolute, + altText: 'an icon', addQueryParams: true, crop: WindowsImageCrop.circle, placement: WindowsImagePlacement.appLogoOverride, ); - plugin.testDetails(WindowsNotificationDetails(images: [simpleImage])); - plugin.testDetails(WindowsNotificationDetails(images: [simpleImage, complexImage])); - plugin.testDetails(WindowsNotificationDetails(images: List.filled(6, simpleImage))); + plugin + ..testDetails( + WindowsNotificationDetails(images: [simpleImage])) + ..testDetails(WindowsNotificationDetails( + images: [simpleImage, complexImage])) + ..testDetails( + WindowsNotificationDetails( + images: List.filled(6, simpleImage), + ), + ); }); - test("Inputs", () async { - const textInput = WindowsTextInput(id: "input", placeHolderContent: "Text hint", title: "Text title"); - const selection = WindowsSelectionInput(id: "input", items: [ - WindowsSelection(id: "item1", content: "Item 1"), - WindowsSelection(id: "item2", content: "Item 2"), - WindowsSelection(id: "item3", content: "Item 3"), - ],); - const action = WindowsAction(content: "Submit", arguments: "submit", inputId: "input"); - plugin.testDetails(const WindowsNotificationDetails(inputs: [textInput])); - plugin.testDetails(const WindowsNotificationDetails(inputs: [selection])); - plugin.testDetails(WindowsNotificationDetails(inputs: List.filled(5, textInput))); - plugin.testDetails(const WindowsNotificationDetails(inputs: [textInput], actions: [action])); - expect(plugin.showDetails(WindowsNotificationDetails(inputs: List.filled(6, textInput))), throwsArgumentError); - plugin.testDetails(const WindowsNotificationDetails(inputs: [selection, textInput], actions: [action])); + test('Inputs', () async { + const WindowsTextInput textInput = WindowsTextInput( + id: 'input', + placeHolderContent: 'Text hint', + title: 'Text title', + ); + const WindowsSelectionInput selection = WindowsSelectionInput( + id: 'input', + items: [ + WindowsSelection(id: 'item1', content: 'Item 1'), + WindowsSelection(id: 'item2', content: 'Item 2'), + WindowsSelection(id: 'item3', content: 'Item 3'), + ], + ); + const WindowsAction action = WindowsAction( + content: 'Submit', + arguments: 'submit', + inputId: 'input', + ); + plugin + ..testDetails(const WindowsNotificationDetails( + inputs: [textInput])) + ..testDetails(const WindowsNotificationDetails( + inputs: [selection])) + ..testDetails( + WindowsNotificationDetails( + inputs: List.filled(5, textInput), + ), + ) + ..testDetails(const WindowsNotificationDetails( + inputs: [textInput], + actions: [action])) + ..testDetails(const WindowsNotificationDetails( + inputs: [selection, textInput], + actions: [action])); + expect( + plugin.showDetails( + WindowsNotificationDetails( + inputs: List.filled(6, textInput), + ), + ), + throwsArgumentError, + ); }); - test("Progress", retry: 5, () async { - final simple = WindowsProgressBar(id: "simple", status: "Testing...", value: 0.25); - final complex = WindowsProgressBar(id: "complex", status: "Testing...", value: 0.75, label: "Progress label", title: "Progress title"); - final dynamic = WindowsProgressBar(id: "dynamic", status: "Testing...", value: 0); - plugin.testDetails(WindowsNotificationDetails(progressBars: [simple])); - plugin.testDetails(WindowsNotificationDetails(progressBars: [complex])); - plugin.testDetails(WindowsNotificationDetails(progressBars: [simple, complex])); - plugin.testDetails(WindowsNotificationDetails(progressBars: List.filled(6, simple))); - await plugin.show(201, null, null, details: WindowsNotificationDetails(progressBars: [dynamic])); - for (var i = 0.0; i <= 1.5; i += 0.05) { + test('Progress', retry: 5, () async { + final WindowsProgressBar simple = WindowsProgressBar( + id: 'simple', + status: 'Testing...', + value: 0.25, + ); + final WindowsProgressBar complex = WindowsProgressBar( + id: 'complex', + status: 'Testing...', + value: 0.75, + label: 'Progress label', + title: 'Progress title', + ); + final WindowsProgressBar dynamic = WindowsProgressBar( + id: 'dynamic', + status: 'Testing...', + value: 0, + ); + plugin + ..testDetails(WindowsNotificationDetails( + progressBars: [simple])) + ..testDetails(WindowsNotificationDetails( + progressBars: [complex])) + ..testDetails(WindowsNotificationDetails( + progressBars: [simple, complex])) + ..testDetails( + WindowsNotificationDetails( + progressBars: List.filled(6, simple), + ), + ); + await plugin.show(201, null, null, + details: WindowsNotificationDetails( + progressBars: [dynamic], + ), + ); + for (double i = 0; i <= 1.5; i += 0.05) { dynamic.value = i; - final result = await plugin.updateProgressBar(notificationId: 201, progressBar: dynamic); + final NotificationUpdateResult result = await plugin + .updateProgressBar(notificationId: 201, progressBar: dynamic); expect(result, NotificationUpdateResult.success); await Future.delayed(const Duration(milliseconds: 10)); } diff --git a/flutter_local_notifications_windows/test/plugin_test.dart b/flutter_local_notifications_windows/test/plugin_test.dart index 399edbbe8..7e9f6a15d 100644 --- a/flutter_local_notifications_windows/test/plugin_test.dart +++ b/flutter_local_notifications_windows/test/plugin_test.dart @@ -1,71 +1,93 @@ -import "package:test/test.dart"; -import "package:timezone/standalone.dart"; -import "package:timezone/data/latest_all.dart"; +import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; +import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart'; +import 'package:test/test.dart'; +import 'package:timezone/data/latest_all.dart'; +import 'package:timezone/standalone.dart'; +const WindowsInitializationSettings goodSettings = + WindowsInitializationSettings( + appName: 'test', + appUserModelId: 'com.test.test', + guid: 'a8c22b55-049e-422f-b30f-863694de08c8'); +const WindowsInitializationSettings badSettings = WindowsInitializationSettings( + appName: 'test', appUserModelId: 'com.test.test', guid: '123'); -import "package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart"; -import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; +void main() => group('Plugin', () { + setUpAll(initializeTimeZones); -const goodSettings = WindowsInitializationSettings(appName: "test", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); -const badSettings = WindowsInitializationSettings(appName: "test", appUserModelId: "com.test.test", guid: "123"); + test('initializes safely', () async { + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); + final bool result = await plugin.initialize(goodSettings); + expect(result, isTrue); + plugin.dispose(); + }); -void main() => group("Plugin", () { - setUpAll(initializeTimeZones); + test('catches bad GUIDs', () async { + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); + expect(plugin.initialize(badSettings), throwsArgumentError); + plugin.dispose(); + }); - test("initializes safely", () async { - final plugin = FlutterLocalNotificationsWindows(); - final result = await plugin.initialize(goodSettings); - expect(result, isTrue); - plugin.dispose(); - }); + test('cannot be used before initializing', () async { + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); + final WindowsProgressBar progress = + WindowsProgressBar(id: 'progress', status: 'Testing', value: 0); + final TZDateTime now = TZDateTime.local(2024, 7, 18); + expect(plugin.cancel(0), throwsStateError); + expect(plugin.cancelAll(), throwsStateError); + expect(plugin.getActiveNotifications(), throwsStateError); + expect(plugin.getNotificationAppLaunchDetails(), throwsStateError); + expect(plugin.pendingNotificationRequests(), throwsStateError); + expect(plugin.show(0, 'Title', 'Body'), throwsStateError); + expect(plugin.showRawXml(id: 0, xml: ''), throwsStateError); + expect(plugin.updateBindings(id: 0, bindings: {}), + throwsStateError); + expect( + plugin.updateProgressBar(progressBar: progress, notificationId: 0), + throwsStateError); + expect( + plugin.zonedSchedule(0, null, null, now, null), throwsStateError); + plugin.dispose(); + }); - test("catches bad GUIDs", () async { - final plugin = FlutterLocalNotificationsWindows(); - expect(plugin.initialize(badSettings), throwsArgumentError); - plugin.dispose(); - }); + test('cannot be used after disposed', () async { + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); + final WindowsProgressBar progress = + WindowsProgressBar(id: 'progress', status: 'Testing', value: 0); + final TZDateTime now = TZDateTime.local(2024, 7, 18); + await plugin.initialize(goodSettings); + plugin.dispose(); + expect(plugin.cancel(0), throwsStateError); + expect(plugin.cancelAll(), throwsStateError); + expect(plugin.getActiveNotifications(), throwsStateError); + expect(plugin.getNotificationAppLaunchDetails(), throwsStateError); + expect(plugin.pendingNotificationRequests(), throwsStateError); + expect(plugin.show(0, 'Title', 'Body'), throwsStateError); + expect(plugin.showRawXml(id: 0, xml: ''), throwsStateError); + expect(plugin.updateBindings(id: 0, bindings: {}), + throwsStateError); + expect( + plugin.updateProgressBar(progressBar: progress, notificationId: 0), + throwsStateError); + expect( + plugin.zonedSchedule(0, null, null, now, null), throwsStateError); + plugin.dispose(); + }); - test("cannot be used before initializing", () async { - final plugin = FlutterLocalNotificationsWindows(); - final progress = WindowsProgressBar(id: "progress", status: "Testing", value: 0); - final now = TZDateTime.local(2024, 7, 18); - expect(plugin.cancel(0), throwsStateError); - expect(plugin.cancelAll(), throwsStateError); - expect(plugin.getActiveNotifications(), throwsStateError); - expect(plugin.getNotificationAppLaunchDetails(), throwsStateError); - expect(plugin.pendingNotificationRequests(), throwsStateError); - expect(plugin.show(0, "Title", "Body"), throwsStateError); - expect(plugin.showRawXml(id: 0, xml: ""), throwsStateError); - expect(plugin.updateBindings(id: 0, bindings: {}), throwsStateError); - expect(plugin.updateProgressBar(progressBar: progress, notificationId: 0), throwsStateError); - expect(plugin.zonedSchedule(0, null, null, now, null), throwsStateError); - plugin.dispose(); - }); - - test("cannot be used after disposed", () async { - final plugin = FlutterLocalNotificationsWindows(); - final progress = WindowsProgressBar(id: "progress", status: "Testing", value: 0); - final now = TZDateTime.local(2024, 7, 18); - await plugin.initialize(goodSettings); - plugin.dispose(); - expect(plugin.cancel(0), throwsStateError); - expect(plugin.cancelAll(), throwsStateError); - expect(plugin.getActiveNotifications(), throwsStateError); - expect(plugin.getNotificationAppLaunchDetails(), throwsStateError); - expect(plugin.pendingNotificationRequests(), throwsStateError); - expect(plugin.show(0, "Title", "Body"), throwsStateError); - expect(plugin.showRawXml(id: 0, xml: ""), throwsStateError); - expect(plugin.updateBindings(id: 0, bindings: {}), throwsStateError); - expect(plugin.updateProgressBar(progressBar: progress, notificationId: 0), throwsStateError); - expect(plugin.zonedSchedule(0, null, null, now, null), throwsStateError); - plugin.dispose(); - }); - - test("does not support repeating notifications", () async { - final plugin = FlutterLocalNotificationsWindows(); - await plugin.initialize(goodSettings); - expect(plugin.periodicallyShow(0, null, null, RepeatInterval.everyMinute), throwsUnsupportedError); - expect(plugin.periodicallyShowWithDuration(0, null, null, Duration.zero), throwsUnsupportedError); - plugin.dispose(); - }); -}); + test('does not support repeating notifications', () async { + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); + await plugin.initialize(goodSettings); + expect( + plugin.periodicallyShow(0, null, null, RepeatInterval.everyMinute), + throwsUnsupportedError); + expect( + plugin.periodicallyShowWithDuration(0, null, null, Duration.zero), + throwsUnsupportedError); + plugin.dispose(); + }); + }); diff --git a/flutter_local_notifications_windows/test/scheduled_test.dart b/flutter_local_notifications_windows/test/scheduled_test.dart index ff45aa477..fa33e04bd 100644 --- a/flutter_local_notifications_windows/test/scheduled_test.dart +++ b/flutter_local_notifications_windows/test/scheduled_test.dart @@ -1,38 +1,48 @@ -import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; -import "package:test/test.dart"; -import "package:timezone/standalone.dart"; -import "package:timezone/data/latest_all.dart"; +import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart'; +import 'package:test/test.dart'; +import 'package:timezone/data/latest_all.dart'; +import 'package:timezone/standalone.dart'; -const settings = WindowsInitializationSettings(appName: "Test app", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); +const WindowsInitializationSettings settings = WindowsInitializationSettings( + appName: 'Test app', + appUserModelId: 'com.test.test', + guid: 'a8c22b55-049e-422f-b30f-863694de08c8'); -void main() => group("Schedules", () { - final plugin = FlutterLocalNotificationsWindows(); - setUpAll(initializeTimeZones); - setUpAll(() => plugin.initialize(settings)); - tearDownAll(() async { await plugin.cancelAll(); plugin.dispose(); }); +void main() => group('Schedules', () { + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); + setUpAll(initializeTimeZones); + setUpAll(() => plugin.initialize(settings)); + tearDownAll(() async { + await plugin.cancelAll(); + plugin.dispose(); + }); - Future countPending() async => (await plugin.pendingNotificationRequests()).length; - late final location = getLocation("US/Eastern"); + Future countPending() async => + (await plugin.pendingNotificationRequests()).length; + late final Location location = getLocation('US/Eastern'); - test("work with basic times", () async { - await plugin.cancelAll(); - expect(await countPending(), 0); - final now = TZDateTime.now(location); - final later = now.add(const Duration(days: 1)); - expect(plugin.zonedSchedule(300, null, null, later, null), completes); - expect(await countPending(), 1); - expect(plugin.zonedSchedule(301, null, null, later, null), completes); - expect(await countPending(), 2); - expect(plugin.zonedSchedule(302, null, null, later, null), completes); - expect(await countPending(), 3); - }); + test('work with basic times', () async { + await plugin.cancelAll(); + expect(await countPending(), 0); + final TZDateTime now = TZDateTime.now(location); + final TZDateTime later = now.add(const Duration(days: 1)); + expect(plugin.zonedSchedule(300, null, null, later, null), completes); + expect(await countPending(), 1); + expect(plugin.zonedSchedule(301, null, null, later, null), completes); + expect(await countPending(), 2); + expect(plugin.zonedSchedule(302, null, null, later, null), completes); + expect(await countPending(), 3); + }); - test("do not work with earlier time", () async { - final now = TZDateTime.now(location); - final earlier = now.subtract(const Duration(days: 1)); - await plugin.cancelAll(); - expect(await countPending(), 0); - expect(plugin.zonedSchedule(302, null, null, now, null), throwsArgumentError); - expect(plugin.zonedSchedule(302, null, null, earlier, null), throwsArgumentError); - }); -}); + test('do not work with earlier time', () async { + final TZDateTime now = TZDateTime.now(location); + final TZDateTime earlier = now.subtract(const Duration(days: 1)); + await plugin.cancelAll(); + expect(await countPending(), 0); + expect(plugin.zonedSchedule(302, null, null, now, null), + throwsArgumentError); + expect(plugin.zonedSchedule(302, null, null, earlier, null), + throwsArgumentError); + }); + }); diff --git a/flutter_local_notifications_windows/test/xml_test.dart b/flutter_local_notifications_windows/test/xml_test.dart index ca921cb7a..3d1c6a569 100644 --- a/flutter_local_notifications_windows/test/xml_test.dart +++ b/flutter_local_notifications_windows/test/xml_test.dart @@ -1,12 +1,15 @@ -import "package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart"; -import "package:test/test.dart"; +import 'package:flutter_local_notifications_windows/flutter_local_notifications_windows.dart'; +import 'package:test/test.dart'; -const settings = WindowsInitializationSettings(appName: "test", appUserModelId: "com.test.test", guid: "a8c22b55-049e-422f-b30f-863694de08c8"); -const emptyXml = ""; -const invalidXml = "Blah blah blah"; -const notWindowsXml = "Hi"; -const unmatchedXml = "Hi"; -const validXml = """ +const WindowsInitializationSettings settings = WindowsInitializationSettings( + appName: 'test', + appUserModelId: 'com.test.test', + guid: 'a8c22b55-049e-422f-b30f-863694de08c8'); +const String emptyXml = ''; +const String invalidXml = 'Blah blah blah'; +const String notWindowsXml = 'Hi'; +const String unmatchedXml = 'Hi'; +const String validXml = ''' @@ -17,9 +20,9 @@ const validXml = """ -"""; +'''; -const complexXml = """ +const String complexXml = ''' @@ -51,19 +54,25 @@ const complexXml = """ -"""; +'''; -void main() => group("XML", () { - final plugin = FlutterLocalNotificationsWindows(); - setUpAll(() => plugin.initialize(settings)); - tearDownAll(() async { await plugin.cancelAll(); plugin.dispose(); }); +void main() => group('XML', () { + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); + setUpAll(() => plugin.initialize(settings)); + tearDownAll(() async { + await plugin.cancelAll(); + plugin.dispose(); + }); - test("catches invalid XML", () async { - expect(plugin.showRawXml(id: 0, xml: emptyXml), throwsArgumentError); - expect(plugin.showRawXml(id: 1, xml: invalidXml), throwsArgumentError); - expect(plugin.showRawXml(id: 2, xml: notWindowsXml), throwsArgumentError); - expect(plugin.showRawXml(id: 3, xml: unmatchedXml), throwsArgumentError); - expect(plugin.showRawXml(id: 4, xml: validXml), completes); - expect(plugin.showRawXml(id: 5, xml: complexXml), completes); - }); -}); + test('catches invalid XML', () async { + expect(plugin.showRawXml(id: 0, xml: emptyXml), throwsArgumentError); + expect(plugin.showRawXml(id: 1, xml: invalidXml), throwsArgumentError); + expect( + plugin.showRawXml(id: 2, xml: notWindowsXml), throwsArgumentError); + expect( + plugin.showRawXml(id: 3, xml: unmatchedXml), throwsArgumentError); + expect(plugin.showRawXml(id: 4, xml: validXml), completes); + expect(plugin.showRawXml(id: 5, xml: complexXml), completes); + }); + }); From e0be52cd603bd12c0618bab4cb09f545bdc3aa36 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 20:46:26 -0400 Subject: [PATCH 078/112] Changed all C int-bools to plain bools --- .../ffigen.yaml | 2 ++ .../lib/src/ffi/bindings.dart | 22 +++++++++---------- .../lib/src/ffi/utils.dart | 8 ------- .../lib/src/plugin/ffi.dart | 9 ++++---- .../src/ffi_api.h | 10 +++++---- 5 files changed, 23 insertions(+), 28 deletions(-) diff --git a/flutter_local_notifications_windows/ffigen.yaml b/flutter_local_notifications_windows/ffigen.yaml index 82657fdd1..aad4b5e0c 100644 --- a/flutter_local_notifications_windows/ffigen.yaml +++ b/flutter_local_notifications_windows/ffigen.yaml @@ -6,6 +6,8 @@ description: | Regenerate bindings with `dart run ffigen --config ffigen.yaml`. output: 'lib/src/ffi/bindings.dart' +silence-enum-warning: true + headers: entry-points: - 'src/ffi_api.h' diff --git a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart index d5b9634ee..67f90f5ae 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/bindings.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/bindings.dart @@ -55,7 +55,7 @@ class NotificationsPluginBindings { _disposePluginPtr.asFunction)>(); /// Initializes the plugin and registers the callback to be run when a notification is pressed. - int init( + bool init( ffi.Pointer plugin, ffi.Pointer appName, ffi.Pointer aumId, @@ -75,7 +75,7 @@ class NotificationsPluginBindings { late final _initPtr = _lookup< ffi.NativeFunction< - ffi.Int Function( + ffi.Bool Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, @@ -83,7 +83,7 @@ class NotificationsPluginBindings { ffi.Pointer, NativeNotificationCallback)>>('init'); late final _init = _initPtr.asFunction< - int Function( + bool Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, @@ -92,7 +92,7 @@ class NotificationsPluginBindings { NativeNotificationCallback)>(); /// Shows the XML as a notification with the given ID. See [updateNotification] for details on bindings. - int showNotification( + bool showNotification( ffi.Pointer plugin, int id, ffi.Pointer xml, @@ -108,14 +108,14 @@ class NotificationsPluginBindings { late final _showNotificationPtr = _lookup< ffi.NativeFunction< - ffi.Int Function(ffi.Pointer, ffi.Int, + ffi.Bool Function(ffi.Pointer, ffi.Int, ffi.Pointer, NativeStringMap)>>('showNotification'); late final _showNotification = _showNotificationPtr.asFunction< - int Function(ffi.Pointer, int, ffi.Pointer, + bool Function(ffi.Pointer, int, ffi.Pointer, NativeStringMap)>(); /// Schedules the notification to be shown at the given time (as a [time_t]). - int scheduleNotification( + bool scheduleNotification( ffi.Pointer plugin, int id, ffi.Pointer xml, @@ -131,10 +131,10 @@ class NotificationsPluginBindings { late final _scheduleNotificationPtr = _lookup< ffi.NativeFunction< - ffi.Int Function(ffi.Pointer, ffi.Int, + ffi.Bool Function(ffi.Pointer, ffi.Int, ffi.Pointer, ffi.Int)>>('scheduleNotification'); late final _scheduleNotification = _scheduleNotificationPtr.asFunction< - int Function( + bool Function( ffi.Pointer, int, ffi.Pointer, int)>(); /// Updates a notification with the provided bindings after it's been shown. @@ -312,8 +312,8 @@ enum NativeLaunchType { /// Details about how the app was launched. final class NativeLaunchDetails extends ffi.Struct { /// Whether the app was launched by a notification - @ffi.Int() - external int didLaunch; + @ffi.Bool() + external bool didLaunch; /// What part of the notification launched the app. @ffi.UnsignedInt() diff --git a/flutter_local_notifications_windows/lib/src/ffi/utils.dart b/flutter_local_notifications_windows/lib/src/ffi/utils.dart index 1287103e0..f95ffe114 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/utils.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/utils.dart @@ -17,14 +17,6 @@ extension NativeStringMapUtils on NativeStringMap { }; } -/// Helpful methods on integers. -extension IntUtils on int { - /// Converts this integer into a boolean. - /// - /// Useful for return types of C functions. - bool toBool() => this == 1; -} - /// Gets the [NotificationResponseType] from a [NativeLaunchType]. NotificationResponseType getResponseType(int launchType) { switch (NativeLaunchType.fromValue(launchType)) { diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index 40b8d7038..7ace3361b 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -89,8 +89,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { .listener(_globalLaunchCallback) .nativeFunction; final bool result = _bindings - .init(_plugin, appName, aumId, guid, iconPath, callback) - .toBool(); + .init(_plugin, appName, aumId, guid, iconPath, callback); _isReady = result; return result; }); @@ -191,7 +190,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { } final Map data = details.data.toMap(); return NotificationAppLaunchDetails( - details.didLaunch.toBool(), + details.didLaunch, notificationResponse: NotificationResponse( notificationResponseType: getResponseType(details.launchType), payload: details.payload.toDartString(), @@ -257,7 +256,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { id, xml.toNativeUtf8(allocator: arena), nativeMap, - ).toBool(); + ); if (!result) { throw Exception( 'Flutter Local Notifications could not show notification', @@ -282,7 +281,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { id, xml.toNativeUtf8(allocator: arena), bindings.toNativeMap(arena) - ).toBool(); + ); if (!result) { throw ArgumentError('Flutter Local Notifications: Invalid XML'); } diff --git a/flutter_local_notifications_windows/src/ffi_api.h b/flutter_local_notifications_windows/src/ffi_api.h index b619aa58d..08ae0a418 100644 --- a/flutter_local_notifications_windows/src/ffi_api.h +++ b/flutter_local_notifications_windows/src/ffi_api.h @@ -19,6 +19,8 @@ extern "C" { #endif +#include + /// A fake type to represent the C++ class that will own the Windows API handles. typedef struct NativePlugin NativePlugin; @@ -48,7 +50,7 @@ typedef enum NativeLaunchType { /// Details about how the app was launched. typedef struct NativeLaunchDetails { /// Whether the app was launched by a notification - int didLaunch; + bool didLaunch; /// What part of the notification launched the app. NativeLaunchType launchType; /// The payload sent to the app by the notification. Usually the action that was pressed. @@ -76,13 +78,13 @@ FFI_PLUGIN_EXPORT NativePlugin* createPlugin(); FFI_PLUGIN_EXPORT void disposePlugin(NativePlugin* ptr); /// Initializes the plugin and registers the callback to be run when a notification is pressed. -FFI_PLUGIN_EXPORT int init(NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, NativeNotificationCallback callback); +FFI_PLUGIN_EXPORT bool init(NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, NativeNotificationCallback callback); /// Shows the XML as a notification with the given ID. See [updateNotification] for details on bindings. -FFI_PLUGIN_EXPORT int showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bindings); +FFI_PLUGIN_EXPORT bool showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bindings); /// Schedules the notification to be shown at the given time (as a [time_t]). -FFI_PLUGIN_EXPORT int scheduleNotification(NativePlugin* plugin, int id, char* xml, int time); +FFI_PLUGIN_EXPORT bool scheduleNotification(NativePlugin* plugin, int id, char* xml, int time); /// Updates a notification with the provided bindings after it's been shown. /// From 58556127fc19c146e0af03831dc510d2f6f8b63a Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 19 Aug 2024 20:59:18 -0400 Subject: [PATCH 079/112] Changed C++ side to use bools as well --- flutter_local_notifications_windows/src/ffi_api.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter_local_notifications_windows/src/ffi_api.cpp b/flutter_local_notifications_windows/src/ffi_api.cpp index c1491f258..6fa8cfbc1 100644 --- a/flutter_local_notifications_windows/src/ffi_api.cpp +++ b/flutter_local_notifications_windows/src/ffi_api.cpp @@ -16,7 +16,7 @@ void disposePlugin(NativePlugin* plugin) { delete plugin; } -int init(NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, NativeNotificationCallback callback) { +bool init(NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, NativeNotificationCallback callback) { string icon; if (iconPath != nullptr) icon = string(iconPath); const auto didRegister = plugin->registerApp(aumId, appName, guid, icon, callback); @@ -33,7 +33,7 @@ int init(NativePlugin* plugin, char* appName, char* aumId, char* guid, char* ico return true; } -int showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bindings) { +bool showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bindings) { if (!plugin->isReady) return false; XmlDocument doc; try { @@ -49,7 +49,7 @@ int showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bi return true; } -int scheduleNotification(NativePlugin* plugin, int id, char* xml, int time) { +bool scheduleNotification(NativePlugin* plugin, int id, char* xml, int time) { if (!plugin->isReady) return false; XmlDocument doc; try { doc.LoadXml(winrt::to_hstring(xml)); } From 12b67053d76bbd883852302f94c3ac9ae72e9390 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 20 Aug 2024 00:42:28 -0400 Subject: [PATCH 080/112] Experimental multithreading support --- .../bin/crash.dart | 26 +++++++++++++----- .../flutter_local_notifications_windows.dll | Bin 368128 -> 363520 bytes .../lib/src/ffi/bindings.dart | 12 ++++++++ .../lib/src/plugin/base.dart | 9 ++++++ .../lib/src/plugin/ffi.dart | 3 ++ .../lib/src/plugin/stub.dart | 3 ++ .../src/ffi_api.cpp | 4 +++ .../src/ffi_api.h | 5 ++++ .../test/bindings_test.dart | 1 + .../test/details_test.dart | 1 + .../test/plugin_test.dart | 2 ++ .../test/scheduled_test.dart | 1 + .../test/xml_test.dart | 2 ++ 13 files changed, 62 insertions(+), 7 deletions(-) diff --git a/flutter_local_notifications_windows/bin/crash.dart b/flutter_local_notifications_windows/bin/crash.dart index b9ada9c60..d8c5d40a5 100644 --- a/flutter_local_notifications_windows/bin/crash.dart +++ b/flutter_local_notifications_windows/bin/crash.dart @@ -1,15 +1,18 @@ -// This file demonstrates how the WinRT APIs are _not_ thread safe. -// -// If you debug this code into the C++, you'll see that the crash happens when -// declaring a local variable. A quick google shows that dynamic libraries are -// only loaded *once* into the Dart VM. This leads me to believe that it is an -// issue with sharing address spaces, and the two local variables exist at the -// same time, causing the crash. +// This file demonstrates how the plugin is _not_ thread safe. // // This crash can happen when running `dart test -j 1`, which would otherwise // fix other concurrency issues with the tests. This crash is not significant // for users as it depends on having two plugins instantiated at the same time, // which is not recommended, but I left it here as a demonstration if needed. +// +// The experimental function `enableMultithreading()` can fix the issues +// demonstrated by this file, but when testing with `dart test -j 1`, a crash +// occurs as `XmlDocument doc;`, a seemingly harmless statement. I have not +// been able to deduce the cause, and `enableMultithreading()` does not fix it. +// If we can figure that out, tests can be run with `-j 1` and race conditions +// would be eliminated from the tests. + +// ignore_for_file: avoid_print import 'dart:isolate'; @@ -23,13 +26,19 @@ const WindowsInitializationSettings settings = WindowsInitializationSettings( ); void main() async { + print('Starting tests'); await Isolate.spawn(bindingsTest, null); await Isolate.spawn(scheduledTest, null); + // This is the critical line. Removing this causes crashes in the Windows SDK + FlutterLocalNotificationsWindows().enableMultithreading(); + await Future.delayed(const Duration(seconds: 5)); + print('Done. Scheduled and binding tests should have completed'); } Future scheduledTest(_) async { + print('Starting scheduled test'); await Future.delayed(const Duration(seconds: 4)); final FlutterLocalNotificationsWindows plugin = FlutterLocalNotificationsWindows(); @@ -41,9 +50,11 @@ Future scheduledTest(_) async { await plugin.zonedSchedule(300, null, null, later, null); await plugin.zonedSchedule(301, null, null, later, null); await plugin.zonedSchedule(302, null, null, later, null); + print('Scheduled test complete'); } Future bindingsTest(_) async { + print('Starting bindings test'); final Map bindings = { 'title': 'Bindings title', 'body': 'Bindings body' @@ -56,4 +67,5 @@ Future bindingsTest(_) async { await Future.delayed(const Duration(milliseconds: 100)); await plugin.updateBindings(id: 503, bindings: bindings); await plugin.updateBindings(id: 503, bindings: bindings); + print('Bindings test complete'); } diff --git a/flutter_local_notifications_windows/flutter_local_notifications_windows.dll b/flutter_local_notifications_windows/flutter_local_notifications_windows.dll index 6dc60d5ff108966d543c173737b969113e58b939..73cad21c9d69f53354f91858b42092c958bf1142 100644 GIT binary patch literal 363520 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

s6)k=Meth&+CS7rQzcVKlRhP;V;tgJHG+^y!Ca% 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%0A(Ofj)k!++_b~p5&(Mw*VMiZfer}+w#2;RRHc4z@ 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 + + /// EXPERIMENTAL: Enables multithreading for this application. + /// + /// NOTE: This is only to make tests more stable and is not intended to be used in applications. + void enableMultithreading() { + return _enableMultithreading(); + } + + late final _enableMultithreadingPtr = + _lookup>('enableMultithreading'); + late final _enableMultithreading = + _enableMultithreadingPtr.asFunction(); } final class NativePlugin extends ffi.Opaque {} diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart index 8d4313c17..918eb7dc5 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/base.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -1,4 +1,5 @@ import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; +import 'package:meta/meta.dart'; import 'package:timezone/timezone.dart'; import '../details.dart'; @@ -81,4 +82,12 @@ abstract class WindowsNotificationsBase required int id, required Map bindings, }); + + /// EXPERIMENTAL: Enables multithreading + /// + /// NOTE: This is only here to make tests more stable. This has not been + /// tested in an application as it conflicts with Flutter's preferred + /// configuration for Windows APIs. + @visibleForTesting + void enableMultithreading(); } diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index 7ace3361b..31f32b95e 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -362,4 +362,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { .updateNotification(_plugin, id, bindings.toNativeMap(arena)); return getUpdateResult(result); }); + + @override + void enableMultithreading() => _bindings.enableMultithreading(); } diff --git a/flutter_local_notifications_windows/lib/src/plugin/stub.dart b/flutter_local_notifications_windows/lib/src/plugin/stub.dart index c34653f41..96963f556 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/stub.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/stub.dart @@ -89,4 +89,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { required int id, required Map bindings, }) async => NotificationUpdateResult.success; + + @override + void enableMultithreading() { } } diff --git a/flutter_local_notifications_windows/src/ffi_api.cpp b/flutter_local_notifications_windows/src/ffi_api.cpp index 6fa8cfbc1..9d4f33dc2 100644 --- a/flutter_local_notifications_windows/src/ffi_api.cpp +++ b/flutter_local_notifications_windows/src/ffi_api.cpp @@ -137,3 +137,7 @@ void freeLaunchDetails(NativeLaunchDetails details) { } if (details.data.entries != nullptr) delete[] details.data.entries; } + +void enableMultithreading() { + CoInitializeEx(nullptr, COINIT_MULTITHREADED); +} diff --git a/flutter_local_notifications_windows/src/ffi_api.h b/flutter_local_notifications_windows/src/ffi_api.h index 08ae0a418..2bb427d66 100644 --- a/flutter_local_notifications_windows/src/ffi_api.h +++ b/flutter_local_notifications_windows/src/ffi_api.h @@ -116,6 +116,11 @@ FFI_PLUGIN_EXPORT void freeDetailsArray(NativeNotificationDetails* ptr); /// Releases the memory associated with a [NativeLaunchDetails]. FFI_PLUGIN_EXPORT void freeLaunchDetails(NativeLaunchDetails details); +/// EXPERIMENTAL: Enables multithreading for this application. +/// +/// NOTE: This is only to make tests more stable and is not intended to be used in applications. +FFI_PLUGIN_EXPORT void enableMultithreading(); + #ifdef __cplusplus } #endif diff --git a/flutter_local_notifications_windows/test/bindings_test.dart b/flutter_local_notifications_windows/test/bindings_test.dart index 71a6f7832..b26b15ba2 100644 --- a/flutter_local_notifications_windows/test/bindings_test.dart +++ b/flutter_local_notifications_windows/test/bindings_test.dart @@ -13,6 +13,7 @@ const Map bindings = { }; void main() => group('Bindings', () { + FlutterLocalNotificationsWindows().enableMultithreading(); final FlutterLocalNotificationsWindows plugin = FlutterLocalNotificationsWindows(); setUpAll(() => plugin.initialize(settings)); diff --git a/flutter_local_notifications_windows/test/details_test.dart b/flutter_local_notifications_windows/test/details_test.dart index 063ca2b05..d8c2af017 100644 --- a/flutter_local_notifications_windows/test/details_test.dart +++ b/flutter_local_notifications_windows/test/details_test.dart @@ -20,6 +20,7 @@ extension PluginUtils on FlutterLocalNotificationsWindows { } void main() => group('Details:', () { + FlutterLocalNotificationsWindows().enableMultithreading(); final FlutterLocalNotificationsWindows plugin = FlutterLocalNotificationsWindows(); setUpAll(() => plugin.initialize(settings)); diff --git a/flutter_local_notifications_windows/test/plugin_test.dart b/flutter_local_notifications_windows/test/plugin_test.dart index 7e9f6a15d..8f81be324 100644 --- a/flutter_local_notifications_windows/test/plugin_test.dart +++ b/flutter_local_notifications_windows/test/plugin_test.dart @@ -13,6 +13,8 @@ const WindowsInitializationSettings badSettings = WindowsInitializationSettings( appName: 'test', appUserModelId: 'com.test.test', guid: '123'); void main() => group('Plugin', () { + FlutterLocalNotificationsWindows().enableMultithreading(); + setUpAll(initializeTimeZones); test('initializes safely', () async { diff --git a/flutter_local_notifications_windows/test/scheduled_test.dart b/flutter_local_notifications_windows/test/scheduled_test.dart index fa33e04bd..b293c5e5f 100644 --- a/flutter_local_notifications_windows/test/scheduled_test.dart +++ b/flutter_local_notifications_windows/test/scheduled_test.dart @@ -9,6 +9,7 @@ const WindowsInitializationSettings settings = WindowsInitializationSettings( guid: 'a8c22b55-049e-422f-b30f-863694de08c8'); void main() => group('Schedules', () { + FlutterLocalNotificationsWindows().enableMultithreading(); final FlutterLocalNotificationsWindows plugin = FlutterLocalNotificationsWindows(); setUpAll(initializeTimeZones); diff --git a/flutter_local_notifications_windows/test/xml_test.dart b/flutter_local_notifications_windows/test/xml_test.dart index 3d1c6a569..9b5d1ea15 100644 --- a/flutter_local_notifications_windows/test/xml_test.dart +++ b/flutter_local_notifications_windows/test/xml_test.dart @@ -57,6 +57,8 @@ const String complexXml = ''' '''; void main() => group('XML', () { + FlutterLocalNotificationsWindows().enableMultithreading(); + final FlutterLocalNotificationsWindows plugin = FlutterLocalNotificationsWindows(); setUpAll(() => plugin.initialize(settings)); From 18360514f1cfd63c9af723a95a5d978e9d9de466 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Sun, 25 Aug 2024 16:46:31 -0400 Subject: [PATCH 081/112] Responded to feedback - Changed `toXml` to `buildXml` - Fixed comments on stub class - Bumped Dart version to ^3.2 - Fixed `enableMultithreading` not being always annotated - Only test on Windows --- ...ocal_notifications_platform_interface.dart | 8 ++++---- .../bin/crash.dart | 1 + .../dart_test.yaml | 1 + .../flutter_local_notifications_windows.dll | Bin 363520 -> 363520 bytes .../lib/src/details/notification_action.dart | 2 +- .../lib/src/details/notification_audio.dart | 2 +- .../lib/src/details/notification_details.dart | 16 ++++++++-------- .../lib/src/details/notification_header.dart | 2 +- .../lib/src/details/notification_image.dart | 2 +- .../lib/src/details/notification_input.dart | 10 +++++----- .../lib/src/details/notification_part.dart | 2 +- .../src/details/notification_progress.dart | 2 +- .../lib/src/details/notification_row.dart | 4 ++-- .../lib/src/details/notification_text.dart | 2 +- .../lib/src/details/notification_to_xml.dart | 8 +++++--- .../lib/src/plugin/ffi.dart | 2 ++ .../lib/src/plugin/stub.dart | 5 ++++- .../pubspec.yaml | 2 +- 18 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 flutter_local_notifications_windows/dart_test.yaml diff --git a/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart b/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart index 5521a30c0..387968afb 100644 --- a/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart +++ b/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart @@ -87,10 +87,10 @@ abstract class FlutterLocalNotificationsPlatform extends PlatformInterface { /// Returns the list of active notifications shown by the application that /// haven't been dismissed/removed. /// - /// Throws a [PlatformException] with an `unsupported_os_version` error code - /// when the OS version is older than what is supported to have results - /// returned. On platforms that don't support the method at all, - /// it will throw an [UnimplementedError]. + /// Throws a [PlatformException](https://api.flutter.dev/flutter/services/PlatformException-class.html) + /// with an `unsupported_os_version` error code when the OS version is older + /// than what is supported to have results returned. On platforms that don't + /// support the method at all, it will throw an [UnimplementedError]. Future> getActiveNotifications() { throw UnimplementedError( 'getActiveNotifications() has not been implemented'); diff --git a/flutter_local_notifications_windows/bin/crash.dart b/flutter_local_notifications_windows/bin/crash.dart index d8c5d40a5..7354403cd 100644 --- a/flutter_local_notifications_windows/bin/crash.dart +++ b/flutter_local_notifications_windows/bin/crash.dart @@ -31,6 +31,7 @@ void main() async { await Isolate.spawn(scheduledTest, null); // This is the critical line. Removing this causes crashes in the Windows SDK + // ignore: invalid_use_of_visible_for_testing_member FlutterLocalNotificationsWindows().enableMultithreading(); await Future.delayed(const Duration(seconds: 5)); diff --git a/flutter_local_notifications_windows/dart_test.yaml b/flutter_local_notifications_windows/dart_test.yaml new file mode 100644 index 000000000..96d9ec924 --- /dev/null +++ b/flutter_local_notifications_windows/dart_test.yaml @@ -0,0 +1 @@ +platforms: [vm] diff --git a/flutter_local_notifications_windows/flutter_local_notifications_windows.dll b/flutter_local_notifications_windows/flutter_local_notifications_windows.dll index 73cad21c9d69f53354f91858b42092c958bf1142..5f9a8629a392ec1bb7416cdc68a9115a1ebb8e3d 100644 GIT binary patch delta 39145 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�`w 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

5J9b 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 builder.element( + void buildXml(XmlBuilder builder) => builder.element( 'audio', attributes: { 'src': source, diff --git a/flutter_local_notifications_windows/lib/src/details/notification_details.dart b/flutter_local_notifications_windows/lib/src/details/notification_details.dart index d7c6ab39e..f544f0b61 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_details.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_details.dart @@ -104,7 +104,7 @@ class WindowsNotificationDetails { final Map bindings; /// Builds all relevant XML parts under the root `` element. - void toXml(XmlBuilder builder) { + void buildXml(XmlBuilder builder) { if (actions.length > 5) { throw ArgumentError( 'WindowsNotificationDetails can only have up to 5 actions', @@ -119,15 +119,15 @@ class WindowsNotificationDetails { 'actions', nest: () { for (final WindowsInput input in inputs) { - input.toXml(builder); + input.buildXml(builder); } for (final WindowsAction action in actions) { - action.toXml(builder); + action.buildXml(builder); } }, ); - audio?.toXml(builder); - header?.toXml(builder); + audio?.buildXml(builder); + header?.buildXml(builder); } /// Generates the `` element of the notification. @@ -138,13 +138,13 @@ class WindowsNotificationDetails { builder.element('text', nest: subtitle); } for (final WindowsImage image in images) { - image.toXml(builder); + image.buildXml(builder); } for (final WindowsRow group in groups) { - group.toXml(builder); + group.buildXml(builder); } for (final WindowsProgressBar progressBar in progressBars) { - progressBar.toXml(builder); + progressBar.buildXml(builder); } } } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_header.dart b/flutter_local_notifications_windows/lib/src/details/notification_header.dart index 4efa913bc..7c1edd400 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_header.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_header.dart @@ -32,7 +32,7 @@ class WindowsHeader { final WindowsHeaderActivation? activation; /// Serializes this header to XML. - void toXml(XmlBuilder builder) => builder.element( + void buildXml(XmlBuilder builder) => builder.element( 'header', attributes: { 'id': id, diff --git a/flutter_local_notifications_windows/lib/src/details/notification_image.dart b/flutter_local_notifications_windows/lib/src/details/notification_image.dart index 09e1bd2cb..ec178524b 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_image.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_image.dart @@ -46,7 +46,7 @@ class WindowsImage extends WindowsNotificationPart { final WindowsImageCrop? crop; @override - void toXml(XmlBuilder builder) { + void buildXml(XmlBuilder builder) { if (!file.isAbsolute) { throw ArgumentError.value( file.path, diff --git a/flutter_local_notifications_windows/lib/src/details/notification_input.dart b/flutter_local_notifications_windows/lib/src/details/notification_input.dart index d7d3eeefe..3b7a0bdbf 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_input.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_input.dart @@ -30,7 +30,7 @@ abstract class WindowsInput { final String? title; /// Serializes this input to XML. - void toXml(XmlBuilder builder); + void buildXml(XmlBuilder builder); } /// A text input. @@ -46,7 +46,7 @@ class WindowsTextInput extends WindowsInput { final String? placeHolderContent; @override - void toXml(XmlBuilder builder) => builder.element( + void buildXml(XmlBuilder builder) => builder.element( 'input', attributes: { 'id': id, @@ -75,7 +75,7 @@ class WindowsSelectionInput extends WindowsInput { final String? defaultItem; @override - void toXml(XmlBuilder builder) => builder.element( + void buildXml(XmlBuilder builder) => builder.element( 'input', attributes: { 'id': id, @@ -85,7 +85,7 @@ class WindowsSelectionInput extends WindowsInput { }, nest: () { for (final WindowsSelection item in items) { - item.toXml(builder); + item.buildXml(builder); } }, ); @@ -106,7 +106,7 @@ class WindowsSelection { final String content; /// Serializes this item to XML. - void toXml(XmlBuilder builder) => builder.element( + void buildXml(XmlBuilder builder) => builder.element( 'selection', attributes: { 'id': id, diff --git a/flutter_local_notifications_windows/lib/src/details/notification_part.dart b/flutter_local_notifications_windows/lib/src/details/notification_part.dart index c62087dc1..914232798 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_part.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_part.dart @@ -13,5 +13,5 @@ abstract class WindowsNotificationPart { const WindowsNotificationPart(); /// Serializes this part to XML, according to the Windows API. - void toXml(XmlBuilder builder); + void buildXml(XmlBuilder builder); } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_progress.dart b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart index f5f3c6c15..88b67de6b 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_progress.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart @@ -36,7 +36,7 @@ class WindowsProgressBar { String? label; /// Serializes this progress bar to XML. - void toXml(XmlBuilder builder) => builder.element( + void buildXml(XmlBuilder builder) => builder.element( 'progress', attributes: { 'status': status, diff --git a/flutter_local_notifications_windows/lib/src/details/notification_row.dart b/flutter_local_notifications_windows/lib/src/details/notification_row.dart index cb8002900..c334344a2 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_row.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_row.dart @@ -13,7 +13,7 @@ class WindowsRow { final List columns; /// Serializes this group to XML. - void toXml(XmlBuilder builder) => builder.element( + void buildXml(XmlBuilder builder) => builder.element( 'group', nest: () { for (final WindowsColumn column in columns) { @@ -22,7 +22,7 @@ class WindowsRow { attributes: {'hint-weight': '1'}, nest: () { for (final WindowsNotificationPart part in column.parts) { - part.toXml(builder); + part.buildXml(builder); } }, ); diff --git a/flutter_local_notifications_windows/lib/src/details/notification_text.dart b/flutter_local_notifications_windows/lib/src/details/notification_text.dart index cdda9be8e..93b344a5a 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_text.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_text.dart @@ -39,7 +39,7 @@ class WindowsNotificationText extends WindowsNotificationPart { final String? languageCode; @override - void toXml(XmlBuilder builder) => builder.element( + void buildXml(XmlBuilder builder) => builder.element( 'text', attributes: { if (languageCode != null) 'lang': languageCode!, diff --git a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart index 0ce0e87fa..17d643f96 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart @@ -31,8 +31,10 @@ extension on WindowsNotificationDetails { extension ProgressBarXml on WindowsProgressBar { /// The data bindings for this progress bar. /// - /// To support dynamic updates, [toXml] will inject placeholder strings called - /// data bindings instead of actual values. This represents the new data. + /// To support dynamic updates, [buildXml] will inject placeholder strings + /// called data bindings instead of actual values. This can then be updated + /// dynamically later by calling + /// [FlutterLocalNotificationsWindows.updateProgressBar]. Map get data => { '$id-progressValue': value?.toString() ?? 'indeterminate', if (label != null) '$id-progressString': label!, @@ -72,7 +74,7 @@ String notificationToXml({ ); }, ); - details?.toXml(builder); + details?.buildXml(builder); }, ); return builder diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index 31f32b95e..93ed0a4df 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -1,5 +1,6 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart'; +import 'package:meta/meta.dart'; import '../details.dart'; import '../details/notification_to_xml.dart'; @@ -364,5 +365,6 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { }); @override + @visibleForTesting void enableMultithreading() => _bindings.enableMultithreading(); } diff --git a/flutter_local_notifications_windows/lib/src/plugin/stub.dart b/flutter_local_notifications_windows/lib/src/plugin/stub.dart index 96963f556..95a9f7540 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/stub.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/stub.dart @@ -1,7 +1,9 @@ +import 'package:meta/meta.dart'; + import '../details.dart'; import 'base.dart'; -/// A stub implementation for platforms that don't support FFI. +/// The Windows implementation of `package:flutter_local_notifications`. class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { @override Future initialize( @@ -91,5 +93,6 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { }) async => NotificationUpdateResult.success; @override + @visibleForTesting void enableMultithreading() { } } diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index 0c46b8c61..54099a548 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -3,7 +3,7 @@ description: "A new Flutter FFI plugin project." version: 1.0.0 environment: - sdk: ">=3.1.0 <4.0.0" + sdk: ">=3.2.0 <4.0.0" dependencies: ffi: ^2.1.2 From 53a52c130fb9fa4fee5fbe84d41a2f26ddc86b02 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 26 Aug 2024 13:44:53 -0400 Subject: [PATCH 082/112] Updated minimum Flutter version to 3.16 --- .github/workflows/validate.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ab01def74..d6cde2e5e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -78,14 +78,14 @@ jobs: - name: Build run: melos run build:example_android build_example_android_3_13: - name: Build Android example app (3.13) + name: Build Android example app (3.16) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.13.0 + flutter-version: 3.16.0 cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - name: Install Tools @@ -107,14 +107,14 @@ jobs: - name: Build run: melos run build:example_ios build_example_ios_3_13: - name: Build iOS example app (3.13) + name: Build iOS example app (3.16) runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.13.0 + flutter-version: 3.16.0 cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - name: Install Tools @@ -136,14 +136,14 @@ jobs: - name: Build run: melos run build:example_macos build_example_macos_3_13: - name: Build macOS example app (3.13) + name: Build macOS example app (3.16) runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.13.0 + flutter-version: 3.16.0 cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - name: Install Tools @@ -169,14 +169,14 @@ jobs: - name: Build run: melos run build:example_linux build_example_linux_3_13: - name: Build Linux example app (3.13) + name: Build Linux example app (3.16) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.13.0 + flutter-version: 3.16.0 cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - name: Install Tools From ce437e5c57cb9cdd657bac9325ffd96182fd9b6e Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 26 Aug 2024 13:45:26 -0400 Subject: [PATCH 083/112] Added _windows to dependabot --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f9ef166a9..1010ca75b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,10 @@ updates: directory: "/flutter_local_notifications_linux" schedule: interval: "daily" + - package-ecosystem: "pub" + directory: "/flutter_local_notifications_windows" + schedule: + interval: "daily" - package-ecosystem: "pub" directory: "/flutter_local_notifications" schedule: From 223d311bb59407b5a690a0fcbbaad0afe1d63e48 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 26 Aug 2024 13:47:09 -0400 Subject: [PATCH 084/112] Changed main package to use Dart 3.2, Flutter 3.16 --- flutter_local_notifications/pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index 56d5cc07f..62509fc1b 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -39,5 +39,5 @@ flutter: default_package: flutter_local_notifications_windows environment: - sdk: ^3.1.0 - flutter: ">=3.13.0" + sdk: ^3.2.0 + flutter: ">=3.16.0" From a93fc0eb0bc0e37a36c6642d13c9ac0915dcb573 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 26 Aug 2024 14:27:09 -0400 Subject: [PATCH 085/112] Moved all XML code to private files --- .../example/lib/windows.dart | 2 +- .../lib/src/details/notification_action.dart | 30 --------- .../lib/src/details/notification_audio.dart | 12 ---- .../lib/src/details/notification_details.dart | 53 +-------------- .../lib/src/details/notification_header.dart | 13 ---- .../lib/src/details/notification_image.dart | 23 ------- .../lib/src/details/notification_input.dart | 44 +----------- .../lib/src/details/notification_part.dart | 5 -- .../src/details/notification_progress.dart | 13 ---- .../lib/src/details/notification_row.dart | 20 ------ .../lib/src/details/notification_text.dart | 15 ----- .../lib/src/details/notification_to_xml.dart | 1 + .../lib/src/details/xml/action.dart | 33 +++++++++ .../lib/src/details/xml/audio.dart | 17 +++++ .../lib/src/details/xml/details.dart | 67 +++++++++++++++++++ .../lib/src/details/xml/header.dart | 19 ++++++ .../lib/src/details/xml/image.dart | 29 ++++++++ .../lib/src/details/xml/input.dart | 55 +++++++++++++++ .../lib/src/details/xml/progress.dart | 19 ++++++ .../lib/src/details/xml/row.dart | 35 ++++++++++ .../lib/src/details/xml/text.dart | 21 ++++++ .../test/details_test.dart | 10 +-- 22 files changed, 306 insertions(+), 230 deletions(-) create mode 100644 flutter_local_notifications_windows/lib/src/details/xml/action.dart create mode 100644 flutter_local_notifications_windows/lib/src/details/xml/audio.dart create mode 100644 flutter_local_notifications_windows/lib/src/details/xml/details.dart create mode 100644 flutter_local_notifications_windows/lib/src/details/xml/header.dart create mode 100644 flutter_local_notifications_windows/lib/src/details/xml/image.dart create mode 100644 flutter_local_notifications_windows/lib/src/details/xml/input.dart create mode 100644 flutter_local_notifications_windows/lib/src/details/xml/progress.dart create mode 100644 flutter_local_notifications_windows/lib/src/details/xml/row.dart create mode 100644 flutter_local_notifications_windows/lib/src/details/xml/text.dart diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index 69781eb47..3452baf0b 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -217,7 +217,7 @@ Future _showWindowsNotificationWithGroups() => flutterLocalNotificationsPl NotificationDetails( windows: WindowsNotificationDetails( subtitle: 'Caption text is fainter', - groups: [ + rows: [ WindowsRow([ WindowsColumn([ WindowsImage.file(File('icons/coworker.png').absolute, altText: 'A coworker'), diff --git a/flutter_local_notifications_windows/lib/src/details/notification_action.dart b/flutter_local_notifications_windows/lib/src/details/notification_action.dart index 06f7f7c47..946785470 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_action.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_action.dart @@ -1,7 +1,5 @@ import 'dart:io'; -import 'package:xml/xml.dart'; - // NOTE: All enum values in this file have Windows RT-specific names. // If you change their Dart names, be sure to override [Enum.name]. @@ -103,32 +101,4 @@ class WindowsAction { /// The tooltip, useful if [content] is empty. final String? tooltip; - - /// Serializes this notification action as Windows-compatible XML. - /// - /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action#syntax - void buildXml(XmlBuilder builder) { - if (image != null && !image!.isAbsolute) { - throw ArgumentError.value( - image!.path, - 'WindowsImage.file', - 'File path must be absolute', - ); - } - builder.element( - 'action', - attributes: { - 'content': content, - 'arguments': arguments, - 'activationType': activationType.name, - 'afterActivationBehavior': activationBehavior.name, - if (placement != null) 'placement': placement!.name, - if (image != null) 'imageUri': - Uri.file(image!.absolute.path, windows: true).toFilePath(), - if (inputId != null) 'hint-inputId': inputId!, - if (buttonStyle != null) 'hint-buttonStyle': buttonStyle!.name, - if (tooltip != null) 'hint-toolTip': tooltip!, - }, - ); - } } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart index e22555080..577fd338b 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart @@ -1,5 +1,3 @@ -import 'package:xml/xml.dart'; - extension on Uri { String get filename => pathSegments.last; String get extension => pathSegments.last.split('.').last; @@ -151,14 +149,4 @@ class WindowsNotificationAudio { /// The source of the audio. final String source; - - /// Serializes this audio to Windows-compatible XML. - void buildXml(XmlBuilder builder) => builder.element( - 'audio', - attributes: { - 'src': source, - 'silent': isSilent.toString(), - 'loop': shouldLoop.toString(), - }, - ); } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_details.dart b/flutter_local_notifications_windows/lib/src/details/notification_details.dart index f544f0b61..5b302dee4 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_details.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_details.dart @@ -1,5 +1,3 @@ -import 'package:xml/xml.dart'; - import 'notification_action.dart'; import 'notification_audio.dart'; import 'notification_header.dart'; @@ -51,7 +49,7 @@ class WindowsNotificationDetails { this.actions = const [], this.inputs = const [], this.images = const [], - this.groups = const [], + this.rows = const [], this.progressBars = const [], this.bindings = const {}, this.header, @@ -89,8 +87,8 @@ class WindowsNotificationDetails { /// A list of images to show. final List images; - /// A list of groups to show. - final List groups; + /// A list of rows to show. + final List rows; /// A list of progress bars to show. final List progressBars; @@ -102,49 +100,4 @@ class WindowsNotificationDetails { /// while or after the notification is launched by using the binding name as /// the key here, and the value as any string you want. final Map bindings; - - /// Builds all relevant XML parts under the root `` element. - void buildXml(XmlBuilder builder) { - if (actions.length > 5) { - throw ArgumentError( - 'WindowsNotificationDetails can only have up to 5 actions', - ); - } - if (inputs.length > 5) { - throw ArgumentError( - 'WindowsNotificationDetails can only have up to 5 inputs', - ); - } - builder.element( - 'actions', - nest: () { - for (final WindowsInput input in inputs) { - input.buildXml(builder); - } - for (final WindowsAction action in actions) { - action.buildXml(builder); - } - }, - ); - audio?.buildXml(builder); - header?.buildXml(builder); - } - - /// Generates the `` element of the notification. - /// - /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-binding - void generateBinding(XmlBuilder builder) { - if (subtitle != null) { - builder.element('text', nest: subtitle); - } - for (final WindowsImage image in images) { - image.buildXml(builder); - } - for (final WindowsRow group in groups) { - group.buildXml(builder); - } - for (final WindowsProgressBar progressBar in progressBars) { - progressBar.buildXml(builder); - } - } } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_header.dart b/flutter_local_notifications_windows/lib/src/details/notification_header.dart index 7c1edd400..a5fbd14f8 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_header.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_header.dart @@ -1,5 +1,3 @@ -import 'package:xml/xml.dart'; - /// Decides how the application will open when the header is pressed. enum WindowsHeaderActivation { /// Opens the app in the foreground. @@ -30,15 +28,4 @@ class WindowsHeader { /// Specifies how the application will open. final WindowsHeaderActivation? activation; - - /// Serializes this header to XML. - void buildXml(XmlBuilder builder) => builder.element( - 'header', - attributes: { - 'id': id, - 'title': title, - 'arguments': arguments, - if (activation != null) 'activationType': activation!.name, - }, - ); } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_image.dart b/flutter_local_notifications_windows/lib/src/details/notification_image.dart index ec178524b..c367299b2 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_image.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_image.dart @@ -1,7 +1,5 @@ import 'dart:io'; -import 'package:xml/xml.dart'; - import 'notification_part.dart'; /// Where a Windows notification image can be placed. @@ -44,25 +42,4 @@ class WindowsImage extends WindowsNotificationPart { /// How the image will be cropped. Null indicates uncropped. final WindowsImageCrop? crop; - - @override - void buildXml(XmlBuilder builder) { - if (!file.isAbsolute) { - throw ArgumentError.value( - file.path, - 'WindowsImage.file', - 'File path must be absolute', - ); - } - builder.element( - 'image', - attributes: { - 'src': Uri.file(file.absolute.path, windows: true).toFilePath(), - 'alt': altText, - 'addImageQuery': addQueryParams.toString(), - if (placement != null) 'placement': placement!.name, - if (crop != null) 'hint-crop': crop!.name, - }, - ); - } } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_input.dart b/flutter_local_notifications_windows/lib/src/details/notification_input.dart index 3b7a0bdbf..f13291efb 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_input.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_input.dart @@ -1,5 +1,3 @@ -import 'package:xml/xml.dart'; - /// The type of a [WindowsInput]. enum WindowsInputType { /// A text input. @@ -10,7 +8,7 @@ enum WindowsInputType { } /// A text or multiple choice input element in a Windows notification. -abstract class WindowsInput { +sealed class WindowsInput { /// Creates an input field in a notification. const WindowsInput({ required this.id, @@ -28,9 +26,6 @@ abstract class WindowsInput { /// The title of this input. final String? title; - - /// Serializes this input to XML. - void buildXml(XmlBuilder builder); } /// A text input. @@ -44,18 +39,6 @@ class WindowsTextInput extends WindowsInput { /// A placeholder shown before the user enters input, like a hint text. final String? placeHolderContent; - - @override - void buildXml(XmlBuilder builder) => builder.element( - 'input', - attributes: { - 'id': id, - 'type': type.name, - if (title != null) 'title': title!, - if (placeHolderContent != null) - 'placeHolderContent': placeHolderContent!, - }, - ); } /// A multiple choice input. @@ -73,22 +56,6 @@ class WindowsSelectionInput extends WindowsInput { /// The default item that is selected. final String? defaultItem; - - @override - void buildXml(XmlBuilder builder) => builder.element( - 'input', - attributes: { - 'id': id, - 'type': type.name, - if (title != null) 'title': title!, - if (defaultItem != null) 'defaultInput': defaultItem!, - }, - nest: () { - for (final WindowsSelection item in items) { - item.buildXml(builder); - } - }, - ); } /// An option that can be selected by a [WindowsSelectionInput]. @@ -104,13 +71,4 @@ class WindowsSelection { /// The content of this item in the UI. final String content; - - /// Serializes this item to XML. - void buildXml(XmlBuilder builder) => builder.element( - 'selection', - attributes: { - 'id': id, - 'content': content, - }, - ); } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_part.dart b/flutter_local_notifications_windows/lib/src/details/notification_part.dart index 914232798..d409b4ae9 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_part.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_part.dart @@ -1,5 +1,3 @@ -import 'package:xml/xml.dart'; - /// A text or image element in a Windows notification. /// /// Note: This should not be used for anything else as notification @@ -11,7 +9,4 @@ import 'package:xml/xml.dart'; abstract class WindowsNotificationPart { /// A const constructor. const WindowsNotificationPart(); - - /// Serializes this part to XML, according to the Windows API. - void buildXml(XmlBuilder builder); } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_progress.dart b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart index 88b67de6b..f7847a422 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_progress.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart @@ -1,5 +1,3 @@ -import 'package:xml/xml.dart'; - import '../../flutter_local_notifications_windows.dart'; /// A progress bar in a Windows notification. @@ -34,15 +32,4 @@ class WindowsProgressBar { /// /// Useful for indicating discrete progress, like `3/10` instead of `30%`. String? label; - - /// Serializes this progress bar to XML. - void buildXml(XmlBuilder builder) => builder.element( - 'progress', - attributes: { - 'status': status, - 'value': '{$id-progressValue}', - if (title != null) 'title': title!, - if (label != null) 'valueStringOverride': '{$id-progressString}', - }, - ); } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_row.dart b/flutter_local_notifications_windows/lib/src/details/notification_row.dart index c334344a2..8edee9648 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_row.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_row.dart @@ -1,5 +1,3 @@ -import 'package:xml/xml.dart'; - import 'notification_part.dart'; /// A group of notification content that must be displayed as a whole row. @@ -11,24 +9,6 @@ class WindowsRow { /// The different columns being grouped together. final List columns; - - /// Serializes this group to XML. - void buildXml(XmlBuilder builder) => builder.element( - 'group', - nest: () { - for (final WindowsColumn column in columns) { - builder.element( - 'subgroup', - attributes: {'hint-weight': '1'}, - nest: () { - for (final WindowsNotificationPart part in column.parts) { - part.buildXml(builder); - } - }, - ); - } - }, - ); } /// A vertical column of text and images in a Windows notification. diff --git a/flutter_local_notifications_windows/lib/src/details/notification_text.dart b/flutter_local_notifications_windows/lib/src/details/notification_text.dart index 93b344a5a..9437d5bb9 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_text.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_text.dart @@ -1,5 +1,3 @@ -import 'package:xml/xml.dart'; - import 'notification_part.dart'; /// Where text can be placed in a Windows notification. @@ -37,17 +35,4 @@ class WindowsNotificationText extends WindowsNotificationPart { /// The language of this text. final String? languageCode; - - @override - void buildXml(XmlBuilder builder) => builder.element( - 'text', - attributes: { - if (languageCode != null) 'lang': languageCode!, - if (placement != null) 'placement': placement!.name, - 'hint-callScenarioCenterAlign': centerIfCall.toString(), - 'hint-align': 'center', - if (isCaption) 'hint-style': 'captionsubtle', - }, - nest: text, - ); } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart index 17d643f96..7e18f3f2a 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart @@ -1,6 +1,7 @@ import 'package:xml/xml.dart'; import '../../flutter_local_notifications_windows.dart'; +import 'xml/details.dart'; extension on DateTime { String toIso8601StringTz() { diff --git a/flutter_local_notifications_windows/lib/src/details/xml/action.dart b/flutter_local_notifications_windows/lib/src/details/xml/action.dart new file mode 100644 index 000000000..42b986adb --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details/xml/action.dart @@ -0,0 +1,33 @@ +import 'package:xml/xml.dart'; +import '../notification_action.dart'; + +/// Converts a [WindowsAction] to XML +extension ActionToXml on WindowsAction { + /// Serializes this notification action as Windows-compatible XML. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action#syntax + void buildXml(XmlBuilder builder) { + if (image != null && !image!.isAbsolute) { + throw ArgumentError.value( + image!.path, + 'WindowsImage.file', + 'File path must be absolute', + ); + } + builder.element( + 'action', + attributes: { + 'content': content, + 'arguments': arguments, + 'activationType': activationType.name, + 'afterActivationBehavior': activationBehavior.name, + if (placement != null) 'placement': placement!.name, + if (image != null) 'imageUri': + Uri.file(image!.absolute.path, windows: true).toFilePath(), + if (inputId != null) 'hint-inputId': inputId!, + if (buttonStyle != null) 'hint-buttonStyle': buttonStyle!.name, + if (tooltip != null) 'hint-toolTip': tooltip!, + }, + ); + } +} diff --git a/flutter_local_notifications_windows/lib/src/details/xml/audio.dart b/flutter_local_notifications_windows/lib/src/details/xml/audio.dart new file mode 100644 index 000000000..d62539fc9 --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details/xml/audio.dart @@ -0,0 +1,17 @@ +import 'package:xml/xml.dart'; +import '../notification_audio.dart'; + +/// Converts a [WindowsNotificationAudio] to XML +extension AudioToXml on WindowsNotificationAudio { + /// Serializes this audio to Windows-compatible XML. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-audio + void buildXml(XmlBuilder builder) => builder.element( + 'audio', + attributes: { + 'src': source, + 'silent': isSilent.toString(), + 'loop': shouldLoop.toString(), + }, + ); +} diff --git a/flutter_local_notifications_windows/lib/src/details/xml/details.dart b/flutter_local_notifications_windows/lib/src/details/xml/details.dart new file mode 100644 index 000000000..f2423531f --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details/xml/details.dart @@ -0,0 +1,67 @@ +import 'package:xml/xml.dart'; + +import '../notification_action.dart'; +import '../notification_details.dart'; +import '../notification_image.dart'; +import '../notification_input.dart'; +import '../notification_progress.dart'; +import '../notification_row.dart'; + +import 'action.dart'; +import 'audio.dart'; +import 'header.dart'; +import 'image.dart'; +import 'input.dart'; +import 'progress.dart'; +import 'row.dart' +; +/// Converts a [WindowsNotificationDetails] to XML +extension DetailsToXml on WindowsNotificationDetails { + /// Builds all relevant XML parts under the root `` element. + void buildXml(XmlBuilder builder) { + if (actions.length > 5) { + throw ArgumentError( + 'WindowsNotificationDetails can only have up to 5 actions', + ); + } + if (inputs.length > 5) { + throw ArgumentError( + 'WindowsNotificationDetails can only have up to 5 inputs', + ); + } + builder.element( + 'actions', + nest: () { + for (final WindowsInput input in inputs) { + switch (input) { + case WindowsTextInput(): input.buildXml(builder); + case WindowsSelectionInput(): input.buildXml(builder); + } + } + for (final WindowsAction action in actions) { + action.buildXml(builder); + } + }, + ); + audio?.buildXml(builder); + header?.buildXml(builder); + } + + /// Generates the `` element of the notification. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-binding + void generateBinding(XmlBuilder builder) { + if (subtitle != null) { + builder.element('text', nest: subtitle); + } + for (final WindowsImage image in images) { + image.buildXml(builder); + } + for (final WindowsRow row in rows) { + row.buildXml(builder); + } + for (final WindowsProgressBar progressBar in progressBars) { + progressBar.buildXml(builder); + } + } +} diff --git a/flutter_local_notifications_windows/lib/src/details/xml/header.dart b/flutter_local_notifications_windows/lib/src/details/xml/header.dart new file mode 100644 index 000000000..12a8a66ea --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details/xml/header.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +import '../notification_header.dart'; + +/// Converts a [WindowsHeader] to XML +extension HeaderToXml on WindowsHeader { + /// Serializes this header to XML. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-header + void buildXml(XmlBuilder builder) => builder.element( + 'header', + attributes: { + 'id': id, + 'title': title, + 'arguments': arguments, + if (activation != null) 'activationType': activation!.name, + }, + ); +} diff --git a/flutter_local_notifications_windows/lib/src/details/xml/image.dart b/flutter_local_notifications_windows/lib/src/details/xml/image.dart new file mode 100644 index 000000000..ac9fef5ea --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details/xml/image.dart @@ -0,0 +1,29 @@ +import 'package:xml/xml.dart'; + +import '../notification_image.dart'; + +/// Converts a [WindowsImage] to XML +extension ImageToXml on WindowsImage { + /// Serializes this image to Windows-compatible XML. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-image + void buildXml(XmlBuilder builder) { + if (!file.isAbsolute) { + throw ArgumentError.value( + file.path, + 'WindowsImage.file', + 'File path must be absolute', + ); + } + builder.element( + 'image', + attributes: { + 'src': Uri.file(file.absolute.path, windows: true).toFilePath(), + 'alt': altText, + 'addImageQuery': addQueryParams.toString(), + if (placement != null) 'placement': placement!.name, + if (crop != null) 'hint-crop': crop!.name, + }, + ); + } +} diff --git a/flutter_local_notifications_windows/lib/src/details/xml/input.dart b/flutter_local_notifications_windows/lib/src/details/xml/input.dart new file mode 100644 index 000000000..55cce695c --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details/xml/input.dart @@ -0,0 +1,55 @@ +import 'package:xml/xml.dart'; + +import '../notification_input.dart'; + +/// Converts a [WindowsTextInput] to XML +extension TextInputToXml on WindowsTextInput { + /// Serializes this input to Windows-compatible XML. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-input + void buildXml(XmlBuilder builder) => builder.element( + 'input', + attributes: { + 'id': id, + 'type': type.name, + if (title != null) 'title': title!, + if (placeHolderContent != null) + 'placeHolderContent': placeHolderContent!, + }, + ); +} + +/// Converts a [WindowsSelectionInput] to XML +extension SelectionInputToXml on WindowsSelectionInput { + /// Serializes this input to Windows-compatible XML. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-input + void buildXml(XmlBuilder builder) => builder.element( + 'input', + attributes: { + 'id': id, + 'type': type.name, + if (title != null) 'title': title!, + if (defaultItem != null) 'defaultInput': defaultItem!, + }, + nest: () { + for (final WindowsSelection item in items) { + item.buildXml(builder); + } + }, + ); +} + +/// Converts a [WindowsSelection] to XML +extension SelectionToXml on WindowsSelection { + /// Serializes this selection to Windows-compatible XML. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-selection + void buildXml(XmlBuilder builder) => builder.element( + 'selection', + attributes: { + 'id': id, + 'content': content, + }, + ); +} diff --git a/flutter_local_notifications_windows/lib/src/details/xml/progress.dart b/flutter_local_notifications_windows/lib/src/details/xml/progress.dart new file mode 100644 index 000000000..111d0129d --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details/xml/progress.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +import '../notification_progress.dart'; + +/// Converts a [WindowsProgressBar] to XML +extension ProgressBarToXml on WindowsProgressBar { + /// Serializes this progress bar to Windows-compatible XML. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-progress + void buildXml(XmlBuilder builder) => builder.element( + 'progress', + attributes: { + 'status': status, + 'value': '{$id-progressValue}', + if (title != null) 'title': title!, + if (label != null) 'valueStringOverride': '{$id-progressString}', + }, + ); +} diff --git a/flutter_local_notifications_windows/lib/src/details/xml/row.dart b/flutter_local_notifications_windows/lib/src/details/xml/row.dart new file mode 100644 index 000000000..fe7f57b5a --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details/xml/row.dart @@ -0,0 +1,35 @@ +import 'package:xml/xml.dart'; + +import '../notification_image.dart'; +import '../notification_part.dart'; +import '../notification_row.dart'; +import '../notification_text.dart'; + +import 'image.dart'; +import 'text.dart'; + +/// Converts a [WindowsRow] to XML +extension RowToXml on WindowsRow { + /// Serializes this group to XML. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-group + void buildXml(XmlBuilder builder) => builder.element( + 'group', + nest: () { + for (final WindowsColumn column in columns) { + builder.element( + 'subgroup', + attributes: {'hint-weight': '1'}, + nest: () { + for (final WindowsNotificationPart part in column.parts) { + switch (part) { + case WindowsImage(): part.buildXml(builder); + case WindowsNotificationText(): part.buildXml(builder); + } + } + }, + ); + } + }, + ); +} diff --git a/flutter_local_notifications_windows/lib/src/details/xml/text.dart b/flutter_local_notifications_windows/lib/src/details/xml/text.dart new file mode 100644 index 000000000..93fe1de37 --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details/xml/text.dart @@ -0,0 +1,21 @@ +import 'package:xml/xml.dart'; + +import '../notification_text.dart'; + +/// Converts a [WindowsNotificationText] to XML +extension TextToXml on WindowsNotificationText { + /// Serializes this text to Windows-compatible XML. + /// + /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-text + void buildXml(XmlBuilder builder) => builder.element( + 'text', + attributes: { + if (languageCode != null) 'lang': languageCode!, + if (placement != null) 'placement': placement!.name, + 'hint-callScenarioCenterAlign': centerIfCall.toString(), + 'hint-align': 'center', + if (isCaption) 'hint-style': 'captionsubtle', + }, + nest: text, + ); +} diff --git a/flutter_local_notifications_windows/test/details_test.dart b/flutter_local_notifications_windows/test/details_test.dart index d8c2af017..733bd5ca0 100644 --- a/flutter_local_notifications_windows/test/details_test.dart +++ b/flutter_local_notifications_windows/test/details_test.dart @@ -107,17 +107,17 @@ void main() => group('Details:', () { ..testDetails(const WindowsNotificationDetails()) ..testDetails(const WindowsNotificationDetails( - groups: [WindowsRow([])])) - ..testDetails(const WindowsNotificationDetails(groups: [ + rows: [WindowsRow([])])) + ..testDetails(const WindowsNotificationDetails(rows: [ WindowsRow([emptyColumn]) ])) - ..testDetails(WindowsNotificationDetails(groups: [ + ..testDetails(WindowsNotificationDetails(rows: [ WindowsRow([simpleColumn]) ])) ..testDetails( - WindowsNotificationDetails(groups: [bigRow])) + WindowsNotificationDetails(rows: [bigRow])) ..testDetails( - WindowsNotificationDetails(groups: List.filled(5, bigRow))); + WindowsNotificationDetails(rows: List.filled(5, bigRow))); }); test('Header', () async { From ec69b538c6a9ad4fd8957c088b8bef96d7578c80 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 26 Aug 2024 14:31:55 -0400 Subject: [PATCH 086/112] Cleanup --- .../lib/src/details/notification_to_xml.dart | 39 +------------------ .../lib/src/details/xml/details.dart | 27 ++++++++++++- .../lib/src/details/xml/progress.dart | 11 ++++++ .../lib/src/plugin/base.dart | 2 +- 4 files changed, 38 insertions(+), 41 deletions(-) diff --git a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart index 7e18f3f2a..38747efcc 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart @@ -3,44 +3,7 @@ import 'package:xml/xml.dart'; import '../../flutter_local_notifications_windows.dart'; import 'xml/details.dart'; -extension on DateTime { - String toIso8601StringTz() { - // Get offset - final Duration offset = timeZoneOffset; - final String sign = offset.isNegative ? '-' : '+'; - final String hours = offset.inHours.abs().toString().padLeft(2, '0'); - final String minutes = - offset.inMinutes.abs().remainder(60).toString().padLeft(2, '0'); - final String offsetString = '$sign$hours:$minutes'; - // Get first part of properly formatted ISO 8601 date - final String formattedDate = toIso8601String().split('.').first; - return '$formattedDate$offsetString'; - } -} - -extension on WindowsNotificationDetails { - /// XML attributes for the toast notification as a whole. - Map get attributes => { - if (duration != null) 'duration': duration!.name, - if (timestamp != null) - 'displayTimestamp': timestamp!.toIso8601StringTz(), - if (scenario != null) 'scenario': scenario!.name, - }; -} - -/// Extensions on [WindowsProgressBar]. -extension ProgressBarXml on WindowsProgressBar { - /// The data bindings for this progress bar. - /// - /// To support dynamic updates, [buildXml] will inject placeholder strings - /// called data bindings instead of actual values. This can then be updated - /// dynamically later by calling - /// [FlutterLocalNotificationsWindows.updateProgressBar]. - Map get data => { - '$id-progressValue': value?.toString() ?? 'indeterminate', - if (label != null) '$id-progressString': label!, - }; -} +export 'xml/progress.dart'; /// Converts a notification with [WindowsNotificationDetails] into XML. /// diff --git a/flutter_local_notifications_windows/lib/src/details/xml/details.dart b/flutter_local_notifications_windows/lib/src/details/xml/details.dart index f2423531f..accc46f3d 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/details.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/details.dart @@ -13,8 +13,23 @@ import 'header.dart'; import 'image.dart'; import 'input.dart'; import 'progress.dart'; -import 'row.dart' -; +import 'row.dart'; + +extension on DateTime { + String toIso8601StringTz() { + // Get offset + final Duration offset = timeZoneOffset; + final String sign = offset.isNegative ? '-' : '+'; + final String hours = offset.inHours.abs().toString().padLeft(2, '0'); + final String minutes = + offset.inMinutes.abs().remainder(60).toString().padLeft(2, '0'); + final String offsetString = '$sign$hours:$minutes'; + // Get first part of properly formatted ISO 8601 date + final String formattedDate = toIso8601String().split('.').first; + return '$formattedDate$offsetString'; + } +} + /// Converts a [WindowsNotificationDetails] to XML extension DetailsToXml on WindowsNotificationDetails { /// Builds all relevant XML parts under the root `` element. @@ -64,4 +79,12 @@ extension DetailsToXml on WindowsNotificationDetails { progressBar.buildXml(builder); } } + + /// XML attributes for the toast notification as a whole. + Map get attributes => { + if (duration != null) 'duration': duration!.name, + if (timestamp != null) + 'displayTimestamp': timestamp!.toIso8601StringTz(), + if (scenario != null) 'scenario': scenario!.name, + }; } diff --git a/flutter_local_notifications_windows/lib/src/details/xml/progress.dart b/flutter_local_notifications_windows/lib/src/details/xml/progress.dart index 111d0129d..765d04efb 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/progress.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/progress.dart @@ -16,4 +16,15 @@ extension ProgressBarToXml on WindowsProgressBar { if (label != null) 'valueStringOverride': '{$id-progressString}', }, ); + + /// The data bindings for this progress bar. + /// + /// To support dynamic updates, [buildXml] will inject placeholder strings + /// called data bindings instead of actual values. This can then be updated + /// dynamically later by calling + /// [FlutterLocalNotificationsWindows.updateProgressBar]. + Map get data => { + '$id-progressValue': value?.toString() ?? 'indeterminate', + if (label != null) '$id-progressString': label!, + }; } diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart index 918eb7dc5..ee48c8668 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/base.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -3,7 +3,7 @@ import 'package:meta/meta.dart'; import 'package:timezone/timezone.dart'; import '../details.dart'; -import '../details/notification_to_xml.dart'; +import '../details/xml/progress.dart'; export 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart'; export 'package:timezone/timezone.dart'; From cb993caacfb6b0ea33e1e3c94b01d20f8093e0d1 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 26 Aug 2024 14:45:21 -0400 Subject: [PATCH 087/112] Sealed WindowsNotificationPart --- .../lib/src/details.dart | 4 +- .../lib/src/details/notification_details.dart | 5 +- .../lib/src/details/notification_image.dart | 45 --------- .../lib/src/details/notification_part.dart | 12 --- .../lib/src/details/notification_parts.dart | 94 +++++++++++++++++++ .../lib/src/details/notification_row.dart | 2 +- .../lib/src/details/notification_text.dart | 38 -------- .../lib/src/details/xml/details.dart | 1 - .../lib/src/details/xml/image.dart | 2 +- .../lib/src/details/xml/row.dart | 4 +- .../lib/src/details/xml/text.dart | 2 +- 11 files changed, 101 insertions(+), 108 deletions(-) delete mode 100644 flutter_local_notifications_windows/lib/src/details/notification_image.dart delete mode 100644 flutter_local_notifications_windows/lib/src/details/notification_part.dart create mode 100644 flutter_local_notifications_windows/lib/src/details/notification_parts.dart delete mode 100644 flutter_local_notifications_windows/lib/src/details/notification_text.dart diff --git a/flutter_local_notifications_windows/lib/src/details.dart b/flutter_local_notifications_windows/lib/src/details.dart index ec48637f0..a69027094 100644 --- a/flutter_local_notifications_windows/lib/src/details.dart +++ b/flutter_local_notifications_windows/lib/src/details.dart @@ -3,12 +3,10 @@ export 'details/notification_action.dart'; export 'details/notification_audio.dart'; export 'details/notification_details.dart'; export 'details/notification_header.dart'; -export 'details/notification_image.dart'; export 'details/notification_input.dart'; -export 'details/notification_part.dart'; +export 'details/notification_parts.dart'; export 'details/notification_progress.dart'; export 'details/notification_row.dart'; -export 'details/notification_text.dart'; /// The result of updating a notification. enum NotificationUpdateResult { diff --git a/flutter_local_notifications_windows/lib/src/details/notification_details.dart b/flutter_local_notifications_windows/lib/src/details/notification_details.dart index 5b302dee4..255224d5d 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_details.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_details.dart @@ -1,13 +1,12 @@ import 'notification_action.dart'; import 'notification_audio.dart'; import 'notification_header.dart'; -import 'notification_image.dart'; import 'notification_input.dart'; +import 'notification_parts.dart'; import 'notification_progress.dart'; import 'notification_row.dart'; -export 'notification_part.dart'; -export 'notification_text.dart'; +export 'notification_parts.dart'; /// The duration for a Windows notification. enum WindowsNotificationDuration { diff --git a/flutter_local_notifications_windows/lib/src/details/notification_image.dart b/flutter_local_notifications_windows/lib/src/details/notification_image.dart deleted file mode 100644 index c367299b2..000000000 --- a/flutter_local_notifications_windows/lib/src/details/notification_image.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'dart:io'; - -import 'notification_part.dart'; - -/// Where a Windows notification image can be placed. -enum WindowsImagePlacement { - /// The image replaces the app logo. - appLogoOverride, - - /// The image is shown on top of the notification body. - hero, -} - -/// How a Windows notification image can be cropped. -enum WindowsImageCrop { - /// The image is cropped into a circle. - circle, -} - -/// An image in a Windows notification. -class WindowsImage extends WindowsNotificationPart { - /// Creates a Windows notification image. - const WindowsImage.file( - this.file, { - required this.altText, - this.addQueryParams = false, - this.placement, - this.crop, - }); - - /// Whether Windows should add URL query parameters when fetching the image. - final bool addQueryParams; - - /// A description of the image to be used by assistive technology. - final String altText; - - /// The source of the image. - final File file; - - /// Where this image will be placed. Null indicates below the notification. - final WindowsImagePlacement? placement; - - /// How the image will be cropped. Null indicates uncropped. - final WindowsImageCrop? crop; -} diff --git a/flutter_local_notifications_windows/lib/src/details/notification_part.dart b/flutter_local_notifications_windows/lib/src/details/notification_part.dart deleted file mode 100644 index d409b4ae9..000000000 --- a/flutter_local_notifications_windows/lib/src/details/notification_part.dart +++ /dev/null @@ -1,12 +0,0 @@ -/// A text or image element in a Windows notification. -/// -/// Note: This should not be used for anything else as notification -/// groups can only contain text and images. -// This class needs to be abstract so [WindowsNotificationText] and -// [WindowsImage] can extend it. Specifically, this class is a marker -// type for classes that are valid as part of a [WindowsColumn]. -// ignore: one_member_abstracts -abstract class WindowsNotificationPart { - /// A const constructor. - const WindowsNotificationPart(); -} diff --git a/flutter_local_notifications_windows/lib/src/details/notification_parts.dart b/flutter_local_notifications_windows/lib/src/details/notification_parts.dart new file mode 100644 index 000000000..59f0ea469 --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/details/notification_parts.dart @@ -0,0 +1,94 @@ +import 'dart:io'; + +/// A text or image element in a Windows notification. +/// +/// Note: This should not be used for anything else as notification +/// groups can only contain text and images. +// This class needs to be abstract so [WindowsNotificationText] and +// [WindowsImage] can extend it. Specifically, this class is a marker +// type for classes that are valid as part of a [WindowsColumn]. +// ignore: one_member_abstracts +sealed class WindowsNotificationPart { + /// A const constructor. + const WindowsNotificationPart(); +} + +/// Where a Windows notification image can be placed. +enum WindowsImagePlacement { + /// The image replaces the app logo. + appLogoOverride, + + /// The image is shown on top of the notification body. + hero, +} + +/// How a Windows notification image can be cropped. +enum WindowsImageCrop { + /// The image is cropped into a circle. + circle, +} + +/// An image in a Windows notification. +class WindowsImage extends WindowsNotificationPart { + /// Creates a Windows notification image. + const WindowsImage.file( + this.file, { + required this.altText, + this.addQueryParams = false, + this.placement, + this.crop, + }); + + /// Whether Windows should add URL query parameters when fetching the image. + final bool addQueryParams; + + /// A description of the image to be used by assistive technology. + final String altText; + + /// The source of the image. + final File file; + + /// Where this image will be placed. Null indicates below the notification. + final WindowsImagePlacement? placement; + + /// How the image will be cropped. Null indicates uncropped. + final WindowsImageCrop? crop; +} + + +/// Where text can be placed in a Windows notification. +enum WindowsTextPlacement { + /// Shown at the bottom of the notification body in smaller text. + attribution, +} + +/// Text in a Windows notification. +/// +/// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-text +class WindowsNotificationText extends WindowsNotificationPart { + /// Creates text for a Windows notification. + const WindowsNotificationText({ + required this.text, + this.centerIfCall = false, + this.isCaption = false, + this.placement, + this.languageCode, + }); + + /// The text being displayed. + final String text; + + /// Whether to center this text. Only relevant if in an incoming call. + final bool centerIfCall; + + /// Whether the text should be smaller like a caption. + final bool isCaption; + + /// The placement of this text. + /// + /// The default placement (null) is in the main body of the notification. + final WindowsTextPlacement? placement; + + /// The language of this text. + final String? languageCode; +} diff --git a/flutter_local_notifications_windows/lib/src/details/notification_row.dart b/flutter_local_notifications_windows/lib/src/details/notification_row.dart index 8edee9648..8e3f6a9f6 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_row.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_row.dart @@ -1,4 +1,4 @@ -import 'notification_part.dart'; +import 'notification_parts.dart'; /// A group of notification content that must be displayed as a whole row. /// diff --git a/flutter_local_notifications_windows/lib/src/details/notification_text.dart b/flutter_local_notifications_windows/lib/src/details/notification_text.dart deleted file mode 100644 index 9437d5bb9..000000000 --- a/flutter_local_notifications_windows/lib/src/details/notification_text.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'notification_part.dart'; - -/// Where text can be placed in a Windows notification. -enum WindowsTextPlacement { - /// Shown at the bottom of the notification body in smaller text. - attribution, -} - -/// Text in a Windows notification. -/// -/// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-text -class WindowsNotificationText extends WindowsNotificationPart { - /// Creates text for a Windows notification. - const WindowsNotificationText({ - required this.text, - this.centerIfCall = false, - this.isCaption = false, - this.placement, - this.languageCode, - }); - - /// The text being displayed. - final String text; - - /// Whether to center this text. Only relevant if in an incoming call. - final bool centerIfCall; - - /// Whether the text should be smaller like a caption. - final bool isCaption; - - /// The placement of this text. - /// - /// The default placement (null) is in the main body of the notification. - final WindowsTextPlacement? placement; - - /// The language of this text. - final String? languageCode; -} diff --git a/flutter_local_notifications_windows/lib/src/details/xml/details.dart b/flutter_local_notifications_windows/lib/src/details/xml/details.dart index accc46f3d..81e166783 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/details.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/details.dart @@ -2,7 +2,6 @@ import 'package:xml/xml.dart'; import '../notification_action.dart'; import '../notification_details.dart'; -import '../notification_image.dart'; import '../notification_input.dart'; import '../notification_progress.dart'; import '../notification_row.dart'; diff --git a/flutter_local_notifications_windows/lib/src/details/xml/image.dart b/flutter_local_notifications_windows/lib/src/details/xml/image.dart index ac9fef5ea..e652f3db5 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/image.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/image.dart @@ -1,6 +1,6 @@ import 'package:xml/xml.dart'; -import '../notification_image.dart'; +import '../notification_parts.dart'; /// Converts a [WindowsImage] to XML extension ImageToXml on WindowsImage { diff --git a/flutter_local_notifications_windows/lib/src/details/xml/row.dart b/flutter_local_notifications_windows/lib/src/details/xml/row.dart index fe7f57b5a..998533ee8 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/row.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/row.dart @@ -1,9 +1,7 @@ import 'package:xml/xml.dart'; -import '../notification_image.dart'; -import '../notification_part.dart'; +import '../notification_parts.dart'; import '../notification_row.dart'; -import '../notification_text.dart'; import 'image.dart'; import 'text.dart'; diff --git a/flutter_local_notifications_windows/lib/src/details/xml/text.dart b/flutter_local_notifications_windows/lib/src/details/xml/text.dart index 93fe1de37..eed381971 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/text.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/text.dart @@ -1,6 +1,6 @@ import 'package:xml/xml.dart'; -import '../notification_text.dart'; +import '../notification_parts.dart'; /// Converts a [WindowsNotificationText] to XML extension TextToXml on WindowsNotificationText { From aad7293021604ebe4714ebcaf6a2d0e7e61efd14 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Mon, 26 Aug 2024 14:48:15 -0400 Subject: [PATCH 088/112] Restrict tests to Windows platforms --- flutter_local_notifications_windows/dart_test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter_local_notifications_windows/dart_test.yaml b/flutter_local_notifications_windows/dart_test.yaml index 96d9ec924..0675cf5ba 100644 --- a/flutter_local_notifications_windows/dart_test.yaml +++ b/flutter_local_notifications_windows/dart_test.yaml @@ -1 +1,2 @@ platforms: [vm] +test_on: windows From 8411647d252aa35061396c84146cec5493782aeb Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 4 Sep 2024 14:38:33 -0400 Subject: [PATCH 089/112] Update CI to use windows-latest for Windows tests + build --- .github/workflows/validate.yml | 70 +++++++++++++++---- flutter_local_notifications/pubspec.yaml | 4 +- .../dart_test.yaml | 1 + .../pubspec.yaml | 2 +- melos.yaml | 7 ++ 5 files changed, 66 insertions(+), 18 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d6cde2e5e..c8aa1ff52 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -77,15 +77,15 @@ jobs: run: ./.github/workflows/scripts/install-tools.sh - name: Build run: melos run build:example_android - build_example_android_3_13: - name: Build Android example app (3.16) + build_example_android_3_19: + name: Build Android example app (3.19) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.16.0 + flutter-version: 3.19.0 cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - name: Install Tools @@ -106,15 +106,15 @@ jobs: run: ./.github/workflows/scripts/install-tools.sh - name: Build run: melos run build:example_ios - build_example_ios_3_13: - name: Build iOS example app (3.16) + build_example_ios_3_19: + name: Build iOS example app (3.19) runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.16.0 + flutter-version: 3.19.0 cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - name: Install Tools @@ -135,15 +135,15 @@ jobs: run: ./.github/workflows/scripts/install-tools.sh - name: Build run: melos run build:example_macos - build_example_macos_3_13: - name: Build macOS example app (3.16) + build_example_macos_3_19: + name: Build macOS example app (3.19) runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.16.0 + flutter-version: 3.19.0 cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - name: Install Tools @@ -168,15 +168,15 @@ jobs: - run: flutter config --enable-linux-desktop - name: Build run: melos run build:example_linux - build_example_linux_3_13: - name: Build Linux example app (3.16) + build_example_linux_3_19: + name: Build Linux example app (3.19) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.16.0 + flutter-version: 3.19.0 cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - name: Install Tools @@ -187,6 +187,35 @@ jobs: - run: flutter config --enable-linux-desktop - name: Build run: melos run build:example_linux + build_example_windows_stable: + name: Build Windows example app (stable channel) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' + - name: Install Tools + run: ./.github/workflows/scripts/install-tools.sh + - name: Build + run: melos run build:example_windows + build_example_windows_3_19: + name: Build Linux example app (3.19) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: 3.19.0 + cache: true + cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' + - name: Install Tools + run: ./.github/workflows/scripts/install-tools.sh + - name: Build + run: melos run build:example_windows unit_tests_dart: name: Run all unit tests (Dart) runs-on: ubuntu-latest @@ -215,6 +244,20 @@ jobs: run: ./.github/workflows/scripts/install-tools.sh - name: Run Tests run: melos run test:unit:android + unit_tests_windows: + name: Run all unit tests (Windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' + - name: Install tools + run: ./github/workflows/scripts/install-tools.sh + - name: Run Tests + run: melos run test:unit:windows integration_tests_android: name: Run integration tests (Android) runs-on: ubuntu-latest @@ -261,6 +304,3 @@ jobs: brew install applesimutils applesimutils --byId ${{ steps.simulator-action.outputs.udid}} --bundle com.dexterous.flutterLocalNotificationsExample --setPermissions notifications=YES - run: melos run test:integration - - - diff --git a/flutter_local_notifications/pubspec.yaml b/flutter_local_notifications/pubspec.yaml index 62509fc1b..796acb785 100644 --- a/flutter_local_notifications/pubspec.yaml +++ b/flutter_local_notifications/pubspec.yaml @@ -39,5 +39,5 @@ flutter: default_package: flutter_local_notifications_windows environment: - sdk: ^3.2.0 - flutter: ">=3.16.0" + sdk: ^3.3.0 + flutter: ">=3.19.0" diff --git a/flutter_local_notifications_windows/dart_test.yaml b/flutter_local_notifications_windows/dart_test.yaml index 0675cf5ba..2305630e4 100644 --- a/flutter_local_notifications_windows/dart_test.yaml +++ b/flutter_local_notifications_windows/dart_test.yaml @@ -1,2 +1,3 @@ platforms: [vm] test_on: windows +retry: 5 # These tests have concurrency issues. See bin/crash.dart diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index 54099a548..ad50d8672 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -3,7 +3,7 @@ description: "A new Flutter FFI plugin project." version: 1.0.0 environment: - sdk: ">=3.2.0 <4.0.0" + sdk: ">=3.3.0 <4.0.0" dependencies: ffi: ^2.1.2 diff --git a/melos.yaml b/melos.yaml index f6c544c94..b20942f60 100644 --- a/melos.yaml +++ b/melos.yaml @@ -22,6 +22,7 @@ scripts: description: Run unit tests in a specific package. run: melos exec -c 1 -- "flutter test" packageFilters: + ignore: '*_windows' dirExists: - test test:unit:android: @@ -29,10 +30,16 @@ scripts: run: melos exec -c 1 -- "flutter build apk --debug && cd android && ./gradlew flutter_local_notifications:testDebug" packageFilters: scope: "*example*" + test:unit:windows: + description: Runs Windows-specific unit tests + run: melos exec -c 1 -- "dart test" + packageFilters: + scope: '*_windows' test:integration: run: melos exec -c 1 -- "flutter test integration_test" description: Run integration tests packageFilters: + ignore: '*_windows' dirExists: - integration_test scope: "*example*" From aa4404946c14d30fae2a7910072cfff8de0bc094 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 4 Sep 2024 14:39:45 -0400 Subject: [PATCH 090/112] Formatted repo --- .../example/lib/main.dart | 228 ++++----- .../example/lib/padded_button.dart | 12 +- .../example/lib/plugin.dart | 2 +- .../example/lib/repeating.dart | 134 +++-- .../example/lib/windows.dart | 456 +++++++++--------- .../flutter_local_notifications_plugin.dart | 20 +- .../platform_flutter_local_notifications.dart | 49 +- .../lib/src/details/notification_audio.dart | 24 +- .../lib/src/details/notification_parts.dart | 1 - .../lib/src/details/notification_to_xml.dart | 4 +- .../lib/src/details/xml/action.dart | 5 +- .../lib/src/details/xml/audio.dart | 14 +- .../lib/src/details/xml/details.dart | 18 +- .../lib/src/details/xml/header.dart | 16 +- .../lib/src/details/xml/input.dart | 56 +-- .../lib/src/details/xml/progress.dart | 22 +- .../lib/src/details/xml/row.dart | 38 +- .../lib/src/details/xml/text.dart | 20 +- .../lib/src/ffi/utils.dart | 26 +- .../lib/src/plugin/base.dart | 12 +- .../lib/src/plugin/ffi.dart | 340 +++++++------ .../lib/src/plugin/stub.dart | 11 +- .../test/bindings_test.dart | 80 +-- .../test/details_test.dart | 455 ++++++++--------- .../test/plugin_test.dart | 2 +- .../test/scheduled_test.dart | 2 +- .../test/xml_test.dart | 2 +- 27 files changed, 1036 insertions(+), 1013 deletions(-) diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 20a5a6f9a..77232ca19 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -90,52 +90,52 @@ Future main() async { await _configureLocalTimeZone(); const AndroidInitializationSettings initializationSettingsAndroid = - AndroidInitializationSettings('app_icon'); + AndroidInitializationSettings('app_icon'); final List darwinNotificationCategories = - [ - DarwinNotificationCategory( - darwinNotificationCategoryText, - actions: [ - DarwinNotificationAction.text( - 'text_1', - 'Action 1', - buttonTitle: 'Send', - placeholder: 'Placeholder', - ), - ], - ), - DarwinNotificationCategory( - darwinNotificationCategoryPlain, - actions: [ - DarwinNotificationAction.plain('id_1', 'Action 1'), - DarwinNotificationAction.plain( - 'id_2', - 'Action 2 (destructive)', - options: { - DarwinNotificationActionOption.destructive, - }, - ), - DarwinNotificationAction.plain( - navigationActionId, - 'Action 3 (foreground)', - options: { - DarwinNotificationActionOption.foreground, - }, - ), - DarwinNotificationAction.plain( - 'id_4', - 'Action 4 (auth required)', - options: { - DarwinNotificationActionOption.authenticationRequired, - }, - ), - ], - options: { - DarwinNotificationCategoryOption.hiddenPreviewShowTitle, - }, - ) - ]; + [ + DarwinNotificationCategory( + darwinNotificationCategoryText, + actions: [ + DarwinNotificationAction.text( + 'text_1', + 'Action 1', + buttonTitle: 'Send', + placeholder: 'Placeholder', + ), + ], + ), + DarwinNotificationCategory( + darwinNotificationCategoryPlain, + actions: [ + DarwinNotificationAction.plain('id_1', 'Action 1'), + DarwinNotificationAction.plain( + 'id_2', + 'Action 2 (destructive)', + options: { + DarwinNotificationActionOption.destructive, + }, + ), + DarwinNotificationAction.plain( + navigationActionId, + 'Action 3 (foreground)', + options: { + DarwinNotificationActionOption.foreground, + }, + ), + DarwinNotificationAction.plain( + 'id_4', + 'Action 4 (auth required)', + options: { + DarwinNotificationActionOption.authenticationRequired, + }, + ), + ], + options: { + DarwinNotificationCategoryOption.hiddenPreviewShowTitle, + }, + ) + ]; /// Note: permissions aren't requested here just to demonstrate that can be /// done later @@ -147,10 +147,10 @@ Future main() async { onDidReceiveLocalNotification: (int id, String? title, String? body, String? payload) async { didReceiveLocalNotificationStream.add(ReceivedNotification( - id: id, - title: title, - body: body, - payload: payload, + id: id, + title: title, + body: body, + payload: payload, )); }, notificationCategories: darwinNotificationCategories, @@ -176,8 +176,8 @@ Future main() async { onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); - final NotificationAppLaunchDetails? notificationAppLaunchDetails = - !kIsWeb && Platform.isLinux + final NotificationAppLaunchDetails? notificationAppLaunchDetails = !kIsWeb && + Platform.isLinux ? null : await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); String initialRoute = HomePage.routeName; @@ -309,8 +309,9 @@ class _HomePageState extends State { Navigator.of(context, rootNavigator: true).pop(); await Navigator.of(context).push( MaterialPageRoute( - builder: (BuildContext context) => - SecondPage(receivedNotification.payload, data: receivedNotification.data), + builder: (BuildContext context) => SecondPage( + receivedNotification.payload, + data: receivedNotification.data), ), ); }, @@ -323,9 +324,11 @@ class _HomePageState extends State { } void _configureSelectNotificationSubject() { - selectNotificationStream.stream.listen((NotificationResponse? response) async { + selectNotificationStream.stream + .listen((NotificationResponse? response) async { await Navigator.of(context).push(MaterialPageRoute( - builder: (BuildContext context) => SecondPage(response?.payload, data: response?.data), + builder: (BuildContext context) => + SecondPage(response?.payload, data: response?.data), )); }); } @@ -460,8 +463,7 @@ class _HomePageState extends State { await _cancelAllNotifications(); }, ), - if (!Platform.isWindows) - ...repeating.examples(context), + if (!Platform.isWindows) ...repeating.examples(context), const Divider(), const Text( 'Notifications with actions', @@ -1097,25 +1099,25 @@ class _HomePageState extends State { ); final WindowsNotificationDetails windowsNotificationsDetails = - WindowsNotificationDetails( - subtitle: 'Click the three dots for another button', - actions: [ - const WindowsAction( - content: 'Text', - arguments: 'text', - ), - WindowsAction( - content: 'Image', - arguments: 'image', - image: File('icons/coworker.png').absolute, - ), - const WindowsAction( - content: 'Context', - arguments: 'context', - placement: WindowsActionPlacement.contextMenu, - ), - ], - ); + WindowsNotificationDetails( + subtitle: 'Click the three dots for another button', + actions: [ + const WindowsAction( + content: 'Text', + arguments: 'text', + ), + WindowsAction( + content: 'Image', + arguments: 'image', + image: File('icons/coworker.png').absolute, + ), + const WindowsAction( + content: 'Context', + arguments: 'context', + placement: WindowsActionPlacement.contextMenu, + ), + ], + ); final NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, @@ -1158,14 +1160,16 @@ class _HomePageState extends State { ); const WindowsNotificationDetails windowsNotificationDetails = - WindowsNotificationDetails( - actions: [ - WindowsAction(content: 'Send', arguments: 'send-reply', inputId: 'text'), - ], - inputs: [ - WindowsTextInput(id: 'text', title: 'Send a reply?', placeHolderContent: 'Message'), - ], - ); + WindowsNotificationDetails( + actions: [ + WindowsAction( + content: 'Send', arguments: 'send-reply', inputId: 'text'), + ], + inputs: [ + WindowsTextInput( + id: 'text', title: 'Send a reply?', placeHolderContent: 'Message'), + ], + ); const NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, @@ -1229,21 +1233,22 @@ class _HomePageState extends State { ); const WindowsNotificationDetails windowsNotificationDetails = - WindowsNotificationDetails( - actions: [ - WindowsAction(content: 'Submit', arguments: 'submit', inputId: 'choice'), - ], - inputs: [ - WindowsSelectionInput( - id: 'choice', - defaultItem: 'abc', - items: [ - WindowsSelection(id: 'abc', content: 'abc'), - WindowsSelection(id: 'def', content: 'def'), - ], - ), - ], - ); + WindowsNotificationDetails( + actions: [ + WindowsAction( + content: 'Submit', arguments: 'submit', inputId: 'choice'), + ], + inputs: [ + WindowsSelectionInput( + id: 'choice', + defaultItem: 'abc', + items: [ + WindowsSelection(id: 'abc', content: 'abc'), + WindowsSelection(id: 'def', content: 'def'), + ], + ), + ], + ); const NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, @@ -1375,9 +1380,10 @@ class _HomePageState extends State { sound: AssetsLinuxSound('sound/slow_spring_board.mp3'), ); final WindowsNotificationDetails windowsNotificationDetails = - WindowsNotificationDetails( - audio: WindowsNotificationAudio.preset(sound: WindowsNotificationSound.alarm5), - ); + WindowsNotificationDetails( + audio: WindowsNotificationAudio.preset( + sound: WindowsNotificationSound.alarm5), + ); final NotificationDetails notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, @@ -1463,7 +1469,7 @@ class _HomePageState extends State { presentSound: false, ); final WindowsNotificationDetails windowsDetails = - WindowsNotificationDetails(audio: WindowsNotificationAudio.silent()); + WindowsNotificationDetails(audio: WindowsNotificationAudio.silent()); final NotificationDetails notificationDetails = NotificationDetails( windows: windowsDetails, android: androidNotificationDetails, @@ -1486,7 +1492,7 @@ class _HomePageState extends State { presentSound: false, ); final WindowsNotificationDetails windowsDetails = - WindowsNotificationDetails(audio: WindowsNotificationAudio.silent()); + WindowsNotificationDetails(audio: WindowsNotificationAudio.silent()); final NotificationDetails notificationDetails = NotificationDetails( windows: windowsDetails, android: androidNotificationDetails, @@ -2749,13 +2755,15 @@ class _HomePageState extends State { ); } - Future? _showWindowsNotificationWithRawXml() => flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation() - ?.showRawXml( - id: id++, - xml: _windowsRawXmlController.text, - bindings: {'message': 'Hello, World!'}, - ); + Future? _showWindowsNotificationWithRawXml() => + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + FlutterLocalNotificationsWindows>() + ?.showRawXml( + id: id++, + xml: _windowsRawXmlController.text, + bindings: {'message': 'Hello, World!'}, + ); } Future _showLinuxNotificationWithBodyMarkup() async { diff --git a/flutter_local_notifications/example/lib/padded_button.dart b/flutter_local_notifications/example/lib/padded_button.dart index 254377061..5867d5e3e 100644 --- a/flutter_local_notifications/example/lib/padded_button.dart +++ b/flutter_local_notifications/example/lib/padded_button.dart @@ -12,10 +12,10 @@ class PaddedElevatedButton extends StatelessWidget { @override Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), - child: ElevatedButton( - onPressed: onPressed, - child: Text(buttonText), - ), - ); + padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), + child: ElevatedButton( + onPressed: onPressed, + child: Text(buttonText), + ), + ); } diff --git a/flutter_local_notifications/example/lib/plugin.dart b/flutter_local_notifications/example/lib/plugin.dart index 52a6d496a..b64f21809 100644 --- a/flutter_local_notifications/example/lib/plugin.dart +++ b/flutter_local_notifications/example/lib/plugin.dart @@ -1,6 +1,6 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); + FlutterLocalNotificationsPlugin(); int id = 0; diff --git a/flutter_local_notifications/example/lib/repeating.dart b/flutter_local_notifications/example/lib/repeating.dart index 2286daf5f..8eea896a4 100644 --- a/flutter_local_notifications/example/lib/repeating.dart +++ b/flutter_local_notifications/example/lib/repeating.dart @@ -6,72 +6,66 @@ import 'padded_button.dart'; import 'plugin.dart'; List examples(BuildContext context) => [ - const Divider(), - const Text( - 'Repeating notifications', - style: TextStyle(fontWeight: FontWeight.bold), - ), - PaddedElevatedButton( - buttonText: 'Repeat notification every minute', - onPressed: () async { - await _repeatNotification(); - }, - ), - PaddedElevatedButton( - buttonText: 'Repeat notification every 5 minutes', - onPressed: () async { - await _repeatPeriodicallyWithDurationNotification(); - }, - ), - PaddedElevatedButton( - buttonText: - 'Schedule daily 10:00:00 am notification in your ' - 'local time zone', - onPressed: () async { - await _scheduleDailyTenAMNotification(); - }, - ), - PaddedElevatedButton( - buttonText: - 'Schedule daily 10:00:00 am notification in your ' - "local time zone using last year's date", - onPressed: () async { - await _scheduleDailyTenAMLastYearNotification(); - }, - ), - PaddedElevatedButton( - buttonText: - 'Schedule weekly 10:00:00 am notification in your ' - 'local time zone', - onPressed: () async { - await _scheduleWeeklyTenAMNotification(); - }, - ), - PaddedElevatedButton( - buttonText: - 'Schedule weekly Monday 10:00:00 am notification ' - 'in your local time zone', - onPressed: () async { - await _scheduleWeeklyMondayTenAMNotification(); - }, - ), - PaddedElevatedButton( - buttonText: - 'Schedule monthly Monday 10:00:00 am notification in ' - 'your local time zone', - onPressed: () async { - await _scheduleMonthlyMondayTenAMNotification(); - }, - ), - PaddedElevatedButton( - buttonText: - 'Schedule yearly Monday 10:00:00 am notification in ' - 'your local time zone', - onPressed: () async { - await _scheduleYearlyMondayTenAMNotification(); - }, - ), -]; + const Divider(), + const Text( + 'Repeating notifications', + style: TextStyle(fontWeight: FontWeight.bold), + ), + PaddedElevatedButton( + buttonText: 'Repeat notification every minute', + onPressed: () async { + await _repeatNotification(); + }, + ), + PaddedElevatedButton( + buttonText: 'Repeat notification every 5 minutes', + onPressed: () async { + await _repeatPeriodicallyWithDurationNotification(); + }, + ), + PaddedElevatedButton( + buttonText: 'Schedule daily 10:00:00 am notification in your ' + 'local time zone', + onPressed: () async { + await _scheduleDailyTenAMNotification(); + }, + ), + PaddedElevatedButton( + buttonText: 'Schedule daily 10:00:00 am notification in your ' + "local time zone using last year's date", + onPressed: () async { + await _scheduleDailyTenAMLastYearNotification(); + }, + ), + PaddedElevatedButton( + buttonText: 'Schedule weekly 10:00:00 am notification in your ' + 'local time zone', + onPressed: () async { + await _scheduleWeeklyTenAMNotification(); + }, + ), + PaddedElevatedButton( + buttonText: 'Schedule weekly Monday 10:00:00 am notification ' + 'in your local time zone', + onPressed: () async { + await _scheduleWeeklyMondayTenAMNotification(); + }, + ), + PaddedElevatedButton( + buttonText: 'Schedule monthly Monday 10:00:00 am notification in ' + 'your local time zone', + onPressed: () async { + await _scheduleMonthlyMondayTenAMNotification(); + }, + ), + PaddedElevatedButton( + buttonText: 'Schedule yearly Monday 10:00:00 am notification in ' + 'your local time zone', + onPressed: () async { + await _scheduleYearlyMondayTenAMNotification(); + }, + ), + ]; /// To test we don't validate past dates when using `matchDateTimeComponents` Future _scheduleDailyTenAMLastYearNotification() async { @@ -81,8 +75,8 @@ Future _scheduleDailyTenAMLastYearNotification() async { 'daily scheduled notification body', _nextInstanceOfTenAMLastYear(), const NotificationDetails( - android: AndroidNotificationDetails('daily notification channel id', - 'daily notification channel name', + android: AndroidNotificationDetails( + 'daily notification channel id', 'daily notification channel name', channelDescription: 'daily notification description'), ), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, @@ -200,8 +194,8 @@ Future _scheduleDailyTenAMNotification() async { 'daily scheduled notification body', _nextInstanceOfTenAM(), const NotificationDetails( - android: AndroidNotificationDetails('daily notification channel id', - 'daily notification channel name', + android: AndroidNotificationDetails( + 'daily notification channel id', 'daily notification channel name', channelDescription: 'daily notification description'), ), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index 3452baf0b..c80a4e70d 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -7,7 +7,8 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'padded_button.dart'; import 'plugin.dart'; -const WindowsInitializationSettings initSettings = WindowsInitializationSettings( +const WindowsInitializationSettings initSettings = + WindowsInitializationSettings( appName: 'Flutter Local Notifications Example', appUserModelId: 'Com.Dexterous.FlutterLocalNotificationsExample', guid: 'd49b0314-ee7a-4626-bf79-97cdb8a991bb', @@ -16,97 +17,100 @@ const WindowsInitializationSettings initSettings = WindowsInitializationSettings List examples({ required TextEditingController xmlController, required VoidCallback showXmlNotification, -}) => [ - const Text('Windows-specific examples', - style: TextStyle(fontWeight: FontWeight.bold), - ), - PaddedElevatedButton( - buttonText: 'Show short and long notifications notification', - onPressed: () async { - await _showWindowsNotificationWithDuration(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show different scenarios', - onPressed: () async { - await _showWindowsNotificationWithScenarios(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notifications with some detail', - onPressed: () async { - await _showWindowsNotificationWithDetails(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notifications with image', - onPressed: () async { - await _showWindowsNotificationWithImages(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notifications with columns', - onPressed: () async { - await _showWindowsNotificationWithGroups(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notifications with progress bar', - onPressed: () async { - await _showWindowsNotificationWithProgress(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notifications with dynamic content', - onPressed: () async { - await _showWindowsNotificationWithDynamic(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notification with activation', - onPressed: () async { - await _showWindowsNotificationWithActivation(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notification with button styles', - onPressed: () async { - await _showWindowsNotificationWithButtonStyle(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notifications in a group', - onPressed: () async { - await _showWindowsNotificationWithHeader(); - }, - ), - PaddedElevatedButton( - buttonText: 'Show notification with raw XML', - onPressed: showXmlNotification, - ), - const SizedBox(height: 8), - SizedBox( - width: 500, - child: ExpansionTile( - title: const Text('Click to expand raw XML'), - children: [TextField( - maxLines: 20, - style: const TextStyle(fontFamily: 'RobotoMono'), - controller: xmlController, - decoration: InputDecoration( - hintText: 'Enter the raw xml', - helperText: 'Bindings: {message} --> Hello, World!', - constraints: const BoxConstraints.tightFor( - width: 600, height: 480), - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () => xmlController.clear(), - ), - ), - ),] - ), - ), -]; +}) => + [ + const Text( + 'Windows-specific examples', + style: TextStyle(fontWeight: FontWeight.bold), + ), + PaddedElevatedButton( + buttonText: 'Show short and long notifications notification', + onPressed: () async { + await _showWindowsNotificationWithDuration(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show different scenarios', + onPressed: () async { + await _showWindowsNotificationWithScenarios(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with some detail', + onPressed: () async { + await _showWindowsNotificationWithDetails(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with image', + onPressed: () async { + await _showWindowsNotificationWithImages(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with columns', + onPressed: () async { + await _showWindowsNotificationWithGroups(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with progress bar', + onPressed: () async { + await _showWindowsNotificationWithProgress(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications with dynamic content', + onPressed: () async { + await _showWindowsNotificationWithDynamic(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification with activation', + onPressed: () async { + await _showWindowsNotificationWithActivation(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification with button styles', + onPressed: () async { + await _showWindowsNotificationWithButtonStyle(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notifications in a group', + onPressed: () async { + await _showWindowsNotificationWithHeader(); + }, + ), + PaddedElevatedButton( + buttonText: 'Show notification with raw XML', + onPressed: showXmlNotification, + ), + const SizedBox(height: 8), + SizedBox( + width: 500, + child: ExpansionTile( + title: const Text('Click to expand raw XML'), + children: [ + TextField( + maxLines: 20, + style: const TextStyle(fontFamily: 'RobotoMono'), + controller: xmlController, + decoration: InputDecoration( + hintText: 'Enter the raw xml', + helperText: 'Bindings: {message} --> Hello, World!', + constraints: + const BoxConstraints.tightFor(width: 600, height: 480), + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () => xmlController.clear(), + ), + ), + ), + ]), + ), + ]; Future _showWindowsNotificationWithDuration() async { await flutterLocalNotificationsPlugin.show( @@ -114,7 +118,8 @@ Future _showWindowsNotificationWithDuration() async { 'This is a short notification', 'This will last about 7 seconds', const NotificationDetails( - windows: WindowsNotificationDetails(duration: WindowsNotificationDuration.short), + windows: WindowsNotificationDetails( + duration: WindowsNotificationDuration.short), ), ); await flutterLocalNotificationsPlugin.show( @@ -122,7 +127,8 @@ Future _showWindowsNotificationWithDuration() async { 'This is a long notification', 'This will last about 25 seconds', const NotificationDetails( - windows: WindowsNotificationDetails(duration: WindowsNotificationDuration.long), + windows: WindowsNotificationDetails( + duration: WindowsNotificationDuration.long), ), ); } @@ -134,11 +140,10 @@ Future _showWindowsNotificationWithScenarios() async { null, const NotificationDetails( windows: WindowsNotificationDetails( - scenario: WindowsNotificationScenario.alarm, - actions: [ - WindowsAction(content: 'Button', arguments: 'button') - ] - ), + scenario: WindowsNotificationScenario.alarm, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ]), ), ); await flutterLocalNotificationsPlugin.show( @@ -147,11 +152,10 @@ Future _showWindowsNotificationWithScenarios() async { null, const NotificationDetails( windows: WindowsNotificationDetails( - scenario: WindowsNotificationScenario.incomingCall, - actions: [ - WindowsAction(content: 'Button', arguments: 'button') - ] - ), + scenario: WindowsNotificationScenario.incomingCall, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ]), ), ); await flutterLocalNotificationsPlugin.show( @@ -160,11 +164,10 @@ Future _showWindowsNotificationWithScenarios() async { null, const NotificationDetails( windows: WindowsNotificationDetails( - scenario: WindowsNotificationScenario.reminder, - actions: [ - WindowsAction(content: 'Button', arguments: 'button') - ] - ), + scenario: WindowsNotificationScenario.reminder, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ]), ), ); await flutterLocalNotificationsPlugin.show( @@ -173,74 +176,85 @@ Future _showWindowsNotificationWithScenarios() async { null, const NotificationDetails( windows: WindowsNotificationDetails( - scenario: WindowsNotificationScenario.urgent, - actions: [ - WindowsAction(content: 'Button', arguments: 'button') - ] - ), + scenario: WindowsNotificationScenario.urgent, + actions: [ + WindowsAction(content: 'Button', arguments: 'button') + ]), ), ); } -Future _showWindowsNotificationWithDetails() => flutterLocalNotificationsPlugin.show( - id++, - 'This one has more details', - 'And a different timestamp!', - NotificationDetails( - windows: WindowsNotificationDetails( - subtitle: 'This is the subtitle', - timestamp: DateTime.now().subtract(const Duration(hours: 2, minutes: 5)), - ), - ), -); +Future _showWindowsNotificationWithDetails() => + flutterLocalNotificationsPlugin.show( + id++, + 'This one has more details', + 'And a different timestamp!', + NotificationDetails( + windows: WindowsNotificationDetails( + subtitle: 'This is the subtitle', + timestamp: + DateTime.now().subtract(const Duration(hours: 2, minutes: 5)), + ), + ), + ); -Future _showWindowsNotificationWithImages() => flutterLocalNotificationsPlugin.show( - id++, - 'This notification has an image', - 'You can only show images from files', - NotificationDetails( - windows: WindowsNotificationDetails( - images: [ - WindowsImage.file( - File('./icons/4.0x/app_icon_density.png').absolute, - altText: 'A beautiful image', +Future _showWindowsNotificationWithImages() => + flutterLocalNotificationsPlugin.show( + id++, + 'This notification has an image', + 'You can only show images from files', + NotificationDetails( + windows: WindowsNotificationDetails( + images: [ + WindowsImage.file( + File('./icons/4.0x/app_icon_density.png').absolute, + altText: 'A beautiful image', + ), + ], ), - ], - ), - ), -); + ), + ); -Future _showWindowsNotificationWithGroups() => flutterLocalNotificationsPlugin.show( - id++, - 'This notification has many groups', - 'Each group stays together', - NotificationDetails( - windows: WindowsNotificationDetails( - subtitle: 'Caption text is fainter', - rows: [ - WindowsRow([ - WindowsColumn([ - WindowsImage.file(File('icons/coworker.png').absolute, altText: 'A coworker'), - const WindowsNotificationText(text: 'A coworker', isCaption: true), - ]), - WindowsColumn([ - WindowsImage.file(File('icons/4.0x/app_icon_density.png').absolute, altText: 'The icon'), - const WindowsNotificationText(text: 'The icon'), - ]), - ]), - ], - ), - ), -); +Future _showWindowsNotificationWithGroups() => + flutterLocalNotificationsPlugin.show( + id++, + 'This notification has many groups', + 'Each group stays together', + NotificationDetails( + windows: WindowsNotificationDetails( + subtitle: 'Caption text is fainter', + rows: [ + WindowsRow([ + WindowsColumn([ + WindowsImage.file(File('icons/coworker.png').absolute, + altText: 'A coworker'), + const WindowsNotificationText( + text: 'A coworker', isCaption: true), + ]), + WindowsColumn([ + WindowsImage.file( + File('icons/4.0x/app_icon_density.png').absolute, + altText: 'The icon'), + const WindowsNotificationText(text: 'The icon'), + ]), + ]), + ], + ), + ), + ); Future _showWindowsNotificationWithProgress() async { - final WindowsProgressBar fastProgress = - WindowsProgressBar(id: 'fast-progress', status: 'Updating quickly...', value: 0); - final WindowsProgressBar slowProgress = - WindowsProgressBar(id: 'slow-progress', status: 'Updating slowly...', value: 0, label: '0 / 10'); + final WindowsProgressBar fastProgress = WindowsProgressBar( + id: 'fast-progress', status: 'Updating quickly...', value: 0); + final WindowsProgressBar slowProgress = WindowsProgressBar( + id: 'slow-progress', + status: 'Updating slowly...', + value: 0, + label: '0 / 10'); final int notificationId = id++; final FlutterLocalNotificationsWindows? windows = - flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation(); + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< + FlutterLocalNotificationsWindows>(); await flutterLocalNotificationsPlugin.show( notificationId, 'This notification has progress bars', @@ -261,12 +275,11 @@ Future _showWindowsNotificationWithProgress() async { value: 0.75, ), WindowsProgressBar( - id: 'discrete', - title: 'This has discrete progress', - status: 'Syncing...', - value: 0.75, - label: '9/12 complete' - ), + id: 'discrete', + title: 'This has discrete progress', + status: 'Syncing...', + value: 0.75, + label: '9/12 complete'), fastProgress, slowProgress, ], @@ -285,8 +298,10 @@ Future _showWindowsNotificationWithProgress() async { } count = count.clamp(0, 50); slowProgress.label = '$count / 50'; - await windows?.updateProgressBar(notificationId: notificationId, progressBar: fastProgress); - await windows?.updateProgressBar(notificationId: notificationId, progressBar: slowProgress); + await windows?.updateProgressBar( + notificationId: notificationId, progressBar: fastProgress); + await windows?.updateProgressBar( + notificationId: notificationId, progressBar: slowProgress); }); } @@ -294,8 +309,8 @@ Future _showWindowsNotificationWithDynamic() async { final DateTime start = DateTime.now(); final int notificationId = id++; final FlutterLocalNotificationsWindows? windows = - flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation(); + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< + FlutterLocalNotificationsWindows>(); await flutterLocalNotificationsPlugin.show( notificationId, 'Dynamic content', @@ -307,8 +322,9 @@ Future _showWindowsNotificationWithDynamic() async { ), ); Map getBindings() => { - 'stopwatch': 'Elapsed time: ${DateTime.now().difference(start).inSeconds} seconds', - }; + 'stopwatch': + 'Elapsed time: ${DateTime.now().difference(start).inSeconds} seconds', + }; await windows?.updateBindings(id: notificationId, bindings: getBindings()); Timer.periodic(const Duration(seconds: 1), (Timer timer) async { if (timer.tick > 10) { @@ -320,50 +336,52 @@ Future _showWindowsNotificationWithDynamic() async { }); } -Future _showWindowsNotificationWithActivation() => flutterLocalNotificationsPlugin.show( - id++, - 'These buttons do different things', - 'Click on each one!', - const NotificationDetails( - windows: WindowsNotificationDetails( - actions: [ - WindowsAction( - content: 'Loading', - arguments: 'loading', - activationBehavior: WindowsNotificationBehavior.pendingUpdate, - ), - WindowsAction( - content: 'Google', - arguments: 'https://google.com', - activationType: WindowsActivationType.protocol, - activationBehavior: WindowsNotificationBehavior.pendingUpdate, +Future _showWindowsNotificationWithActivation() => + flutterLocalNotificationsPlugin.show( + id++, + 'These buttons do different things', + 'Click on each one!', + const NotificationDetails( + windows: WindowsNotificationDetails( + actions: [ + WindowsAction( + content: 'Loading', + arguments: 'loading', + activationBehavior: WindowsNotificationBehavior.pendingUpdate, + ), + WindowsAction( + content: 'Google', + arguments: 'https://google.com', + activationType: WindowsActivationType.protocol, + activationBehavior: WindowsNotificationBehavior.pendingUpdate, + ), + ], ), - ], - ), - ), -); + ), + ); -Future _showWindowsNotificationWithButtonStyle() => flutterLocalNotificationsPlugin.show( - id++, - 'Incoming call', - 'Your best friend', - const NotificationDetails( - windows: WindowsNotificationDetails( - actions: [ - WindowsAction( - content: 'Accept', - arguments: 'accept', - buttonStyle: WindowsButtonStyle.success, - ), - WindowsAction( - content: 'Reject', - arguments: 'reject', - buttonStyle: WindowsButtonStyle.critical, +Future _showWindowsNotificationWithButtonStyle() => + flutterLocalNotificationsPlugin.show( + id++, + 'Incoming call', + 'Your best friend', + const NotificationDetails( + windows: WindowsNotificationDetails( + actions: [ + WindowsAction( + content: 'Accept', + arguments: 'accept', + buttonStyle: WindowsButtonStyle.success, + ), + WindowsAction( + content: 'Reject', + arguments: 'reject', + buttonStyle: WindowsButtonStyle.critical, + ), + ], ), - ], - ), - ), -); + ), + ); Future _showWindowsNotificationWithHeader() async { const WindowsHeader header = WindowsHeader( diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index 2cd7d6554..4cd16d0e3 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -190,13 +190,12 @@ class FlutterLocalNotificationsPlugin { } else if (defaultTargetPlatform == TargetPlatform.windows) { if (initializationSettings.windows == null) { throw ArgumentError( - 'Windows settings must be set when targeting Windows platform.' - ); + 'Windows settings must be set when targeting Windows platform.'); } return await resolvePlatformSpecificImplementation< - FlutterLocalNotificationsWindows - >()?.initialize( + FlutterLocalNotificationsWindows>() + ?.initialize( initializationSettings.windows!, onNotificationReceived: onDidReceiveNotificationResponse, ); @@ -283,8 +282,7 @@ class FlutterLocalNotificationsPlugin { await resolvePlatformSpecificImplementation< FlutterLocalNotificationsWindows>() ?.show(id, title, body, - details: notificationDetails?.windows, - payload: payload); + details: notificationDetails?.windows, payload: payload); } else { await FlutterLocalNotificationsPlatform.instance.show(id, title, body); } @@ -394,9 +392,13 @@ class FlutterLocalNotificationsPlugin { matchDateTimeComponents: matchDateTimeComponents); } else if (defaultTargetPlatform == TargetPlatform.windows) { await resolvePlatformSpecificImplementation< - FlutterLocalNotificationsWindows - >()?.zonedSchedule( - id, title, body, scheduledDate, notificationDetails.windows, + FlutterLocalNotificationsWindows>() + ?.zonedSchedule( + id, + title, + body, + scheduledDate, + notificationDetails.windows, payload: payload, ); } else { diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 4be8dd44a..4696fa266 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -45,31 +45,34 @@ class MethodChannelFlutterLocalNotificationsPlugin Future cancelAll() => _channel.invokeMethod('cancelAll'); @override - Future getNotificationAppLaunchDetails( - ) async { + Future + getNotificationAppLaunchDetails() async { final Map? result = - await _channel.invokeMethod('getNotificationAppLaunchDetails'); + await _channel.invokeMethod('getNotificationAppLaunchDetails'); final Map? notificationResponse = - result != null && result.containsKey('notificationResponse') - ? result['notificationResponse'] : null; - return result == null ? null : NotificationAppLaunchDetails( - result['notificationLaunchedApp'], - notificationResponse: notificationResponse == null ? null - : NotificationResponse( - id: notificationResponse['notificationId'], - actionId: notificationResponse['actionId'], - input: notificationResponse['input'], - notificationResponseType: NotificationResponseType.values[ - notificationResponse['notificationResponseType']], - payload: notificationResponse.containsKey('payload') - ? notificationResponse['payload'] - : null, - data: Map.from( - notificationResponse['data'] - ?? {}, - ), - ), - ); + result != null && result.containsKey('notificationResponse') + ? result['notificationResponse'] + : null; + return result == null + ? null + : NotificationAppLaunchDetails( + result['notificationLaunchedApp'], + notificationResponse: notificationResponse == null + ? null + : NotificationResponse( + id: notificationResponse['notificationId'], + actionId: notificationResponse['actionId'], + input: notificationResponse['input'], + notificationResponseType: NotificationResponseType.values[ + notificationResponse['notificationResponseType']], + payload: notificationResponse.containsKey('payload') + ? notificationResponse['payload'] + : null, + data: Map.from( + notificationResponse['data'] ?? {}, + ), + ), + ); } @override diff --git a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart index 577fd338b..cd2c2e8a9 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart @@ -89,26 +89,24 @@ enum WindowsNotificationSound { /// Specifies custom audio to play during a notification. class WindowsNotificationAudio { /// No sound will play during this notification. - WindowsNotificationAudio.silent() : - source = WindowsNotificationSound.defaultSound.name, - shouldLoop = false, - isSilent = true; + WindowsNotificationAudio.silent() + : source = WindowsNotificationSound.defaultSound.name, + shouldLoop = false, + isSilent = true; /// Audio from a Windows preset. See [WindowsNotificationSound] for options. WindowsNotificationAudio.preset({ required WindowsNotificationSound sound, this.shouldLoop = false, - }) : isSilent = false, - source = sound.name; + }) : isSilent = false, + source = sound.name; /// Audio from a file. See [allowedSchemes] and [allowedExtensions]. WindowsNotificationAudio.fromFile({ required Uri file, this.shouldLoop = false, - }) : - isSilent = false, - source = file.toFilePath() - { + }) : isSilent = false, + source = file.toFilePath() { if (!allowedSchemes.contains(file.scheme)) { throw ArgumentError.value( file.toString(), @@ -116,10 +114,8 @@ class WindowsNotificationAudio { 'URI scheme must be one of the following schemes: $allowedSchemes', ); } - if ( - !file.filename.contains('.') - || !allowedExtensions.contains(file.extension) - ) { + if (!file.filename.contains('.') || + !allowedExtensions.contains(file.extension)) { throw ArgumentError.value( file.toString(), 'WindowsNotificationAudio.file', diff --git a/flutter_local_notifications_windows/lib/src/details/notification_parts.dart b/flutter_local_notifications_windows/lib/src/details/notification_parts.dart index 59f0ea469..43dc29d69 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_parts.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_parts.dart @@ -55,7 +55,6 @@ class WindowsImage extends WindowsNotificationPart { final WindowsImageCrop? crop; } - /// Where text can be placed in a Windows notification. enum WindowsTextPlacement { /// Shown at the bottom of the notification body in smaller text. diff --git a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart index 38747efcc..67184c1b1 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart @@ -42,6 +42,6 @@ String notificationToXml({ }, ); return builder - .buildDocument() - .toXmlString(pretty: true, indentAttribute: (_) => true); + .buildDocument() + .toXmlString(pretty: true, indentAttribute: (_) => true); } diff --git a/flutter_local_notifications_windows/lib/src/details/xml/action.dart b/flutter_local_notifications_windows/lib/src/details/xml/action.dart index 42b986adb..67a39d024 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/action.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/action.dart @@ -22,8 +22,9 @@ extension ActionToXml on WindowsAction { 'activationType': activationType.name, 'afterActivationBehavior': activationBehavior.name, if (placement != null) 'placement': placement!.name, - if (image != null) 'imageUri': - Uri.file(image!.absolute.path, windows: true).toFilePath(), + if (image != null) + 'imageUri': + Uri.file(image!.absolute.path, windows: true).toFilePath(), if (inputId != null) 'hint-inputId': inputId!, if (buttonStyle != null) 'hint-buttonStyle': buttonStyle!.name, if (tooltip != null) 'hint-toolTip': tooltip!, diff --git a/flutter_local_notifications_windows/lib/src/details/xml/audio.dart b/flutter_local_notifications_windows/lib/src/details/xml/audio.dart index d62539fc9..586a53f0d 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/audio.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/audio.dart @@ -7,11 +7,11 @@ extension AudioToXml on WindowsNotificationAudio { /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-audio void buildXml(XmlBuilder builder) => builder.element( - 'audio', - attributes: { - 'src': source, - 'silent': isSilent.toString(), - 'loop': shouldLoop.toString(), - }, - ); + 'audio', + attributes: { + 'src': source, + 'silent': isSilent.toString(), + 'loop': shouldLoop.toString(), + }, + ); } diff --git a/flutter_local_notifications_windows/lib/src/details/xml/details.dart b/flutter_local_notifications_windows/lib/src/details/xml/details.dart index 81e166783..98ba8d6a7 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/details.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/details.dart @@ -21,7 +21,7 @@ extension on DateTime { final String sign = offset.isNegative ? '-' : '+'; final String hours = offset.inHours.abs().toString().padLeft(2, '0'); final String minutes = - offset.inMinutes.abs().remainder(60).toString().padLeft(2, '0'); + offset.inMinutes.abs().remainder(60).toString().padLeft(2, '0'); final String offsetString = '$sign$hours:$minutes'; // Get first part of properly formatted ISO 8601 date final String formattedDate = toIso8601String().split('.').first; @@ -48,8 +48,10 @@ extension DetailsToXml on WindowsNotificationDetails { nest: () { for (final WindowsInput input in inputs) { switch (input) { - case WindowsTextInput(): input.buildXml(builder); - case WindowsSelectionInput(): input.buildXml(builder); + case WindowsTextInput(): + input.buildXml(builder); + case WindowsSelectionInput(): + input.buildXml(builder); } } for (final WindowsAction action in actions) { @@ -81,9 +83,9 @@ extension DetailsToXml on WindowsNotificationDetails { /// XML attributes for the toast notification as a whole. Map get attributes => { - if (duration != null) 'duration': duration!.name, - if (timestamp != null) - 'displayTimestamp': timestamp!.toIso8601StringTz(), - if (scenario != null) 'scenario': scenario!.name, - }; + if (duration != null) 'duration': duration!.name, + if (timestamp != null) + 'displayTimestamp': timestamp!.toIso8601StringTz(), + if (scenario != null) 'scenario': scenario!.name, + }; } diff --git a/flutter_local_notifications_windows/lib/src/details/xml/header.dart b/flutter_local_notifications_windows/lib/src/details/xml/header.dart index 12a8a66ea..b7de790d4 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/header.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/header.dart @@ -8,12 +8,12 @@ extension HeaderToXml on WindowsHeader { /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-header void buildXml(XmlBuilder builder) => builder.element( - 'header', - attributes: { - 'id': id, - 'title': title, - 'arguments': arguments, - if (activation != null) 'activationType': activation!.name, - }, - ); + 'header', + attributes: { + 'id': id, + 'title': title, + 'arguments': arguments, + if (activation != null) 'activationType': activation!.name, + }, + ); } diff --git a/flutter_local_notifications_windows/lib/src/details/xml/input.dart b/flutter_local_notifications_windows/lib/src/details/xml/input.dart index 55cce695c..407421a5a 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/input.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/input.dart @@ -8,15 +8,15 @@ extension TextInputToXml on WindowsTextInput { /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-input void buildXml(XmlBuilder builder) => builder.element( - 'input', - attributes: { - 'id': id, - 'type': type.name, - if (title != null) 'title': title!, - if (placeHolderContent != null) - 'placeHolderContent': placeHolderContent!, - }, - ); + 'input', + attributes: { + 'id': id, + 'type': type.name, + if (title != null) 'title': title!, + if (placeHolderContent != null) + 'placeHolderContent': placeHolderContent!, + }, + ); } /// Converts a [WindowsSelectionInput] to XML @@ -25,19 +25,19 @@ extension SelectionInputToXml on WindowsSelectionInput { /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-input void buildXml(XmlBuilder builder) => builder.element( - 'input', - attributes: { - 'id': id, - 'type': type.name, - if (title != null) 'title': title!, - if (defaultItem != null) 'defaultInput': defaultItem!, - }, - nest: () { - for (final WindowsSelection item in items) { - item.buildXml(builder); - } - }, - ); + 'input', + attributes: { + 'id': id, + 'type': type.name, + if (title != null) 'title': title!, + if (defaultItem != null) 'defaultInput': defaultItem!, + }, + nest: () { + for (final WindowsSelection item in items) { + item.buildXml(builder); + } + }, + ); } /// Converts a [WindowsSelection] to XML @@ -46,10 +46,10 @@ extension SelectionToXml on WindowsSelection { /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-selection void buildXml(XmlBuilder builder) => builder.element( - 'selection', - attributes: { - 'id': id, - 'content': content, - }, - ); + 'selection', + attributes: { + 'id': id, + 'content': content, + }, + ); } diff --git a/flutter_local_notifications_windows/lib/src/details/xml/progress.dart b/flutter_local_notifications_windows/lib/src/details/xml/progress.dart index 765d04efb..ee8ef441b 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/progress.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/progress.dart @@ -8,14 +8,14 @@ extension ProgressBarToXml on WindowsProgressBar { /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-progress void buildXml(XmlBuilder builder) => builder.element( - 'progress', - attributes: { - 'status': status, - 'value': '{$id-progressValue}', - if (title != null) 'title': title!, - if (label != null) 'valueStringOverride': '{$id-progressString}', - }, - ); + 'progress', + attributes: { + 'status': status, + 'value': '{$id-progressValue}', + if (title != null) 'title': title!, + if (label != null) 'valueStringOverride': '{$id-progressString}', + }, + ); /// The data bindings for this progress bar. /// @@ -24,7 +24,7 @@ extension ProgressBarToXml on WindowsProgressBar { /// dynamically later by calling /// [FlutterLocalNotificationsWindows.updateProgressBar]. Map get data => { - '$id-progressValue': value?.toString() ?? 'indeterminate', - if (label != null) '$id-progressString': label!, - }; + '$id-progressValue': value?.toString() ?? 'indeterminate', + if (label != null) '$id-progressString': label!, + }; } diff --git a/flutter_local_notifications_windows/lib/src/details/xml/row.dart b/flutter_local_notifications_windows/lib/src/details/xml/row.dart index 998533ee8..cada3be4a 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/row.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/row.dart @@ -12,22 +12,24 @@ extension RowToXml on WindowsRow { /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-group void buildXml(XmlBuilder builder) => builder.element( - 'group', - nest: () { - for (final WindowsColumn column in columns) { - builder.element( - 'subgroup', - attributes: {'hint-weight': '1'}, - nest: () { - for (final WindowsNotificationPart part in column.parts) { - switch (part) { - case WindowsImage(): part.buildXml(builder); - case WindowsNotificationText(): part.buildXml(builder); - } - } - }, - ); - } - }, - ); + 'group', + nest: () { + for (final WindowsColumn column in columns) { + builder.element( + 'subgroup', + attributes: {'hint-weight': '1'}, + nest: () { + for (final WindowsNotificationPart part in column.parts) { + switch (part) { + case WindowsImage(): + part.buildXml(builder); + case WindowsNotificationText(): + part.buildXml(builder); + } + } + }, + ); + } + }, + ); } diff --git a/flutter_local_notifications_windows/lib/src/details/xml/text.dart b/flutter_local_notifications_windows/lib/src/details/xml/text.dart index eed381971..419e66422 100644 --- a/flutter_local_notifications_windows/lib/src/details/xml/text.dart +++ b/flutter_local_notifications_windows/lib/src/details/xml/text.dart @@ -8,14 +8,14 @@ extension TextToXml on WindowsNotificationText { /// /// See: https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-text void buildXml(XmlBuilder builder) => builder.element( - 'text', - attributes: { - if (languageCode != null) 'lang': languageCode!, - if (placement != null) 'placement': placement!.name, - 'hint-callScenarioCenterAlign': centerIfCall.toString(), - 'hint-align': 'center', - if (isCaption) 'hint-style': 'captionsubtle', - }, - nest: text, - ); + 'text', + attributes: { + if (languageCode != null) 'lang': languageCode!, + if (placement != null) 'placement': placement!.name, + 'hint-callScenarioCenterAlign': centerIfCall.toString(), + 'hint-align': 'center', + if (isCaption) 'hint-style': 'captionsubtle', + }, + nest: text, + ); } diff --git a/flutter_local_notifications_windows/lib/src/ffi/utils.dart b/flutter_local_notifications_windows/lib/src/ffi/utils.dart index f95ffe114..3a63be14b 100644 --- a/flutter_local_notifications_windows/lib/src/ffi/utils.dart +++ b/flutter_local_notifications_windows/lib/src/ffi/utils.dart @@ -11,10 +11,10 @@ import 'bindings.dart'; extension NativeStringMapUtils on NativeStringMap { /// Converts this map to a typical Dart map. Map toMap() => { - for (int index = 0; index < size; index++) - entries[index].key.toDartString(): - entries[index].value.toDartString(), - }; + for (int index = 0; index < size; index++) + entries[index].key.toDartString(): + entries[index].value.toDartString(), + }; } /// Gets the [NotificationResponseType] from a [NativeLaunchType]. @@ -50,7 +50,7 @@ extension MapToNativeMap on Map { for (final MapEntry entry in entries) { pointer.ref.entries[index].key = entry.key.toNativeUtf8(allocator: arena); pointer.ref.entries[index].value = - entry.value.toNativeUtf8(allocator: arena); + entry.value.toNativeUtf8(allocator: arena); index++; } return pointer.ref; @@ -61,15 +61,15 @@ extension MapToNativeMap on Map { extension NativeNotificationDetailsUtils on Pointer { /// Parses this array as a list of [ActiveNotification]s. List asActiveNotifications(int length) => - [ - for (int index = 0; index < length; index++) - ActiveNotification(id: this[index].id), - ]; + [ + for (int index = 0; index < length; index++) + ActiveNotification(id: this[index].id), + ]; /// Parses this array os a list of [PendingNotificationRequest]s. List asPendingRequests(int length) => - [ - for (int index = 0; index < length; index++) - PendingNotificationRequest(this[index].id, null, null, null), - ]; + [ + for (int index = 0; index < length; index++) + PendingNotificationRequest(this[index].id, null, null, null), + ]; } diff --git a/flutter_local_notifications_windows/lib/src/plugin/base.dart b/flutter_local_notifications_windows/lib/src/plugin/base.dart index ee48c8668..a86128aaf 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/base.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/base.dart @@ -10,8 +10,7 @@ export 'package:timezone/timezone.dart'; /// The Windows implementation of `package:flutter_local_notifications`. abstract class WindowsNotificationsBase - extends FlutterLocalNotificationsPlatform -{ + extends FlutterLocalNotificationsPlatform { /// Initializes the plugin. No other method should be called before this. Future initialize( WindowsInitializationSettings settings, { @@ -68,10 +67,11 @@ abstract class WindowsNotificationsBase Future updateProgressBar({ required int notificationId, required WindowsProgressBar progressBar, - }) => updateBindings( - id: notificationId, - bindings: progressBar.data, - ); + }) => + updateBindings( + id: notificationId, + bindings: progressBar.data, + ); /// Updates any data binding in the given notification. /// diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index 93ed0a4df..f4077b110 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -15,11 +15,11 @@ void _globalLaunchCallback(NativeLaunchDetails details) { extension on String { bool get isValidGuid => - length == 36 && - this[8] == '-' && - this[13] == '-' && - this[18] == '-' && - this[23] == '-'; + length == 36 && + this[8] == '-' && + this[13] == '-' && + this[18] == '-' && + this[23] == '-'; } /// The Windows implementation of `package:flutter_local_notifications`. @@ -30,7 +30,7 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { /// Registers the Windows implementation with Flutter. static void registerWith() { FlutterLocalNotificationsPlatform.instance = - FlutterLocalNotificationsWindows(); + FlutterLocalNotificationsWindows(); } /// The global instance of this plugin. Used in [_globalLaunchCallback]. @@ -38,10 +38,10 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { /// The FFI generated bindings to the native code. late final NotificationsPluginBindings _bindings = - NotificationsPluginBindings(_library); + NotificationsPluginBindings(_library); final DynamicLibrary _library = - DynamicLibrary.open('flutter_local_notifications_windows.dll'); + DynamicLibrary.open('flutter_local_notifications_windows.dll'); /// A pointer to the C++ handler class. late final Pointer _plugin; @@ -62,38 +62,40 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { Future initialize( WindowsInitializationSettings settings, { DidReceiveNotificationResponseCallback? onNotificationReceived, - }) async => using((Arena arena) { - if (_isReady) { - return true; - } - _plugin = _bindings.createPlugin(); - // The C++ code will crash if there's an invalid GUID, so check it here - if (!settings.guid.isValidGuid) { - throw ArgumentError.value( - settings.guid, - 'GUID', - 'Invalid GUID. Please use xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format\n' - 'You can get one by searching GUID generators online', - ); - } - instance = this; - userCallback = onNotificationReceived; - final Pointer appName = - settings.appName.toNativeUtf8(allocator: arena); - final Pointer aumId = - settings.appUserModelId.toNativeUtf8(allocator: arena); - final Pointer guid = settings.guid.toNativeUtf8(allocator: arena); - final Pointer iconPath = - settings.iconPath?.toNativeUtf8(allocator: arena) ?? nullptr; - final Pointer> callback = - NativeCallable - .listener(_globalLaunchCallback) - .nativeFunction; - final bool result = _bindings - .init(_plugin, appName, aumId, guid, iconPath, callback); - _isReady = result; - return result; - }); + }) async => + using((Arena arena) { + if (_isReady) { + return true; + } + _plugin = _bindings.createPlugin(); + // The C++ code will crash if there's an invalid GUID, so check it here + if (!settings.guid.isValidGuid) { + throw ArgumentError.value( + settings.guid, + 'GUID', + 'Invalid GUID. Please use xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format\n' + 'You can get one by searching GUID generators online', + ); + } + instance = this; + userCallback = onNotificationReceived; + final Pointer appName = + settings.appName.toNativeUtf8(allocator: arena); + final Pointer aumId = + settings.appUserModelId.toNativeUtf8(allocator: arena); + final Pointer guid = settings.guid.toNativeUtf8(allocator: arena); + final Pointer iconPath = + settings.iconPath?.toNativeUtf8(allocator: arena) ?? nullptr; + final Pointer> + callback = + NativeCallable.listener( + _globalLaunchCallback) + .nativeFunction; + final bool result = + _bindings.init(_plugin, appName, aumId, guid, iconPath, callback); + _isReady = result; + return result; + }); @override void dispose() { @@ -144,24 +146,6 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { @override Future> getActiveNotifications() async => - using((Arena arena) { - if (!_isReady) { - throw StateError( - 'Flutter Local Notifications must be initialized before use', - ); - } - final Pointer length = arena(); - final Pointer array = - _bindings.getActiveNotifications(_plugin, length); - final List result = - array.asActiveNotifications(length.value); - _bindings.freeDetailsArray(array); - return result; - }); - - @override - Future> - pendingNotificationRequests() async => using((Arena arena) { if (!_isReady) { throw StateError( @@ -170,16 +154,33 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { } final Pointer length = arena(); final Pointer array = - _bindings.getPendingNotifications(_plugin, length); - final List result = - array.asPendingRequests(length.value); + _bindings.getActiveNotifications(_plugin, length); + final List result = + array.asActiveNotifications(length.value); _bindings.freeDetailsArray(array); return result; }); + @override + Future> + pendingNotificationRequests() async => using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + final Pointer length = arena(); + final Pointer array = + _bindings.getPendingNotifications(_plugin, length); + final List result = + array.asPendingRequests(length.value); + _bindings.freeDetailsArray(array); + return result; + }); + @override Future - getNotificationAppLaunchDetails() async { + getNotificationAppLaunchDetails() async { if (!_isReady) { throw StateError( 'Flutter Local Notifications must be initialized before use', @@ -226,67 +227,58 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { } @override - Future show( - int id, - String? title, - String? body, - {String? payload, WindowsNotificationDetails? details} - ) async => using((Arena arena) { - if (!_isReady) { - throw StateError( - 'Flutter Local Notifications must be initialized before use', - ); - } - final Map bindings = { - if (details != null) ...details.bindings, - for ( - final WindowsProgressBar progressBar - in details?.progressBars ?? [] - ) ...progressBar.data, - }; - final NativeStringMap nativeMap = bindings.toNativeMap(arena); - final String xml = notificationToXml( - title: title, - body: body, - payload: payload, - details: details, - ); - final bool result = _bindings - .showNotification( - _plugin, - id, - xml.toNativeUtf8(allocator: arena), - nativeMap, - ); - if (!result) { - throw Exception( - 'Flutter Local Notifications could not show notification', - ); - } - }); + Future show(int id, String? title, String? body, + {String? payload, WindowsNotificationDetails? details}) async => + using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + final Map bindings = { + if (details != null) ...details.bindings, + for (final WindowsProgressBar progressBar + in details?.progressBars ?? []) + ...progressBar.data, + }; + final NativeStringMap nativeMap = bindings.toNativeMap(arena); + final String xml = notificationToXml( + title: title, + body: body, + payload: payload, + details: details, + ); + final bool result = _bindings.showNotification( + _plugin, + id, + xml.toNativeUtf8(allocator: arena), + nativeMap, + ); + if (!result) { + throw Exception( + 'Flutter Local Notifications could not show notification', + ); + } + }); @override Future showRawXml({ required int id, required String xml, Map bindings = const {}, - }) async => using((Arena arena) { - if (!_isReady) { - throw StateError( - 'Flutter Local Notifications must be initialized before use', - ); - } - final bool result = _bindings - .showNotification( - _plugin, - id, - xml.toNativeUtf8(allocator: arena), - bindings.toNativeMap(arena) - ); - if (!result) { - throw ArgumentError('Flutter Local Notifications: Invalid XML'); - } - }); + }) async => + using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + final bool result = _bindings.showNotification(_plugin, id, + xml.toNativeUtf8(allocator: arena), bindings.toNativeMap(arena)); + if (!result) { + throw ArgumentError('Flutter Local Notifications: Invalid XML'); + } + }); @override Future zonedSchedule( @@ -296,32 +288,33 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { TZDateTime scheduledDate, WindowsNotificationDetails? details, { String? payload, - }) async => using((Arena arena) { - if (!_isReady) { - throw StateError( - 'Flutter Local Notifications must be initialized before use', - ); - } - if (scheduledDate.isBefore(DateTime.now())) { - throw ArgumentError( - 'Flutter Local Notifications cannot schedule notifications in the past', - ); - } - final String xml = notificationToXml( - title: title, - body: body, - payload: payload, - details: details, - ); - final int secondsSinceEpoch = - scheduledDate.millisecondsSinceEpoch ~/ 1000; - _bindings.scheduleNotification( - _plugin, - id, - xml.toNativeUtf8(allocator: arena), - secondsSinceEpoch, - ); - }); + }) async => + using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + if (scheduledDate.isBefore(DateTime.now())) { + throw ArgumentError( + 'Flutter Local Notifications cannot schedule notifications in the past', + ); + } + final String xml = notificationToXml( + title: title, + body: body, + payload: payload, + details: details, + ); + final int secondsSinceEpoch = + scheduledDate.millisecondsSinceEpoch ~/ 1000; + _bindings.scheduleNotification( + _plugin, + id, + xml.toNativeUtf8(allocator: arena), + secondsSinceEpoch, + ); + }); @override Future zonedScheduleRawXml( @@ -329,40 +322,43 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { String xml, TZDateTime scheduledDate, WindowsNotificationDetails? details, - ) async => using((Arena arena) { - if (!_isReady) { - throw StateError( - 'Flutter Local Notifications must be initialized before use', - ); - } - if (scheduledDate.isBefore(DateTime.now())) { - throw ArgumentError( - 'Flutter Local Notifications cannot schedule notifications in the past', - ); - } - final int secondsSinceEpoch = scheduledDate.millisecondsSinceEpoch ~/ 1000; - _bindings.scheduleNotification( - _plugin, - id, - xml.toNativeUtf8(allocator: arena), - secondsSinceEpoch, - ); - }); + ) async => + using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + if (scheduledDate.isBefore(DateTime.now())) { + throw ArgumentError( + 'Flutter Local Notifications cannot schedule notifications in the past', + ); + } + final int secondsSinceEpoch = + scheduledDate.millisecondsSinceEpoch ~/ 1000; + _bindings.scheduleNotification( + _plugin, + id, + xml.toNativeUtf8(allocator: arena), + secondsSinceEpoch, + ); + }); @override Future updateBindings({ required int id, required Map bindings, - }) async => using((Arena arena) { - if (!_isReady) { - throw StateError( - 'Flutter Local Notifications must be initialized before use', - ); - } - final NativeUpdateResult result = _bindings - .updateNotification(_plugin, id, bindings.toNativeMap(arena)); - return getUpdateResult(result); - }); + }) async => + using((Arena arena) { + if (!_isReady) { + throw StateError( + 'Flutter Local Notifications must be initialized before use', + ); + } + final NativeUpdateResult result = _bindings.updateNotification( + _plugin, id, bindings.toNativeMap(arena)); + return getUpdateResult(result); + }); @override @visibleForTesting diff --git a/flutter_local_notifications_windows/lib/src/plugin/stub.dart b/flutter_local_notifications_windows/lib/src/plugin/stub.dart index 95a9f7540..7aba9e34a 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/stub.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/stub.dart @@ -26,15 +26,15 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { @override Future> getActiveNotifications() async => - []; + []; @override Future - getNotificationAppLaunchDetails() async => null; + getNotificationAppLaunchDetails() async => null; @override Future> - pendingNotificationRequests() async => []; + pendingNotificationRequests() async => []; @override Future periodicallyShow( @@ -90,9 +90,10 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { Future updateBindings({ required int id, required Map bindings, - }) async => NotificationUpdateResult.success; + }) async => + NotificationUpdateResult.success; @override @visibleForTesting - void enableMultithreading() { } + void enableMultithreading() {} } diff --git a/flutter_local_notifications_windows/test/bindings_test.dart b/flutter_local_notifications_windows/test/bindings_test.dart index b26b15ba2..506ad202e 100644 --- a/flutter_local_notifications_windows/test/bindings_test.dart +++ b/flutter_local_notifications_windows/test/bindings_test.dart @@ -13,47 +13,47 @@ const Map bindings = { }; void main() => group('Bindings', () { - FlutterLocalNotificationsWindows().enableMultithreading(); - final FlutterLocalNotificationsWindows plugin = - FlutterLocalNotificationsWindows(); - setUpAll(() => plugin.initialize(settings)); - tearDownAll(() async { - await plugin.cancelAll(); - plugin.dispose(); - }); + FlutterLocalNotificationsWindows().enableMultithreading(); + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); + setUpAll(() => plugin.initialize(settings)); + tearDownAll(() async { + await plugin.cancelAll(); + plugin.dispose(); + }); - test('work in simple cases', () async { - await plugin.show(500, '{title}', '{body}'); - final NotificationUpdateResult result = - await plugin.updateBindings(id: 500, bindings: bindings); - expect(result, NotificationUpdateResult.success); - }); + test('work in simple cases', () async { + await plugin.show(500, '{title}', '{body}'); + final NotificationUpdateResult result = + await plugin.updateBindings(id: 500, bindings: bindings); + expect(result, NotificationUpdateResult.success); + }); - test('fail when ID is not found in simple cases', () async { - await plugin.show(501, '{title}', '{body}'); - final NotificationUpdateResult result = - await plugin.updateBindings(id: 599, bindings: bindings); - expect(result, NotificationUpdateResult.notFound); - }); + test('fail when ID is not found in simple cases', () async { + await plugin.show(501, '{title}', '{body}'); + final NotificationUpdateResult result = + await plugin.updateBindings(id: 599, bindings: bindings); + expect(result, NotificationUpdateResult.notFound); + }); - test('are included in show()', () async { - await plugin.show( - 502, - '{title}', - '{body}', - details: const WindowsNotificationDetails(bindings: bindings), - ); - }); + test('are included in show()', () async { + await plugin.show( + 502, + '{title}', + '{body}', + details: const WindowsNotificationDetails(bindings: bindings), + ); + }); - test('fail when notification has been cancelled', retry: 5, () async { - await Future.delayed(const Duration(milliseconds: 200)); - await plugin.show(503, '{title}', '{body}'); - final NotificationUpdateResult result = - await plugin.updateBindings(id: 503, bindings: bindings); - expect(result, NotificationUpdateResult.success); - await plugin.cancelAll(); - final NotificationUpdateResult result2 = - await plugin.updateBindings(id: 503, bindings: bindings); - expect(result2, NotificationUpdateResult.notFound); - }); -}); + test('fail when notification has been cancelled', retry: 5, () async { + await Future.delayed(const Duration(milliseconds: 200)); + await plugin.show(503, '{title}', '{body}'); + final NotificationUpdateResult result = + await plugin.updateBindings(id: 503, bindings: bindings); + expect(result, NotificationUpdateResult.success); + await plugin.cancelAll(); + final NotificationUpdateResult result2 = + await plugin.updateBindings(id: 503, bindings: bindings); + expect(result2, NotificationUpdateResult.notFound); + }); + }); diff --git a/flutter_local_notifications_windows/test/details_test.dart b/flutter_local_notifications_windows/test/details_test.dart index 733bd5ca0..ebfc553d7 100644 --- a/flutter_local_notifications_windows/test/details_test.dart +++ b/flutter_local_notifications_windows/test/details_test.dart @@ -13,235 +13,236 @@ extension PluginUtils on FlutterLocalNotificationsWindows { static int id = 15; Future showDetails(WindowsNotificationDetails details) => - show(id++, 'Title', 'Body', details: details); + show(id++, 'Title', 'Body', details: details); void testDetails(WindowsNotificationDetails details) => - expect(showDetails(details), completes); + expect(showDetails(details), completes); } void main() => group('Details:', () { - FlutterLocalNotificationsWindows().enableMultithreading(); - final FlutterLocalNotificationsWindows plugin = - FlutterLocalNotificationsWindows(); - setUpAll(() => plugin.initialize(settings)); - tearDownAll(() async { - await plugin.cancelAll(); - plugin.dispose(); - }); - - test('No details', () async { - expect(plugin.show(100, null, null), completes); - expect(plugin.show(101, 'Title', null), completes); - expect(plugin.show(102, null, 'Body'), completes); - expect(plugin.show(103, 'Title', 'Body'), completes); - expect(plugin.show(-1, 'Negative ID', 'Body'), completes); - }); - - test('Simple details', () async => plugin - ..testDetails(const WindowsNotificationDetails()) - ..testDetails( - const WindowsNotificationDetails(subtitle: 'Subtitle')) - ..testDetails(const WindowsNotificationDetails( - duration: WindowsNotificationDuration.long)) - ..testDetails(const WindowsNotificationDetails( - scenario: WindowsNotificationScenario.reminder)) - ..testDetails(WindowsNotificationDetails(timestamp: DateTime.now())) - ..testDetails(const WindowsNotificationDetails( - subtitle: '{message}', - bindings: {'message': 'Hello, Mr. Person'})) - ); - - test('Actions', () { - const WindowsAction simpleAction = - WindowsAction(content: 'Press me', arguments: '123'); - final WindowsAction complexAction = WindowsAction( - content: 'content', - arguments: 'args', - activationBehavior: WindowsNotificationBehavior.pendingUpdate, - buttonStyle: WindowsButtonStyle.success, - inputId: 'input-id', - tooltip: 'tooltip', - image: File('test/icon.png').absolute, - ); - plugin - ..testDetails(const WindowsNotificationDetails( - actions: [simpleAction])) - ..testDetails(WindowsNotificationDetails( - actions: [complexAction])) - ..testDetails( - WindowsNotificationDetails( - actions: List.filled(5, simpleAction)) - ); - expect( - plugin.showDetails( - WindowsNotificationDetails( - actions: List.filled(6, simpleAction), - ), - ), - throwsArgumentError, - ); - }); - - test('Audio', () => plugin - ..testDetails(WindowsNotificationDetails( - audio: WindowsNotificationAudio.silent())) - ..testDetails(WindowsNotificationDetails( - audio: WindowsNotificationAudio.preset( - sound: WindowsNotificationSound.call10))) - ); - - test('Rows', () { - const WindowsColumn emptyColumn = - WindowsColumn([]); - final WindowsImage image = WindowsImage.file( - File('test/icon.png').absolute, - altText: 'an icon'); - const WindowsNotificationText text = - WindowsNotificationText(text: 'Text'); - final WindowsColumn simpleColumn = - WindowsColumn([image, text]); - final WindowsRow bigRow = WindowsRow( - List.filled(5, simpleColumn), - ); - plugin - - ..testDetails(const WindowsNotificationDetails()) - ..testDetails(const WindowsNotificationDetails( - rows: [WindowsRow([])])) - ..testDetails(const WindowsNotificationDetails(rows: [ - WindowsRow([emptyColumn]) - ])) - ..testDetails(WindowsNotificationDetails(rows: [ - WindowsRow([simpleColumn]) - ])) - ..testDetails( - WindowsNotificationDetails(rows: [bigRow])) - ..testDetails( - WindowsNotificationDetails(rows: List.filled(5, bigRow))); - }); - - test('Header', () async { - const WindowsHeader header = WindowsHeader( - id: 'header1', - title: 'Header 1', - arguments: 'args1', - activation: WindowsHeaderActivation.foreground, - ); - plugin - ..testDetails(const WindowsNotificationDetails(header: header)) - ..testDetails(const WindowsNotificationDetails(header: header)); - }); - - test('Images', () async { - final WindowsImage simpleImage = WindowsImage.file( - File('test/icon.png').absolute, - altText: 'an icon', - ); - final WindowsImage complexImage = WindowsImage.file( - File('test/icon.png').absolute, - altText: 'an icon', - addQueryParams: true, - crop: WindowsImageCrop.circle, - placement: WindowsImagePlacement.appLogoOverride, - ); - plugin - ..testDetails( - WindowsNotificationDetails(images: [simpleImage])) - ..testDetails(WindowsNotificationDetails( - images: [simpleImage, complexImage])) - ..testDetails( - WindowsNotificationDetails( - images: List.filled(6, simpleImage), - ), - ); - }); - - test('Inputs', () async { - const WindowsTextInput textInput = WindowsTextInput( - id: 'input', - placeHolderContent: 'Text hint', - title: 'Text title', - ); - const WindowsSelectionInput selection = WindowsSelectionInput( - id: 'input', - items: [ - WindowsSelection(id: 'item1', content: 'Item 1'), - WindowsSelection(id: 'item2', content: 'Item 2'), - WindowsSelection(id: 'item3', content: 'Item 3'), - ], - ); - const WindowsAction action = WindowsAction( - content: 'Submit', - arguments: 'submit', - inputId: 'input', - ); - plugin - ..testDetails(const WindowsNotificationDetails( - inputs: [textInput])) - ..testDetails(const WindowsNotificationDetails( - inputs: [selection])) - ..testDetails( - WindowsNotificationDetails( - inputs: List.filled(5, textInput), - ), - ) - ..testDetails(const WindowsNotificationDetails( - inputs: [textInput], - actions: [action])) - ..testDetails(const WindowsNotificationDetails( - inputs: [selection, textInput], - actions: [action])); - expect( - plugin.showDetails( - WindowsNotificationDetails( - inputs: List.filled(6, textInput), - ), - ), - throwsArgumentError, - ); - }); - - test('Progress', retry: 5, () async { - final WindowsProgressBar simple = WindowsProgressBar( - id: 'simple', - status: 'Testing...', - value: 0.25, - ); - final WindowsProgressBar complex = WindowsProgressBar( - id: 'complex', - status: 'Testing...', - value: 0.75, - label: 'Progress label', - title: 'Progress title', - ); - final WindowsProgressBar dynamic = WindowsProgressBar( - id: 'dynamic', - status: 'Testing...', - value: 0, - ); - plugin - ..testDetails(WindowsNotificationDetails( - progressBars: [simple])) - ..testDetails(WindowsNotificationDetails( - progressBars: [complex])) - ..testDetails(WindowsNotificationDetails( - progressBars: [simple, complex])) - ..testDetails( - WindowsNotificationDetails( - progressBars: List.filled(6, simple), - ), - ); - await plugin.show(201, null, null, - details: WindowsNotificationDetails( - progressBars: [dynamic], - ), - ); - for (double i = 0; i <= 1.5; i += 0.05) { - dynamic.value = i; - final NotificationUpdateResult result = await plugin - .updateProgressBar(notificationId: 201, progressBar: dynamic); - expect(result, NotificationUpdateResult.success); - await Future.delayed(const Duration(milliseconds: 10)); - } - }); -}); + FlutterLocalNotificationsWindows().enableMultithreading(); + final FlutterLocalNotificationsWindows plugin = + FlutterLocalNotificationsWindows(); + setUpAll(() => plugin.initialize(settings)); + tearDownAll(() async { + await plugin.cancelAll(); + plugin.dispose(); + }); + + test('No details', () async { + expect(plugin.show(100, null, null), completes); + expect(plugin.show(101, 'Title', null), completes); + expect(plugin.show(102, null, 'Body'), completes); + expect(plugin.show(103, 'Title', 'Body'), completes); + expect(plugin.show(-1, 'Negative ID', 'Body'), completes); + }); + + test( + 'Simple details', + () async => plugin + ..testDetails(const WindowsNotificationDetails()) + ..testDetails( + const WindowsNotificationDetails(subtitle: 'Subtitle')) + ..testDetails(const WindowsNotificationDetails( + duration: WindowsNotificationDuration.long)) + ..testDetails(const WindowsNotificationDetails( + scenario: WindowsNotificationScenario.reminder)) + ..testDetails(WindowsNotificationDetails(timestamp: DateTime.now())) + ..testDetails(const WindowsNotificationDetails( + subtitle: '{message}', + bindings: {'message': 'Hello, Mr. Person'}))); + + test('Actions', () { + const WindowsAction simpleAction = + WindowsAction(content: 'Press me', arguments: '123'); + final WindowsAction complexAction = WindowsAction( + content: 'content', + arguments: 'args', + activationBehavior: WindowsNotificationBehavior.pendingUpdate, + buttonStyle: WindowsButtonStyle.success, + inputId: 'input-id', + tooltip: 'tooltip', + image: File('test/icon.png').absolute, + ); + plugin + ..testDetails(const WindowsNotificationDetails( + actions: [simpleAction])) + ..testDetails(WindowsNotificationDetails( + actions: [complexAction])) + ..testDetails(WindowsNotificationDetails( + actions: List.filled(5, simpleAction))); + expect( + plugin.showDetails( + WindowsNotificationDetails( + actions: List.filled(6, simpleAction), + ), + ), + throwsArgumentError, + ); + }); + + test( + 'Audio', + () => plugin + ..testDetails(WindowsNotificationDetails( + audio: WindowsNotificationAudio.silent())) + ..testDetails(WindowsNotificationDetails( + audio: WindowsNotificationAudio.preset( + sound: WindowsNotificationSound.call10)))); + + test('Rows', () { + const WindowsColumn emptyColumn = + WindowsColumn([]); + final WindowsImage image = WindowsImage.file( + File('test/icon.png').absolute, + altText: 'an icon'); + const WindowsNotificationText text = + WindowsNotificationText(text: 'Text'); + final WindowsColumn simpleColumn = + WindowsColumn([image, text]); + final WindowsRow bigRow = WindowsRow( + List.filled(5, simpleColumn), + ); + plugin + ..testDetails(const WindowsNotificationDetails()) + ..testDetails(const WindowsNotificationDetails( + rows: [WindowsRow([])])) + ..testDetails(const WindowsNotificationDetails(rows: [ + WindowsRow([emptyColumn]) + ])) + ..testDetails(WindowsNotificationDetails(rows: [ + WindowsRow([simpleColumn]) + ])) + ..testDetails(WindowsNotificationDetails(rows: [bigRow])) + ..testDetails(WindowsNotificationDetails( + rows: List.filled(5, bigRow))); + }); + + test('Header', () async { + const WindowsHeader header = WindowsHeader( + id: 'header1', + title: 'Header 1', + arguments: 'args1', + activation: WindowsHeaderActivation.foreground, + ); + plugin + ..testDetails(const WindowsNotificationDetails(header: header)) + ..testDetails(const WindowsNotificationDetails(header: header)); + }); + + test('Images', () async { + final WindowsImage simpleImage = WindowsImage.file( + File('test/icon.png').absolute, + altText: 'an icon', + ); + final WindowsImage complexImage = WindowsImage.file( + File('test/icon.png').absolute, + altText: 'an icon', + addQueryParams: true, + crop: WindowsImageCrop.circle, + placement: WindowsImagePlacement.appLogoOverride, + ); + plugin + ..testDetails( + WindowsNotificationDetails(images: [simpleImage])) + ..testDetails(WindowsNotificationDetails( + images: [simpleImage, complexImage])) + ..testDetails( + WindowsNotificationDetails( + images: List.filled(6, simpleImage), + ), + ); + }); + + test('Inputs', () async { + const WindowsTextInput textInput = WindowsTextInput( + id: 'input', + placeHolderContent: 'Text hint', + title: 'Text title', + ); + const WindowsSelectionInput selection = WindowsSelectionInput( + id: 'input', + items: [ + WindowsSelection(id: 'item1', content: 'Item 1'), + WindowsSelection(id: 'item2', content: 'Item 2'), + WindowsSelection(id: 'item3', content: 'Item 3'), + ], + ); + const WindowsAction action = WindowsAction( + content: 'Submit', + arguments: 'submit', + inputId: 'input', + ); + plugin + ..testDetails(const WindowsNotificationDetails( + inputs: [textInput])) + ..testDetails(const WindowsNotificationDetails( + inputs: [selection])) + ..testDetails( + WindowsNotificationDetails( + inputs: List.filled(5, textInput), + ), + ) + ..testDetails(const WindowsNotificationDetails( + inputs: [textInput], + actions: [action])) + ..testDetails(const WindowsNotificationDetails( + inputs: [selection, textInput], + actions: [action])); + expect( + plugin.showDetails( + WindowsNotificationDetails( + inputs: List.filled(6, textInput), + ), + ), + throwsArgumentError, + ); + }); + + test('Progress', retry: 5, () async { + final WindowsProgressBar simple = WindowsProgressBar( + id: 'simple', + status: 'Testing...', + value: 0.25, + ); + final WindowsProgressBar complex = WindowsProgressBar( + id: 'complex', + status: 'Testing...', + value: 0.75, + label: 'Progress label', + title: 'Progress title', + ); + final WindowsProgressBar dynamic = WindowsProgressBar( + id: 'dynamic', + status: 'Testing...', + value: 0, + ); + plugin + ..testDetails(WindowsNotificationDetails( + progressBars: [simple])) + ..testDetails(WindowsNotificationDetails( + progressBars: [complex])) + ..testDetails(WindowsNotificationDetails( + progressBars: [simple, complex])) + ..testDetails( + WindowsNotificationDetails( + progressBars: List.filled(6, simple), + ), + ); + await plugin.show( + 201, + null, + null, + details: WindowsNotificationDetails( + progressBars: [dynamic], + ), + ); + for (double i = 0; i <= 1.5; i += 0.05) { + dynamic.value = i; + final NotificationUpdateResult result = await plugin + .updateProgressBar(notificationId: 201, progressBar: dynamic); + expect(result, NotificationUpdateResult.success); + await Future.delayed(const Duration(milliseconds: 10)); + } + }); + }); diff --git a/flutter_local_notifications_windows/test/plugin_test.dart b/flutter_local_notifications_windows/test/plugin_test.dart index 8f81be324..06de35d27 100644 --- a/flutter_local_notifications_windows/test/plugin_test.dart +++ b/flutter_local_notifications_windows/test/plugin_test.dart @@ -13,7 +13,7 @@ const WindowsInitializationSettings badSettings = WindowsInitializationSettings( appName: 'test', appUserModelId: 'com.test.test', guid: '123'); void main() => group('Plugin', () { - FlutterLocalNotificationsWindows().enableMultithreading(); + FlutterLocalNotificationsWindows().enableMultithreading(); setUpAll(initializeTimeZones); diff --git a/flutter_local_notifications_windows/test/scheduled_test.dart b/flutter_local_notifications_windows/test/scheduled_test.dart index b293c5e5f..f810b8da3 100644 --- a/flutter_local_notifications_windows/test/scheduled_test.dart +++ b/flutter_local_notifications_windows/test/scheduled_test.dart @@ -9,7 +9,7 @@ const WindowsInitializationSettings settings = WindowsInitializationSettings( guid: 'a8c22b55-049e-422f-b30f-863694de08c8'); void main() => group('Schedules', () { - FlutterLocalNotificationsWindows().enableMultithreading(); + FlutterLocalNotificationsWindows().enableMultithreading(); final FlutterLocalNotificationsWindows plugin = FlutterLocalNotificationsWindows(); setUpAll(initializeTimeZones); diff --git a/flutter_local_notifications_windows/test/xml_test.dart b/flutter_local_notifications_windows/test/xml_test.dart index 9b5d1ea15..c9b100c58 100644 --- a/flutter_local_notifications_windows/test/xml_test.dart +++ b/flutter_local_notifications_windows/test/xml_test.dart @@ -57,7 +57,7 @@ const String complexXml = ''' '''; void main() => group('XML', () { - FlutterLocalNotificationsWindows().enableMultithreading(); + FlutterLocalNotificationsWindows().enableMultithreading(); final FlutterLocalNotificationsWindows plugin = FlutterLocalNotificationsWindows(); From f411b17086ec5a561dbbe90d66b99aed001ded70 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 4 Sep 2024 14:43:29 -0400 Subject: [PATCH 091/112] Formatted again --- .../lib/src/plugin/ffi.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index f4077b110..2a2967317 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -73,8 +73,8 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { throw ArgumentError.value( settings.guid, 'GUID', - 'Invalid GUID. Please use xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format\n' - 'You can get one by searching GUID generators online', + 'Invalid GUID. Please use xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + ' format.\nYou can get one by searching GUID generators online', ); } instance = this; @@ -297,7 +297,8 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { } if (scheduledDate.isBefore(DateTime.now())) { throw ArgumentError( - 'Flutter Local Notifications cannot schedule notifications in the past', + 'Flutter Local Notifications cannot' + ' schedule notifications in the past', ); } final String xml = notificationToXml( @@ -331,7 +332,8 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { } if (scheduledDate.isBefore(DateTime.now())) { throw ArgumentError( - 'Flutter Local Notifications cannot schedule notifications in the past', + 'Flutter Local Notifications cannot' + ' schedule notifications in the past', ); } final int secondsSinceEpoch = From 4f6e9d90bbac44a5772d7808d11d216551c284f5 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 4 Sep 2024 14:50:45 -0400 Subject: [PATCH 092/112] Fix Windows CI to not use .sh script --- .github/workflows/validate.yml | 12 +++++++++--- flutter_local_notifications_windows/CHANGELOG.md | 4 ++-- flutter_local_notifications_windows/pubspec.yaml | 2 ++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c8aa1ff52..226db9d6d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -198,7 +198,9 @@ jobs: cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - name: Install Tools - run: ./.github/workflows/scripts/install-tools.sh + run: | + dart pub global activate melos + melos bootstrap - name: Build run: melos run build:example_windows build_example_windows_3_19: @@ -213,7 +215,9 @@ jobs: cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - name: Install Tools - run: ./.github/workflows/scripts/install-tools.sh + run: | + dart pub global activate melos + melos bootstrap - name: Build run: melos run build:example_windows unit_tests_dart: @@ -255,7 +259,9 @@ jobs: cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - name: Install tools - run: ./github/workflows/scripts/install-tools.sh + run: | + dart pub global activate melos + melos bootstrap - name: Run Tests run: melos run test:unit:windows integration_tests_android: diff --git a/flutter_local_notifications_windows/CHANGELOG.md b/flutter_local_notifications_windows/CHANGELOG.md index 41cc7d819..89dfc0ddb 100644 --- a/flutter_local_notifications_windows/CHANGELOG.md +++ b/flutter_local_notifications_windows/CHANGELOG.md @@ -1,3 +1,3 @@ -## 0.0.1 +## 1.0.0 -* TODO: Describe initial release. +* Initial release for Windows diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index ad50d8672..64834b50e 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -1,6 +1,7 @@ name: flutter_local_notifications_windows description: "A new Flutter FFI plugin project." version: 1.0.0 +homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications_windows environment: sdk: ">=3.3.0 <4.0.0" @@ -8,6 +9,7 @@ environment: dependencies: ffi: ^2.1.2 flutter_local_notifications_platform_interface: ^7.2.0 + meta: ^1.15.0 timezone: ^0.9.4 xml: ^6.5.0 From 32c007d21323945ab15ce48b6167d525a3e3ad77 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 4 Sep 2024 14:52:56 -0400 Subject: [PATCH 093/112] Fix workflow name --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 226db9d6d..9a01a9ca8 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -204,7 +204,7 @@ jobs: - name: Build run: melos run build:example_windows build_example_windows_3_19: - name: Build Linux example app (3.19) + name: Build Windows example app (3.19) runs-on: windows-latest steps: - uses: actions/checkout@v4 From bf5c9cc61d2789325f54723e976d8c3349c90d10 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 5 Sep 2024 14:55:19 -0400 Subject: [PATCH 094/112] Downgraded _windows meta to 1.11 --- flutter_local_notifications_windows/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index 64834b50e..cd4eeea9e 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: ffi: ^2.1.2 flutter_local_notifications_platform_interface: ^7.2.0 - meta: ^1.15.0 + meta: ^1.11.0 timezone: ^0.9.4 xml: ^6.5.0 From b1853f13e3af756d0837b6c73582f169cfe5e984 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 15 Oct 2024 16:23:16 -0400 Subject: [PATCH 095/112] Added parseGuid function for SDKs that don't have it built-in --- .../src/plugin.cpp | 2 +- .../src/utils.cpp | 60 ++++++++++++++++++- .../src/utils.hpp | 2 + 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/flutter_local_notifications_windows/src/plugin.cpp b/flutter_local_notifications_windows/src/plugin.cpp index a4a28590f..983171633 100644 --- a/flutter_local_notifications_windows/src/plugin.cpp +++ b/flutter_local_notifications_windows/src/plugin.cpp @@ -187,7 +187,7 @@ void UpdateRegistry( /// and the guid of the callback. bool RegisterCallback(const std::string& guid, NativeNotificationCallback callback) { DWORD registration{}; - winrt::guid rclsid(guid); + winrt::guid rclsid = parseGuid(guid); const auto factory_ref = winrt::make_self(); const auto factory = factory_ref.get(); factory->callback = callback; diff --git a/flutter_local_notifications_windows/src/utils.cpp b/flutter_local_notifications_windows/src/utils.cpp index f4f3e8df7..a90bae3c9 100644 --- a/flutter_local_notifications_windows/src/utils.cpp +++ b/flutter_local_notifications_windows/src/utils.cpp @@ -1,6 +1,7 @@ #include #include "utils.hpp" +#include char* toNativeString(string str) { const auto size = (int) str.size() + 1; // + 1 for null terminator @@ -24,4 +25,61 @@ NotificationData dataFromMap(NativeStringMap map) { data.Values().Insert(key, value); } return data; -} \ No newline at end of file +} + +constexpr uint8_t hex_to_uint(char const c) { + if (c >= '0' && c <= '9') { + return static_cast(c - '0'); + } else if (c >= 'A' && c <= 'F') { + return static_cast(10 + c - 'A'); + } else if (c >= 'a' && c <= 'f') { + return static_cast(10 + c - 'a'); + } else { + throw std::invalid_argument("Character is not a hexadecimal digit"); + } +} + +constexpr uint8_t hex_to_uint8(char const a, char const b) { + return (hex_to_uint(a) << 4) | hex_to_uint(b); +} + +constexpr uint16_t uint8_to_uint16(uint8_t a, uint8_t b) { + return (static_cast(a) << 8) | static_cast(b); +} + +constexpr uint32_t uint8_to_uint32(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { + return (static_cast(uint8_to_uint16(a, b)) << 16) | + static_cast(uint8_to_uint16(c, d)); +} + +winrt::guid parseGuid(const std::string& guidString) { + if (guidString.size() != 36 || guidString[8] != '-' || guidString[13] != '-' || guidString[18] != '-' || guidString[23] != '-') { + throw std::invalid_argument("guidString is not a valid GUID string"); + } + return { + uint8_to_uint32( + hex_to_uint8(guidString[0], guidString[1]), + hex_to_uint8(guidString[2], guidString[3]), + hex_to_uint8(guidString[4], guidString[5]), + hex_to_uint8(guidString[6], guidString[7]) + ), + uint8_to_uint16( + hex_to_uint8(guidString[9], guidString[10]), + hex_to_uint8(guidString[11], guidString[12]) + ), + uint8_to_uint16( + hex_to_uint8(guidString[14], guidString[15]), + hex_to_uint8(guidString[16], guidString[17]) + ), + { + hex_to_uint8(guidString[19], guidString[20]), + hex_to_uint8(guidString[21], guidString[22]), + hex_to_uint8(guidString[24], guidString[25]), + hex_to_uint8(guidString[26], guidString[27]), + hex_to_uint8(guidString[28], guidString[29]), + hex_to_uint8(guidString[30], guidString[31]), + hex_to_uint8(guidString[32], guidString[33]), + hex_to_uint8(guidString[34], guidString[35]), + } + }; +} diff --git a/flutter_local_notifications_windows/src/utils.hpp b/flutter_local_notifications_windows/src/utils.hpp index 575b73bed..fa4ca5780 100644 --- a/flutter_local_notifications_windows/src/utils.hpp +++ b/flutter_local_notifications_windows/src/utils.hpp @@ -20,3 +20,5 @@ NativeStringMap toNativeMap(vector entries); /// Parses a [NativeStringMap] into a WinRT [NotificationData]. NotificationData dataFromMap(NativeStringMap map); + +winrt::guid parseGuid(const std::string& guidString); From 020b1de06d8146743d316a22ec715482f481efdb Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 31 Oct 2024 20:27:44 -0400 Subject: [PATCH 096/112] Use a vector instead of malloc for plugin.cpp --- flutter_local_notifications_windows/src/plugin.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/flutter_local_notifications_windows/src/plugin.cpp b/flutter_local_notifications_windows/src/plugin.cpp index 983171633..0dfd830b9 100644 --- a/flutter_local_notifications_windows/src/plugin.cpp +++ b/flutter_local_notifications_windows/src/plugin.cpp @@ -218,10 +218,8 @@ std::optional NativePlugin::checkIdentity() { auto error = GetCurrentPackageFullName(&length, nullptr); if (error == APPMODEL_ERROR_NO_PACKAGE) return false; else if (error != ERROR_INSUFFICIENT_BUFFER) return std::nullopt; - PWSTR fullName = (PWSTR) malloc(length * sizeof(*fullName)); - if (fullName == nullptr) return std::nullopt; - error = GetCurrentPackageFullName(&length, fullName); + std::vector fullName; + error = GetCurrentPackageFullName(&length, fullName.data()); if (error != ERROR_SUCCESS) return std::nullopt; - free(fullName); return true; } From 8a87d175609437bb96f6fda02ae291a452c5f9c2 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Thu, 31 Oct 2024 20:53:28 -0400 Subject: [PATCH 097/112] Updated pubspec to include Flutter and updated platform_interface --- flutter_local_notifications_windows/pubspec.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter_local_notifications_windows/pubspec.yaml b/flutter_local_notifications_windows/pubspec.yaml index cd4eeea9e..54580e8d3 100644 --- a/flutter_local_notifications_windows/pubspec.yaml +++ b/flutter_local_notifications_windows/pubspec.yaml @@ -5,10 +5,11 @@ homepage: https://github.com/MaikuB/flutter_local_notifications/tree/master/flut environment: sdk: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" dependencies: ffi: ^2.1.2 - flutter_local_notifications_platform_interface: ^7.2.0 + flutter_local_notifications_platform_interface: ^8.1.0 meta: ^1.11.0 timezone: ^0.9.4 xml: ^6.5.0 From 6336bb4d7a2f6f6614721fe526e831b48aae4e81 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Sun, 3 Nov 2024 03:42:59 -0500 Subject: [PATCH 098/112] Test renaming repo --- .github/workflows/validate.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4db688a17..0abf11bdc 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -5,9 +5,9 @@ on: paths-ignore: - '**.md' push: - branches: - - master - - hotfix/* + # branches: + # - master + # - hotfix/* paths-ignore: - '**.md' @@ -181,6 +181,10 @@ jobs: channel: stable cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' + - name: Rename directory + run: | + move flutter_local_notifications repo + dir - name: Install Tools run: | dart pub global activate melos From b2bfed30863c7687147e7b156013a5f0d1851192 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Sun, 3 Nov 2024 03:49:02 -0500 Subject: [PATCH 099/112] Shorter names --- .github/workflows/validate.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0abf11bdc..b95fb545d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -181,16 +181,24 @@ jobs: channel: stable cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - - name: Rename directory - run: | - move flutter_local_notifications repo - dir - name: Install Tools run: | dart pub global activate melos melos bootstrap + - name: Rename directory + run: | + move flutter_local_notifications f + move f\example f\e + dir + dir f + dir f\e - name: Build - run: melos run build:example_windows + # run: melos run build:example_windows + run: | + cd f\e + dart pub get + dart run msix:create + build_example_windows_3_19: name: Build Windows example app (3.19) runs-on: windows-latest From 4fe2c7f4f7d967b30cdfba9151933f20b1f38d13 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Sun, 3 Nov 2024 03:59:14 -0500 Subject: [PATCH 100/112] Shorter names on Windows build version and stable --- .github/workflows/validate.yml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b95fb545d..f9e6bb3f7 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -5,9 +5,9 @@ on: paths-ignore: - '**.md' push: - # branches: - # - master - # - hotfix/* + branches: + - master + - hotfix/* paths-ignore: - '**.md' @@ -185,6 +185,8 @@ jobs: run: | dart pub global activate melos melos bootstrap + # Windows has a filename length limit, which this repo just hits + # This saves us precious characters during the compilation - name: Rename directory run: | move flutter_local_notifications f @@ -193,12 +195,10 @@ jobs: dir f dir f\e - name: Build - # run: melos run build:example_windows run: | cd f\e dart pub get dart run msix:create - build_example_windows_3_19: name: Build Windows example app (3.19) runs-on: windows-latest @@ -214,8 +214,17 @@ jobs: run: | dart pub global activate melos melos bootstrap + # Windows has a filename length limit, which this repo just hits + # This saves us precious characters during the compilation + - name: Rename directory + run: | + move flutter_local_notifications f + move f\example f\e - name: Build - run: melos run build:example_windows + run: | + cd f\e + dart pub get + dart run msix:create unit_tests_dart: name: Run all unit tests (Dart) runs-on: ubuntu-latest From 10169a69fdf5ab3eacedd1f26b583204663ae69f Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 12 Nov 2024 04:30:45 -0500 Subject: [PATCH 101/112] Added clang-format to _windows --- .github/workflows/format.yml | 9 + .../src/.clang-format | 41 +++ .../src/ffi_api.cpp | 38 ++- .../src/ffi_api.h | 15 +- .../src/plugin.cpp | 298 ++++++++---------- .../src/plugin.hpp | 14 +- .../src/utils.cpp | 22 +- 7 files changed, 230 insertions(+), 207 deletions(-) create mode 100644 flutter_local_notifications_windows/src/.clang-format diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index aa29fb2c5..d488246f5 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -34,3 +34,12 @@ jobs: which swiftlint || brew install swiftlint swiftlint --fix git diff --exit-code || (git commit --all -m "Swift Format" && git push) + + windows_cpp_format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Format C++ code + run: | + cd flutter_local_notifications_windows/src + clang-format *.cpp *.hpp *.h -i -n -Werror diff --git a/flutter_local_notifications_windows/src/.clang-format b/flutter_local_notifications_windows/src/.clang-format new file mode 100644 index 000000000..36cbc351c --- /dev/null +++ b/flutter_local_notifications_windows/src/.clang-format @@ -0,0 +1,41 @@ +BasedOnStyle: GNU +AlignAfterOpenBracket: BlockIndent +AlignConsecutiveDeclarations: None +AlignOperands: DontAlign +AllowAllParametersOfDeclarationOnNextLine: false +AllowAllArgumentsOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +BreakBeforeBraces: Attach +# This requires a newer clang-format +# BinPackParameters: AlwaysOnePerLine +BreakBeforeTernaryOperators: true +BreakBeforeBinaryOperators: NonAssignment +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterColon +ColumnLimit: 100 +ContinuationIndentWidth: 2 +Cpp11BracedListStyle: true +KeepEmptyLinesAtTheStartOfBlocks: false +Language: Cpp +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +PenaltyBreakAssignment: 200 +PenaltyExcessCharacter: 100 +PenaltyIndentedWhitespace: 200 +PenaltyReturnTypeOnItsOwnLine: 1000 +PointerAlignment: Left +QualifierAlignment: Left +SortIncludes: false +SpaceAfterCStyleCast: true +SpaceBeforeCpp11BracedList: true +SpaceBeforeInheritanceColon: true +SpacesBeforeTrailingComments: 2 +SpaceBeforeParens: ControlStatements +SpacesInContainerLiterals: false +Standard: Cpp11 diff --git a/flutter_local_notifications_windows/src/ffi_api.cpp b/flutter_local_notifications_windows/src/ffi_api.cpp index 9d4f33dc2..33bd3b1e7 100644 --- a/flutter_local_notifications_windows/src/ffi_api.cpp +++ b/flutter_local_notifications_windows/src/ffi_api.cpp @@ -8,15 +8,14 @@ using winrt::Windows::Data::Xml::Dom::XmlDocument; -NativePlugin* createPlugin() { - return new NativePlugin(); -} +NativePlugin* createPlugin() { return new NativePlugin(); } -void disposePlugin(NativePlugin* plugin) { - delete plugin; -} +void disposePlugin(NativePlugin* plugin) { delete plugin; } -bool init(NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, NativeNotificationCallback callback) { +bool init( + NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, + NativeNotificationCallback callback +) { string icon; if (iconPath != nullptr) icon = string(iconPath); const auto didRegister = plugin->registerApp(aumId, appName, guid, icon, callback); @@ -52,8 +51,11 @@ bool showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap b bool scheduleNotification(NativePlugin* plugin, int id, char* xml, int time) { if (!plugin->isReady) return false; XmlDocument doc; - try { doc.LoadXml(winrt::to_hstring(xml)); } - catch (winrt::hresult_error error) { return false; } + try { + doc.LoadXml(winrt::to_hstring(xml)); + } catch (winrt::hresult_error error) { + return false; + } ScheduledToastNotification notification(doc, winrt::clock::from_time_t(time)); notification.Tag(winrt::to_hstring(id)); plugin->notifier.value().AddToSchedule(notification); @@ -94,7 +96,10 @@ void cancelNotification(NativePlugin* plugin, int id) { NativeNotificationDetails* getActiveNotifications(NativePlugin* plugin, int* size) { // TODO: Get more details here - if (!plugin->isReady || !plugin->hasIdentity) { *size = 0; return nullptr; } + if (!plugin->isReady || !plugin->hasIdentity) { + *size = 0; + return nullptr; + } const auto active = plugin->history.value().GetHistory(); *size = active.Size(); const auto result = new NativeNotificationDetails[*size]; @@ -110,7 +115,10 @@ NativeNotificationDetails* getActiveNotifications(NativePlugin* plugin, int* siz NativeNotificationDetails* getPendingNotifications(NativePlugin* plugin, int* size) { // TODO: Get more details here - if (!plugin->isReady) { *size = 0; return nullptr; } + if (!plugin->isReady) { + *size = 0; + return nullptr; + } const auto pending = plugin->notifier.value().GetScheduledToastNotifications(); *size = pending.Size(); const auto result = new NativeNotificationDetails[*size]; @@ -124,9 +132,7 @@ NativeNotificationDetails* getPendingNotifications(NativePlugin* plugin, int* si return result; } -void freeDetailsArray(NativeNotificationDetails* ptr) { - delete[] ptr; -} +void freeDetailsArray(NativeNotificationDetails* ptr) { delete[] ptr; } void freeLaunchDetails(NativeLaunchDetails details) { if (details.payload != nullptr) delete[] details.payload; @@ -138,6 +144,4 @@ void freeLaunchDetails(NativeLaunchDetails details) { if (details.data.entries != nullptr) delete[] details.data.entries; } -void enableMultithreading() { - CoInitializeEx(nullptr, COINIT_MULTITHREADED); -} +void enableMultithreading() { CoInitializeEx(nullptr, COINIT_MULTITHREADED); } diff --git a/flutter_local_notifications_windows/src/ffi_api.h b/flutter_local_notifications_windows/src/ffi_api.h index 2bb427d66..7aaadfaf4 100644 --- a/flutter_local_notifications_windows/src/ffi_api.h +++ b/flutter_local_notifications_windows/src/ffi_api.h @@ -78,10 +78,16 @@ FFI_PLUGIN_EXPORT NativePlugin* createPlugin(); FFI_PLUGIN_EXPORT void disposePlugin(NativePlugin* ptr); /// Initializes the plugin and registers the callback to be run when a notification is pressed. -FFI_PLUGIN_EXPORT bool init(NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, NativeNotificationCallback callback); +FFI_PLUGIN_EXPORT bool init( + NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath, + NativeNotificationCallback callback +); -/// Shows the XML as a notification with the given ID. See [updateNotification] for details on bindings. -FFI_PLUGIN_EXPORT bool showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap bindings); +/// Shows the XML as a notification with the given ID. See [updateNotification] for details on +/// bindings. +FFI_PLUGIN_EXPORT bool showNotification( + NativePlugin* plugin, int id, char* xml, NativeStringMap bindings +); /// Schedules the notification to be shown at the given time (as a [time_t]). FFI_PLUGIN_EXPORT bool scheduleNotification(NativePlugin* plugin, int id, char* xml, int time); @@ -91,7 +97,8 @@ FFI_PLUGIN_EXPORT bool scheduleNotification(NativePlugin* plugin, int id, char* /// String values in the `` element of the XML can be placeholders instead of values, /// for example, `{name}` and then call this function with a map with a `name` key, /// and any string value, and the notification will be updated with that value where `name` was. -FFI_PLUGIN_EXPORT NativeUpdateResult updateNotification(NativePlugin* plugin, int id, NativeStringMap bindings); +FFI_PLUGIN_EXPORT NativeUpdateResult +updateNotification(NativePlugin* plugin, int id, NativeStringMap bindings); /// Cancels all notifications. FFI_PLUGIN_EXPORT void cancelAll(NativePlugin* plugin); diff --git a/flutter_local_notifications_windows/src/plugin.cpp b/flutter_local_notifications_windows/src/plugin.cpp index 0dfd830b9..eafc22544 100644 --- a/flutter_local_notifications_windows/src/plugin.cpp +++ b/flutter_local_notifications_windows/src/plugin.cpp @@ -10,215 +10,183 @@ #include "utils.hpp" struct RegistryHandle { - using type = HKEY; + using type = HKEY; - static void close(type value) noexcept { - WINRT_VERIFY_(ERROR_SUCCESS, RegCloseKey(value)); - } + static void close(type value) noexcept { WINRT_VERIFY_(ERROR_SUCCESS, RegCloseKey(value)); } - static constexpr type invalid() noexcept { return nullptr; } + static constexpr type invalid() noexcept { return nullptr; } }; using RegistryKey = winrt::handle_type; /// This callback will be called when a notification sent by this plugin is clicked on. -struct NotificationActivationCallback : winrt::implements { - NativeNotificationCallback callback; - - HRESULT __stdcall Activate(LPCWSTR app, LPCWSTR args, NOTIFICATION_USER_INPUT_DATA const* data, ULONG count) noexcept final { - try { +struct NotificationActivationCallback : + winrt::implements { + NativeNotificationCallback callback; + + HRESULT __stdcall Activate( + LPCWSTR app, LPCWSTR args, NOTIFICATION_USER_INPUT_DATA const* data, ULONG count + ) noexcept final { + try { // Fill the data map vector entries; - for (ULONG i = 0; i < count; i++) { - auto item = data[i]; + for (ULONG i = 0; i < count; i++) { + auto item = data[i]; const std::string key = CW2A(item.Key); const std::string value = CW2A(item.Value); - const auto pair = StringMapEntry { toNativeString(key), toNativeString(value) }; + const auto pair = StringMapEntry {toNativeString(key), toNativeString(value)}; entries.push_back(pair); - } + } - const auto openedWithAction = args != nullptr; + const auto openedWithAction = args != nullptr; const auto payload = string(CW2A(args)); - const auto launchType = openedWithAction ? NativeLaunchType::action : NativeLaunchType::notification; + const auto launchType = + openedWithAction ? NativeLaunchType::action : NativeLaunchType::notification; NativeLaunchDetails launchDetails; launchDetails.didLaunch = true; launchDetails.launchType = launchType; launchDetails.payload = toNativeString(payload); launchDetails.data = toNativeMap(entries); callback(launchDetails); - return S_OK; - } catch (...) { - return winrt::to_hresult(); - } - } + return S_OK; + } catch (...) { + return winrt::to_hresult(); + } + } }; /// A class factory that creates an instance of NotificationActivationCallback. -struct NotificationActivationCallbackFactory : winrt::implements { - NativeNotificationCallback callback; - - HRESULT __stdcall CreateInstance(IUnknown* outer, GUID const& iid, void** result) noexcept final { - *result = nullptr; - if (outer) return CLASS_E_NOAGGREGATION; - const auto cb = winrt::make_self(); - cb.get()->callback = callback; - return cb->QueryInterface(iid, result); - } - - HRESULT __stdcall LockServer(BOOL) noexcept final { return S_OK; } +struct NotificationActivationCallbackFactory : + winrt::implements { + NativeNotificationCallback callback; + + HRESULT __stdcall CreateInstance(IUnknown* outer, GUID const& iid, void** result) noexcept final { + *result = nullptr; + if (outer) return CLASS_E_NOAGGREGATION; + const auto cb = winrt::make_self(); + cb.get()->callback = callback; + return cb->QueryInterface(iid, result); + } + + HRESULT __stdcall LockServer(BOOL) noexcept final { return S_OK; } }; /// Updates the Registry to enable notifications. /// -/// Related resources: https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast-other-apps +/// Related resources: +/// https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast-other-apps void UpdateRegistry( - const std::string& aumid, - const std::string& appName, - const std::string& guid, - const std::optional& iconPath + const std::string& aumid, const std::string& appName, const std::string& guid, + const std::optional& iconPath ) { - std::stringstream ss; - ss << "Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications\\Backup\\" << aumid; - const auto notifSettingsKeyPath = ss.str(); - RegistryKey key; - - // create registry key - // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\PushNotifications\Backup - winrt::check_win32(RegCreateKeyExA( - HKEY_CURRENT_USER, - notifSettingsKeyPath.c_str(), - 0, - nullptr, - 0, - KEY_WRITE, - nullptr, - key.put(), - nullptr)); - - // put the following key values under the key - // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\PushNotifications\Backup\ - // - // appType = app:desktop - // Setting = s:banner,s:toast,s:audio,c:toast,c:ringing - // wnsId = NonImmersivePackage - - const std::string appType = "app:desktop"; - const std::string setting = "s:banner,s:toast,s:audio,c:toast,c:ringing"; - const std::string wnsId = "NonImmersivePackage"; - winrt::check_win32(RegSetValueExA( - key.get(), - "appType", - 0, - REG_SZ, - reinterpret_cast(appType.c_str()), - static_cast(appType.size() + 1 * sizeof(char)))); - winrt::check_win32(RegSetValueExA( - key.get(), - "Setting", - 0, - REG_SZ, - reinterpret_cast(setting.c_str()), - static_cast(setting.size() + 1 * sizeof(char)))); - winrt::check_win32(RegSetValueExA( - key.get(), - "wnsId", - 0, - REG_SZ, - reinterpret_cast(wnsId.c_str()), - static_cast(wnsId.size() + 1 * sizeof(char)))); - - // now, we register app info to the Registry. - - ss.clear(); - ss.str(std::string()); - ss << "Software\\Classes\\AppUserModelId\\" << aumid; - const auto appInfoKeyPath = ss.str(); - RegistryKey appInfoKey; - - // create registry key - // HKEY_CURRENT_USER\Software\Classes\AppUserModelId\ - winrt::check_win32(RegCreateKeyExA( - HKEY_CURRENT_USER, - appInfoKeyPath.c_str(), - 0, - nullptr, - 0, - KEY_WRITE, - nullptr, - appInfoKey.put(), - nullptr)); - - winrt::check_win32(RegSetValueExA( - appInfoKey.get(), - "DisplayName", - 0, - REG_SZ, - reinterpret_cast(appName.c_str()), - static_cast(appName.size() + 1 * sizeof(char)))); - - if (iconPath.has_value()) { - const auto v = iconPath.value(); - winrt::check_win32(RegSetValueExA( - appInfoKey.get(), - "IconUri", - 0, - REG_SZ, - reinterpret_cast(v.c_str()), - static_cast(v.size() + 1 * sizeof(char)))); - } - - // combine guid to class id - ss.clear(); - ss.str(std::string()); - ss << '{' << guid << '}'; - const auto clsid = ss.str(); - - // register the guid of the notification activation callback - winrt::check_win32(RegSetValueExA( - appInfoKey.get(), - "CustomActivator", - 0, - REG_SZ, - reinterpret_cast(clsid.c_str()), - static_cast(clsid.size() + 1 * sizeof(char)))); + std::stringstream ss; + ss << "Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications\\Backup\\" << aumid; + const auto notifSettingsKeyPath = ss.str(); + RegistryKey key; + + // create registry key + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\PushNotifications\Backup + winrt::check_win32(RegCreateKeyExA( + HKEY_CURRENT_USER, notifSettingsKeyPath.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), + nullptr + )); + + // put the following key values under the key + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\PushNotifications\Backup\ + // + // appType = app:desktop + // Setting = s:banner,s:toast,s:audio,c:toast,c:ringing + // wnsId = NonImmersivePackage + + const std::string appType = "app:desktop"; + const std::string setting = "s:banner,s:toast,s:audio,c:toast,c:ringing"; + const std::string wnsId = "NonImmersivePackage"; + winrt::check_win32(RegSetValueExA( + key.get(), "appType", 0, REG_SZ, reinterpret_cast(appType.c_str()), + static_cast(appType.size() + 1 * sizeof(char)) + )); + winrt::check_win32(RegSetValueExA( + key.get(), "Setting", 0, REG_SZ, reinterpret_cast(setting.c_str()), + static_cast(setting.size() + 1 * sizeof(char)) + )); + winrt::check_win32(RegSetValueExA( + key.get(), "wnsId", 0, REG_SZ, reinterpret_cast(wnsId.c_str()), + static_cast(wnsId.size() + 1 * sizeof(char)) + )); + + // now, we register app info to the Registry. + + ss.clear(); + ss.str(std::string()); + ss << "Software\\Classes\\AppUserModelId\\" << aumid; + const auto appInfoKeyPath = ss.str(); + RegistryKey appInfoKey; + + // create registry key + // HKEY_CURRENT_USER\Software\Classes\AppUserModelId\ + winrt::check_win32(RegCreateKeyExA( + HKEY_CURRENT_USER, appInfoKeyPath.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, appInfoKey.put(), + nullptr + )); + + winrt::check_win32(RegSetValueExA( + appInfoKey.get(), "DisplayName", 0, REG_SZ, reinterpret_cast(appName.c_str()), + static_cast(appName.size() + 1 * sizeof(char)) + )); + + if (iconPath.has_value()) { + const auto v = iconPath.value(); + winrt::check_win32(RegSetValueExA( + appInfoKey.get(), "IconUri", 0, REG_SZ, reinterpret_cast(v.c_str()), + static_cast(v.size() + 1 * sizeof(char)) + )); + } + + // combine guid to class id + ss.clear(); + ss.str(std::string()); + ss << '{' << guid << '}'; + const auto clsid = ss.str(); + + // register the guid of the notification activation callback + winrt::check_win32(RegSetValueExA( + appInfoKey.get(), "CustomActivator", 0, REG_SZ, reinterpret_cast(clsid.c_str()), + static_cast(clsid.size() + 1 * sizeof(char)) + )); } /// Register the notification activation callback factory /// and the guid of the callback. bool RegisterCallback(const std::string& guid, NativeNotificationCallback callback) { - DWORD registration{}; - winrt::guid rclsid = parseGuid(guid); - const auto factory_ref = winrt::make_self(); - const auto factory = factory_ref.get(); - factory->callback = callback; - winrt::check_hresult(CoRegisterClassObject( - rclsid, - factory, - CLSCTX_LOCAL_SERVER, - REGCLS_MULTIPLEUSE, - ®istration - )); - return true; + DWORD registration {}; + winrt::guid rclsid = parseGuid(guid); + const auto factory_ref = winrt::make_self(); + const auto factory = factory_ref.get(); + factory->callback = callback; + winrt::check_hresult( + CoRegisterClassObject(rclsid, factory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, ®istration) + ); + return true; } bool NativePlugin::registerApp( - const string& aumid, - const string& appName, - const string& guid, - const optional& iconPath, - NativeNotificationCallback callback + const string& aumid, const string& appName, const string& guid, const optional& iconPath, + NativeNotificationCallback callback ) { - UpdateRegistry(aumid, appName, guid, iconPath); - return RegisterCallback(guid, callback); + UpdateRegistry(aumid, appName, guid, iconPath); + return RegisterCallback(guid, callback); } std::optional NativePlugin::checkIdentity() { if (!IsWindows8OrGreater()) return false; uint32_t length = 0; auto error = GetCurrentPackageFullName(&length, nullptr); - if (error == APPMODEL_ERROR_NO_PACKAGE) return false; - else if (error != ERROR_INSUFFICIENT_BUFFER) return std::nullopt; - std::vector fullName; + if (error == APPMODEL_ERROR_NO_PACKAGE) { + return false; + } else if (error != ERROR_INSUFFICIENT_BUFFER) { + return std::nullopt; + } + std::vector fullName; error = GetCurrentPackageFullName(&length, fullName.data()); if (error != ERROR_SUCCESS) return std::nullopt; return true; diff --git a/flutter_local_notifications_windows/src/plugin.hpp b/flutter_local_notifications_windows/src/plugin.hpp index 662bd59a0..fba730a50 100644 --- a/flutter_local_notifications_windows/src/plugin.hpp +++ b/flutter_local_notifications_windows/src/plugin.hpp @@ -23,7 +23,8 @@ struct NativePlugin { /// Whether the current application has package identity (ie, was packaged with an MSIX). /// /// This impacts whether apps can query active notifications or cancel them. - /// For more details, see https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/package-identity-overview. + /// For more details, see + /// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/package-identity-overview. bool hasIdentity = false; /// The app user model ID. Used instead of package identity when [hasIdentity] is false. @@ -40,8 +41,8 @@ struct NativePlugin { /// A callback to run when a notification is pressed, when the app is or is not running. NativeNotificationCallback callback; - NativePlugin() { } - ~NativePlugin() { } + NativePlugin() {} + ~NativePlugin() {} /// Checks whether the current application has package identity. See [hasIdentity] for details. /// @@ -50,10 +51,7 @@ struct NativePlugin { /// Registers the given [callback] to run when a notification is pressed. bool registerApp( - const string& aumid, - const string& appName, - const string& guid, - const optional& iconPath, - NativeNotificationCallback callback + const string& aumid, const string& appName, const string& guid, + const optional& iconPath, NativeNotificationCallback callback ); }; diff --git a/flutter_local_notifications_windows/src/utils.cpp b/flutter_local_notifications_windows/src/utils.cpp index a90bae3c9..cccb7e46b 100644 --- a/flutter_local_notifications_windows/src/utils.cpp +++ b/flutter_local_notifications_windows/src/utils.cpp @@ -14,7 +14,7 @@ NativeStringMap toNativeMap(vector entries) { const auto size = (int) entries.size(); const auto array = new StringMapEntry[size]; std::copy(entries.begin(), entries.end(), array); - return { array, size }; + return {array, size}; } NotificationData dataFromMap(NativeStringMap map) { @@ -27,7 +27,7 @@ NotificationData dataFromMap(NativeStringMap map) { return data; } -constexpr uint8_t hex_to_uint(char const c) { +constexpr uint8_t hex_to_uint(const char c) { if (c >= '0' && c <= '9') { return static_cast(c - '0'); } else if (c >= 'A' && c <= 'F') { @@ -39,7 +39,7 @@ constexpr uint8_t hex_to_uint(char const c) { } } -constexpr uint8_t hex_to_uint8(char const a, char const b) { +constexpr uint8_t hex_to_uint8(const char a, const char b) { return (hex_to_uint(a) << 4) | hex_to_uint(b); } @@ -48,8 +48,8 @@ constexpr uint16_t uint8_to_uint16(uint8_t a, uint8_t b) { } constexpr uint32_t uint8_to_uint32(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { - return (static_cast(uint8_to_uint16(a, b)) << 16) | - static_cast(uint8_to_uint16(c, d)); + return (static_cast(uint8_to_uint16(a, b)) << 16) + | static_cast(uint8_to_uint16(c, d)); } winrt::guid parseGuid(const std::string& guidString) { @@ -58,18 +58,14 @@ winrt::guid parseGuid(const std::string& guidString) { } return { uint8_to_uint32( - hex_to_uint8(guidString[0], guidString[1]), - hex_to_uint8(guidString[2], guidString[3]), - hex_to_uint8(guidString[4], guidString[5]), - hex_to_uint8(guidString[6], guidString[7]) + hex_to_uint8(guidString[0], guidString[1]), hex_to_uint8(guidString[2], guidString[3]), + hex_to_uint8(guidString[4], guidString[5]), hex_to_uint8(guidString[6], guidString[7]) ), uint8_to_uint16( - hex_to_uint8(guidString[9], guidString[10]), - hex_to_uint8(guidString[11], guidString[12]) + hex_to_uint8(guidString[9], guidString[10]), hex_to_uint8(guidString[11], guidString[12]) ), uint8_to_uint16( - hex_to_uint8(guidString[14], guidString[15]), - hex_to_uint8(guidString[16], guidString[17]) + hex_to_uint8(guidString[14], guidString[15]), hex_to_uint8(guidString[16], guidString[17]) ), { hex_to_uint8(guidString[19], guidString[20]), From 7a399d7a58d303f4a435bf432bf3a3bf09e727b5 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 12 Nov 2024 04:31:27 -0500 Subject: [PATCH 102/112] Fixed yaml --- .github/workflows/format.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index d488246f5..54fe9efd4 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -35,7 +35,7 @@ jobs: swiftlint --fix git diff --exit-code || (git commit --all -m "Swift Format" && git push) - windows_cpp_format + windows_cpp_format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 077c25d5c333e3c94ef79097fb1ddf606dead3ac Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 12 Nov 2024 04:37:26 -0500 Subject: [PATCH 103/112] Print clang format version --- .github/workflows/format.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 54fe9efd4..1cae8fb2e 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -42,4 +42,5 @@ jobs: - name: Format C++ code run: | cd flutter_local_notifications_windows/src + clang-format --version clang-format *.cpp *.hpp *.h -i -n -Werror From ce9765d4f437d6e5474f804310d4e37b92fe7a0b Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 12 Nov 2024 04:45:26 -0500 Subject: [PATCH 104/112] Latest clang-format --- .github/workflows/format.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 1cae8fb2e..be794c683 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -39,6 +39,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + + - name: Install latest clang-format + run: sudo apt install clang-format-17 + - name: Format C++ code run: | cd flutter_local_notifications_windows/src From 5c1e4dc2cdc0a9443ff8006593677b80ef70d2e2 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 12 Nov 2024 04:46:07 -0500 Subject: [PATCH 105/112] sudo apt update --- .github/workflows/format.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index be794c683..a0332ca94 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -41,7 +41,9 @@ jobs: - uses: actions/checkout@v4 - name: Install latest clang-format - run: sudo apt install clang-format-17 + run: | + sudo apt update + sudo apt install clang-format-17 - name: Format C++ code run: | From b1fc604230cd25a156f627d7cbfae7067ee31f8a Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 12 Nov 2024 04:52:44 -0500 Subject: [PATCH 106/112] Add universe to apt --- .github/workflows/format.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index a0332ca94..2d1b87faf 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -42,6 +42,7 @@ jobs: - name: Install latest clang-format run: | + sudo add-apt-repository universe sudo apt update sudo apt install clang-format-17 From 737a1d22b542f591dda428863c0759a513712588 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 12 Nov 2024 04:58:47 -0500 Subject: [PATCH 107/112] Use clang from pip for new version --- .github/workflows/format.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 2d1b87faf..af1e61a03 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -42,12 +42,11 @@ jobs: - name: Install latest clang-format run: | - sudo add-apt-repository universe - sudo apt update - sudo apt install clang-format-17 + sudo apt install python3-pip -y + python3 -m pip install clang-format - name: Format C++ code run: | cd flutter_local_notifications_windows/src - clang-format --version - clang-format *.cpp *.hpp *.h -i -n -Werror + ~/.local/bin/clang-format --version + ~/.local/bin/clang-format *.cpp *.hpp *.h -i -n -Werror From a93c5a1a66c04d4b249a323b9e6f2e91d996e095 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 12 Nov 2024 05:05:35 -0500 Subject: [PATCH 108/112] More formatting --- flutter_local_notifications_windows/src/.clang-format | 4 +--- flutter_local_notifications_windows/src/ffi_api.h | 8 ++++++-- flutter_local_notifications_windows/src/utils.cpp | 10 +++++++++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/flutter_local_notifications_windows/src/.clang-format b/flutter_local_notifications_windows/src/.clang-format index 36cbc351c..925c658b3 100644 --- a/flutter_local_notifications_windows/src/.clang-format +++ b/flutter_local_notifications_windows/src/.clang-format @@ -25,9 +25,7 @@ KeepEmptyLinesAtTheStartOfBlocks: false Language: Cpp MaxEmptyLinesToKeep: 1 NamespaceIndentation: All -PenaltyBreakAssignment: 200 -PenaltyExcessCharacter: 100 -PenaltyIndentedWhitespace: 200 +PenaltyExcessCharacter: 200 PenaltyReturnTypeOnItsOwnLine: 1000 PointerAlignment: Left QualifierAlignment: Left diff --git a/flutter_local_notifications_windows/src/ffi_api.h b/flutter_local_notifications_windows/src/ffi_api.h index 7aaadfaf4..8cfbfa8b4 100644 --- a/flutter_local_notifications_windows/src/ffi_api.h +++ b/flutter_local_notifications_windows/src/ffi_api.h @@ -112,10 +112,14 @@ FFI_PLUGIN_EXPORT void cancelNotification(NativePlugin* plugin, int id); /// /// Only applications with "package identity" (ie, installed with an MSIX installer), can use this. /// When your app does not have identity, such as in debug mode, this will return an empty array. -FFI_PLUGIN_EXPORT NativeNotificationDetails* getActiveNotifications(NativePlugin* plugin, int* size); +FFI_PLUGIN_EXPORT NativeNotificationDetails* getActiveNotifications( + NativePlugin* plugin, int* size +); /// Gets all notifications that have been scheduled but not yet shown. -FFI_PLUGIN_EXPORT NativeNotificationDetails* getPendingNotifications(NativePlugin* plugin, int* size); +FFI_PLUGIN_EXPORT NativeNotificationDetails* getPendingNotifications( + NativePlugin* plugin, int* size +); /// Releases the memory associated with a [NativeNotificationDetails] array. FFI_PLUGIN_EXPORT void freeDetailsArray(NativeNotificationDetails* ptr); diff --git a/flutter_local_notifications_windows/src/utils.cpp b/flutter_local_notifications_windows/src/utils.cpp index cccb7e46b..83c55fd07 100644 --- a/flutter_local_notifications_windows/src/utils.cpp +++ b/flutter_local_notifications_windows/src/utils.cpp @@ -53,9 +53,17 @@ constexpr uint32_t uint8_to_uint32(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { } winrt::guid parseGuid(const std::string& guidString) { - if (guidString.size() != 36 || guidString[8] != '-' || guidString[13] != '-' || guidString[18] != '-' || guidString[23] != '-') { + // clang-format off + if ( + guidString.size() != 36 + || guidString[8] != '-' + || guidString[13] != '-' + || guidString[18] != '-' + || guidString[23] != '-' + ) { throw std::invalid_argument("guidString is not a valid GUID string"); } + // clang-format on return { uint8_to_uint32( hex_to_uint8(guidString[0], guidString[1]), hex_to_uint8(guidString[2], guidString[3]), From 64e23f4a9213b53fd4ec5b73cead8ab70b996996 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 12 Nov 2024 05:34:48 -0500 Subject: [PATCH 109/112] Commit back any changes that are made by clang --- .github/workflows/format.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index af1e61a03..938419028 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -49,4 +49,5 @@ jobs: run: | cd flutter_local_notifications_windows/src ~/.local/bin/clang-format --version - ~/.local/bin/clang-format *.cpp *.hpp *.h -i -n -Werror + ~/.local/bin/clang-format *.cpp *.hpp *.h -i + git diff --exit-code || (git commit --all -m "Clang Format" && git push) From 46813bd49ce8ca2400a549532d3c2bff2f44c0a2 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 12 Nov 2024 05:36:06 -0500 Subject: [PATCH 110/112] Bad formatting to lure clang into re-formatting --- flutter_local_notifications_windows/src/utils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_local_notifications_windows/src/utils.cpp b/flutter_local_notifications_windows/src/utils.cpp index 83c55fd07..67eca1882 100644 --- a/flutter_local_notifications_windows/src/utils.cpp +++ b/flutter_local_notifications_windows/src/utils.cpp @@ -65,7 +65,7 @@ winrt::guid parseGuid(const std::string& guidString) { } // clang-format on return { - uint8_to_uint32( + uint8_to_uint32 ( hex_to_uint8(guidString[0], guidString[1]), hex_to_uint8(guidString[2], guidString[3]), hex_to_uint8(guidString[4], guidString[5]), hex_to_uint8(guidString[6], guidString[7]) ), From dcf7efd9968325befe8fe9bff21cc403747a7c45 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Tue, 12 Nov 2024 05:43:31 -0500 Subject: [PATCH 111/112] Use auto-commit action --- .github/workflows/format.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 938419028..33f600688 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -37,6 +37,8 @@ jobs: windows_cpp_format: runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v4 @@ -50,4 +52,8 @@ jobs: cd flutter_local_notifications_windows/src ~/.local/bin/clang-format --version ~/.local/bin/clang-format *.cpp *.hpp *.h -i - git diff --exit-code || (git commit --all -m "Clang Format" && git push) + # git diff --exit-code || (git commit --all -m "Clang Format" && git push) + + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: Clang format From 46a24f6b68affd0d5a84f33468adb4206ec62c23 Mon Sep 17 00:00:00 2001 From: Levi-Lesches Date: Tue, 12 Nov 2024 10:43:49 +0000 Subject: [PATCH 112/112] Clang format --- flutter_local_notifications_windows/src/utils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_local_notifications_windows/src/utils.cpp b/flutter_local_notifications_windows/src/utils.cpp index 67eca1882..83c55fd07 100644 --- a/flutter_local_notifications_windows/src/utils.cpp +++ b/flutter_local_notifications_windows/src/utils.cpp @@ -65,7 +65,7 @@ winrt::guid parseGuid(const std::string& guidString) { } // clang-format on return { - uint8_to_uint32 ( + uint8_to_uint32( hex_to_uint8(guidString[0], guidString[1]), hex_to_uint8(guidString[2], guidString[3]), hex_to_uint8(guidString[4], guidString[5]), hex_to_uint8(guidString[6], guidString[7]) ),