From 099012cfd3e1f4985c433336204f3625aa49a240 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sun, 29 Mar 2026 13:56:50 -0500 Subject: [PATCH] Initial commit --- .github/workflows/release.yml | 166 ++ .gitignore | 171 ++ Notify.NET.sln | 25 + README.md | 381 +++++ native/MacNotifyWrapper/MacNotifyWrapper.h | 127 ++ native/MacNotifyWrapper/MacNotifyWrapper.m | 444 +++++ native/MacNotifyWrapper/Makefile | 57 + native/WinToastWrapper/WinToastWrapper.cpp | 332 ++++ native/WinToastWrapper/WinToastWrapper.h | 142 ++ .../WinToastWrapper/WinToastWrapper.vcxproj | 92 + native/WinToastWrapper/vendor/wintoastlib.cpp | 1492 +++++++++++++++++ native/WinToastWrapper/vendor/wintoastlib.h | 318 ++++ .../Notify.NET.Sample.csproj | 20 + samples/Notify.NET.Sample/Program.cs | 153 ++ samples/Notify.NET.Sample/image.jpg | Bin 0 -> 168087 bytes .../Abstractions/INotificationHandler.cs | 26 + .../Abstractions/INotificationService.cs | 33 + .../Abstractions/NotificationRequest.cs | 99 ++ src/Notify.NET/Builder/NotificationBuilder.cs | 202 +++ src/Notify.NET/Builder/NotificationButton.cs | 36 + .../Exceptions/NotificationException.cs | 22 + .../PlatformNotSupportedException.cs | 20 + .../Extensions/ServiceCollectionExtensions.cs | 117 ++ src/Notify.NET/Notify.NET.csproj | 49 + .../Platform/Linux/GLibMainLoopRunner.cs | 125 ++ .../Platform/Linux/LibNotifyCallbackBridge.cs | 152 ++ .../Platform/Linux/LibNotifyNative.cs | 172 ++ .../Linux/LinuxNotificationService.cs | 274 +++ .../Platform/MacOS/MacNotifyCallbackBridge.cs | 153 ++ .../Platform/MacOS/MacNotifyNative.cs | 91 + .../MacOS/MacOSNativeLibraryLoader.cs | 94 ++ .../MacOS/MacOSNotificationService.cs | 302 ++++ .../Platform/Windows/NativeLibraryLoader.cs | 93 + .../Platform/Windows/WinToastHandlerBridge.cs | 158 ++ .../Platform/Windows/WinToastNative.cs | 120 ++ .../Windows/WindowsNotificationService.cs | 417 +++++ 36 files changed, 6675 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Notify.NET.sln create mode 100644 README.md create mode 100644 native/MacNotifyWrapper/MacNotifyWrapper.h create mode 100644 native/MacNotifyWrapper/MacNotifyWrapper.m create mode 100644 native/MacNotifyWrapper/Makefile create mode 100644 native/WinToastWrapper/WinToastWrapper.cpp create mode 100644 native/WinToastWrapper/WinToastWrapper.h create mode 100644 native/WinToastWrapper/WinToastWrapper.vcxproj create mode 100644 native/WinToastWrapper/vendor/wintoastlib.cpp create mode 100644 native/WinToastWrapper/vendor/wintoastlib.h create mode 100644 samples/Notify.NET.Sample/Notify.NET.Sample.csproj create mode 100644 samples/Notify.NET.Sample/Program.cs create mode 100644 samples/Notify.NET.Sample/image.jpg create mode 100644 src/Notify.NET/Abstractions/INotificationHandler.cs create mode 100644 src/Notify.NET/Abstractions/INotificationService.cs create mode 100644 src/Notify.NET/Abstractions/NotificationRequest.cs create mode 100644 src/Notify.NET/Builder/NotificationBuilder.cs create mode 100644 src/Notify.NET/Builder/NotificationButton.cs create mode 100644 src/Notify.NET/Exceptions/NotificationException.cs create mode 100644 src/Notify.NET/Exceptions/PlatformNotSupportedException.cs create mode 100644 src/Notify.NET/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Notify.NET/Notify.NET.csproj create mode 100644 src/Notify.NET/Platform/Linux/GLibMainLoopRunner.cs create mode 100644 src/Notify.NET/Platform/Linux/LibNotifyCallbackBridge.cs create mode 100644 src/Notify.NET/Platform/Linux/LibNotifyNative.cs create mode 100644 src/Notify.NET/Platform/Linux/LinuxNotificationService.cs create mode 100644 src/Notify.NET/Platform/MacOS/MacNotifyCallbackBridge.cs create mode 100644 src/Notify.NET/Platform/MacOS/MacNotifyNative.cs create mode 100644 src/Notify.NET/Platform/MacOS/MacOSNativeLibraryLoader.cs create mode 100644 src/Notify.NET/Platform/MacOS/MacOSNotificationService.cs create mode 100644 src/Notify.NET/Platform/Windows/NativeLibraryLoader.cs create mode 100644 src/Notify.NET/Platform/Windows/WinToastHandlerBridge.cs create mode 100644 src/Notify.NET/Platform/Windows/WinToastNative.cs create mode 100644 src/Notify.NET/Platform/Windows/WindowsNotificationService.cs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..43da5aa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,166 @@ +# .github/workflows/release.yml +# +# Triggered by a semver tag (e.g. v1.2.3). +# +# Jobs: +# build-windows — compiles WinToastWrapper.dll for win-x64 / win-x86 / win-arm64 +# using MSBuild on a Windows runner. +# build-macos — compiles libMacNotifyWrapper.dylib for osx-arm64 and osx-x64 +# using clang cross-compilation on a macOS runner. +# pack — downloads all native artifacts, packs the NuGet package with the +# tag version, creates a GitHub Release, and optionally pushes to +# NuGet.org (requires the NUGET_API_KEY repository secret). + +name: Release + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + +permissions: + contents: write # required to create GitHub Releases + +# --------------------------------------------------------------------------- +# Windows native build — three architectures in parallel +# --------------------------------------------------------------------------- +jobs: + build-windows: + name: Build WinToastWrapper (${{ matrix.rid }}) + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - { platform: x64, rid: win-x64 } + - { platform: Win32, rid: win-x86 } + - { platform: ARM64, rid: win-arm64 } + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Build WinToastWrapper (${{ matrix.platform }}) + run: | + msbuild native\WinToastWrapper\WinToastWrapper.vcxproj ` + /p:Configuration=Release ` + /p:Platform=${{ matrix.platform }} ` + /m ` + /nologo + + # The vcxproj OutDir already targets runtimes//native/ relative to the + # repo root, so the DLL lands in the right place after a successful build. + - name: Upload native artifact + uses: actions/upload-artifact@v4 + with: + name: native-${{ matrix.rid }} + path: runtimes/${{ matrix.rid }}/native/WinToastWrapper.dll + if-no-files-found: error + +# --------------------------------------------------------------------------- +# macOS native build — both architectures on one runner via clang cross-compile +# --------------------------------------------------------------------------- + build-macos: + name: Build MacNotifyWrapper (osx-arm64 + osx-x64) + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # The Makefile targets arm64-apple-macos11.0 and x86_64-apple-macos10.14 + # using -arch flags; cross-compilation works on both Intel and Apple Silicon + # runners because Xcode ships cross-compilers for both targets. + - name: Build dylibs and install to runtimes/ + run: make -C native/MacNotifyWrapper install + + - name: Upload osx-arm64 artifact + uses: actions/upload-artifact@v4 + with: + name: native-osx-arm64 + path: runtimes/osx-arm64/native/libMacNotifyWrapper.dylib + if-no-files-found: error + + - name: Upload osx-x64 artifact + uses: actions/upload-artifact@v4 + with: + name: native-osx-x64 + path: runtimes/osx-x64/native/libMacNotifyWrapper.dylib + if-no-files-found: error + +# --------------------------------------------------------------------------- +# NuGet pack and GitHub Release +# --------------------------------------------------------------------------- + pack: + name: Pack NuGet and publish release + needs: [build-windows, build-macos] + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Strip the leading 'v' from the tag (v1.2.3 → 1.2.3) for use as the + # NuGet package version and .NET assembly version. + - name: Extract version from tag + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" + + # Download every native-* artifact into artifact-staging// + # Each artifact contains a single file at its root (no subdirectory). + - name: Download native artifacts + uses: actions/download-artifact@v4 + with: + pattern: native-* + path: artifact-staging/ + + # Reconstruct the runtimes/ tree that the .csproj Content items reference. + # The Condition="Exists(...)" guards in the .csproj will only bundle a file + # if it is present here, so all five binaries must be staged before packing. + - name: Stage native binaries into runtimes/ + run: | + mkdir -p \ + runtimes/win-x64/native \ + runtimes/win-x86/native \ + runtimes/win-arm64/native \ + runtimes/osx-x64/native \ + runtimes/osx-arm64/native + cp artifact-staging/native-win-x64/WinToastWrapper.dll runtimes/win-x64/native/ + cp artifact-staging/native-win-x86/WinToastWrapper.dll runtimes/win-x86/native/ + cp artifact-staging/native-win-arm64/WinToastWrapper.dll runtimes/win-arm64/native/ + cp artifact-staging/native-osx-x64/libMacNotifyWrapper.dylib runtimes/osx-x64/native/ + cp artifact-staging/native-osx-arm64/libMacNotifyWrapper.dylib runtimes/osx-arm64/native/ + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.x' + + # -p:Version overrides the hardcoded version in the .csproj for this build. + # --no-restore is safe here because we only need the managed code compiled; + # the native binaries are staged directly without a restore step. + - name: Pack NuGet package + run: | + dotnet pack src/Notify.NET/Notify.NET.csproj \ + --configuration Release \ + -p:Version=${{ env.VERSION }} \ + --output ./nupkg + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: ./nupkg/*.nupkg + generate_release_notes: true + fail_on_unmatched_files: true + + # Requires the NUGET_API_KEY repository secret to be configured. + # Skip this step silently if the secret is absent. + - name: Push to NuGet.org + if: ${{ secrets.NUGET_API_KEY != '' }} + run: | + dotnet nuget push ./nupkg/*.nupkg \ + --api-key ${{ secrets.NUGET_API_KEY }} \ + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20886d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,171 @@ +### C++ +# Object files +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Dynamic libraries +*.so +*.dylib +*.dll + +# Static libraries +*.a +*.lib + +# Executables +*.exe +*.out + +# Linker output +*.ilk +*.map +*.exp + +# Debug files +*.dSYM/ +*.idb +*.pdb + +### C# +# Build results +[Bb]in/ +[Oo]bj/ + +# Visual Studio cache/options directory +.vs/ + +# User-specific files +*.suo +*.user + +# Debug symbols +*.pdb + +# NuGet Packages +*.nupkg +**/[Pp]ackages/* +!**/[Pp]ackages/build/ + +# .NET build artifacts +artifacts/ + +# Publish output +publish/ + +### Objective-C +# Xcode user settings +xcuserdata/ + +# Xcode build data +DerivedData/ + +# Obj-C/Swift specific +*.hmap + +# App packaging +*.ipa +*.dSYM.zip +*.dSYM + +# Playgrounds +timeline.xctimeline +playground.xcworkspace + +### macOS +# Finder metadata +.DS_Store + +# Thumbnails +._* + +# Custom folder icons +Icon + +# Volume root files +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +### Windows +# Windows thumbnail cache files +Thumbs.db + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows shortcuts +*.lnk + +### Linux +# Backup files +*~ + +# Temporary files from deleted open files +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder +.Trash-* + +# NFS temporary files +.nfs* + +### JetBrains +# JetBrains IDE directory +.idea/ + +# CMake build directories +cmake-build-*/ + +# File-based project format +*.iws +*.iml + +# IntelliJ build output +out/ + +### VisualStudio +# User-specific files +*.suo +*.user + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NuGet +*.nupkg +**/[Pp]ackages/* +!**/[Pp]ackages/build/ +*.nuget.props +*.nuget.targets + +# Publish Web Output +*.[Pp]ublish.xml \ No newline at end of file diff --git a/Notify.NET.sln b/Notify.NET.sln new file mode 100644 index 0000000..b409878 --- /dev/null +++ b/Notify.NET.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Notify.NET", "src\Notify.NET\Notify.NET.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Notify.NET.Sample", "samples\Notify.NET.Sample\Notify.NET.Sample.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..1600a4f --- /dev/null +++ b/README.md @@ -0,0 +1,381 @@ +# Notify.NET + +A cross-platform .NET Standard library for displaying OS notifications. Provides a single +fluent API that dispatches to the native notification system on each supported platform. + +| Platform | Backend | Minimum OS | +|----------|---------|------------| +| Windows | [WinToastLib](https://github.com/mohabouje/WinToast) via a thin C++ wrapper DLL | Windows 8 | +| Linux | libnotify via P/Invoke | Any distribution with a D-Bus notification daemon | +| macOS | UNUserNotificationCenter via a thin Objective-C wrapper dylib | macOS 10.14 (Mojave) | + +--- + +## Installation + +``` +dotnet add package Notify.NET +``` + +The NuGet package includes the pre-compiled native libraries for all supported platforms. +No separate native installation is required. + +--- + +## Quick start + +```csharp +using Notify.NET.Builder; +using Notify.NET.Extensions; + +using var service = ServiceCollectionExtensions.CreateNotificationService(opts => +{ + opts.AppName = "My App"; + opts.AppUserModelId = "MyCompany.MyApp"; // Windows only; ignored on other platforms +}); + +if (service.IsSupported) +{ + long id = await service.ShowAsync( + NotificationBuilder.Create("Hello") + .WithBody("Notify.NET is working.") + .Build()); +} +``` + +--- + +## Creating the service + +### Without a DI container + +```csharp +using var service = ServiceCollectionExtensions.CreateNotificationService(opts => +{ + opts.AppName = "My App"; + opts.AppUserModelId = "MyCompany.MyApp"; +}); +``` + +`CreateNotificationService` selects the correct implementation for the current OS +automatically. On unsupported platforms it returns a no-op service where `IsSupported` +is `false`. + +### With Microsoft.Extensions.DependencyInjection + +```csharp +services.AddNotifications(opts => +{ + opts.AppName = "My App"; + opts.AppUserModelId = "MyCompany.MyApp"; +}); +``` + +This registers `INotificationService` as a singleton. Resolve it normally: + +```csharp +var service = provider.GetRequiredService(); +``` + +### Checking platform support + +Always check `IsSupported` before calling `ShowAsync`. On unsupported platforms or when +initialisation fails (e.g. the user denied notification permission on macOS), `ShowAsync` +throws `PlatformNotSupportedException`. + +```csharp +if (!service.IsSupported) +{ + Console.WriteLine("Notifications are not available on this platform."); + return; +} +``` + +--- + +## Builder API + +All notification configuration is done through `NotificationBuilder`. The `Build()` call +produces an immutable `NotificationRequest` that can be passed to `ShowAsync`. + +### Title and body + +```csharp +var request = NotificationBuilder.Create("Title goes here") + .WithBody("Optional body text goes here.") + .Build(); +``` + +### Image + +Pass an absolute path to an image file. Relative paths are resolved to absolute paths +at call time; if the file does not exist the notification is shown without an image +rather than failing. + +```csharp +var request = NotificationBuilder.Create("New photo") + .WithBody("A photo has arrived.") + .WithImage("/home/user/photos/latest.jpg") + .Build(); +``` + +Supported formats depend on the platform (PNG and JPEG work on all three). + +### Action buttons + +Up to five action buttons can be added. Each button has a label and an optional click +callback that receives the notification ID. + +```csharp +var request = NotificationBuilder.Create("Update available") + .WithBody("Version 2.0 is ready.") + .AddButton("Install now", id => Installer.Run()) + .AddButton("Remind me later", id => ScheduleReminder()) + .AddButton("Skip this version", null) + .Build(); +``` + +### Lifecycle callbacks + +Register delegates for specific notification events: + +```csharp +var request = NotificationBuilder.Create("Download complete") + .WithBody("report.pdf has been saved.") + .OnActivated(id => OpenFile("report.pdf")) + .OnDismissed((id, reason) => + { + if (reason == DismissReason.UserCancelled) + Console.WriteLine("User dismissed the notification."); + }) + .OnFailed(id => Console.WriteLine($"Notification {id} could not be displayed.")) + .Build(); +``` + +For more complex cases, implement `INotificationHandler` and attach it with `WithHandler`: + +```csharp +public sealed class DownloadHandler : INotificationHandler +{ + public void OnActivated(long id) => OpenDownloadFolder(); + public void OnButtonActivated(long id, int i) => HandleButton(i); + public void OnDismissed(long id, DismissReason r) { } + public void OnFailed(long id) => LogError(id); +} + +var request = NotificationBuilder.Create("Download complete") + .WithHandler(new DownloadHandler()) + .Build(); +``` + +`WithHandler` takes precedence over any delegate callbacks registered on the same builder. + +### Urgency + +```csharp +// Low priority — the platform may suppress or delay it +NotificationBuilder.Create("FYI").WithUrgency(NotificationUrgency.Low) + +// Critical — bypasses Do Not Disturb where the platform supports it +NotificationBuilder.Create("Disk full").WithUrgency(NotificationUrgency.Critical) + +// Windows-specific scenarios +NotificationBuilder.Create("Meeting in 5 minutes").WithUrgency(NotificationUrgency.Reminder) +NotificationBuilder.Create("Incoming call").WithUrgency(NotificationUrgency.Alarm) +``` + +| Value | Windows | Linux | macOS | +|-------|---------|-------|-------| +| `Normal` | Default scenario | Normal urgency | Active interruption level | +| `Low` | Default scenario | Low urgency | Passive interruption level | +| `Critical` | Default scenario | Critical urgency | Critical interruption level (macOS 12+) | +| `Alarm` | Alarm scenario | Critical urgency | Critical interruption level (macOS 12+) | +| `Reminder` | Reminder scenario | Normal urgency | Active interruption level | + +### Audio + +```csharp +NotificationBuilder.Create("Alert").WithAudio(NotificationAudio.Silent) +NotificationBuilder.Create("Alarm").WithAudio(NotificationAudio.Loop) // Windows only +``` + +| Value | Windows | Linux | macOS | +|-------|---------|-------|-------| +| `Default` | System notification sound | Controlled by daemon | System notification sound | +| `Silent` | No sound | No sound | No sound | +| `Loop` | Sound loops until dismissed | Treated as default | Treated as default | + +### Expiration + +Sets how long the notification is visible before it auto-dismisses. Pass `null` or omit +the call to use the platform default. + +```csharp +NotificationBuilder.Create("Reminder") + .WithExpiration(TimeSpan.FromSeconds(10)) + .Build(); +``` + +Note: `UNUserNotificationCenter` on macOS does not expose a per-notification timeout API; +this value is stored in the request but has no effect on macOS. + +--- + +## Showing and hiding notifications + +`ShowAsync` returns a `long` identifier for the notification. Pass this to `HideAsync` +to remove it programmatically before the user interacts with it. + +```csharp +long id = await service.ShowAsync(request); + +// Remove it after two seconds +await Task.Delay(TimeSpan.FromSeconds(2)); +await service.HideAsync(id); +``` + +--- + +## Callback threading + +Callbacks are fired on a background thread: + +- **Windows** — callbacks arrive on a WinRT thread-pool thread. +- **Linux** — callbacks arrive on the GLib main loop thread. +- **macOS** — callbacks arrive on a background GCD thread managed by + `UNUserNotificationCenter`. + +If you need to update UI elements from a callback, marshal the call to your UI thread +(e.g. `Dispatcher.InvokeAsync` on WPF, `Control.Invoke` on WinForms, or +`MainThread.BeginInvokeOnMainThread` on MAUI). + +--- + +## INotificationService interface + +```csharp +public interface INotificationService : IDisposable +{ + // False if the platform is unsupported or initialisation failed. + bool IsSupported { get; } + + // Shows a notification. Returns the notification ID on success. + // Throws PlatformNotSupportedException if IsSupported is false. + Task ShowAsync(NotificationRequest request, + CancellationToken cancellationToken = default); + + // Removes the notification from the notification centre. + Task HideAsync(long notificationId, + CancellationToken cancellationToken = default); +} +``` + +Dispose the service when your application exits to release the native resources +(WinToastLib STA thread on Windows, GLib main loop on Linux, UNUserNotificationCenter +cleanup on macOS). + +--- + +## Platform notes + +### Windows + +- Requires Windows 8 or later. `IsSupported` returns `false` on earlier versions. +- The `AppUserModelId` must match an application shortcut in the Start Menu. The native + wrapper creates this shortcut automatically on first run when it does not already exist, + but the shortcut creation requires that the process can write to the user's + `%APPDATA%\Microsoft\Windows\Start Menu\Programs` folder. +- `WinToastWrapper.dll` is loaded at runtime from `runtimes/win-/native/` relative + to the entry assembly. For published single-file applications, ensure the DLL is + published alongside the executable. +- Toast callbacks are delivered on a WinRT thread-pool thread, not the STA thread. The + library handles this internally. + +### Linux + +`libnotify` must be installed at runtime. Install it via your package manager: + +``` +# Debian / Ubuntu +sudo apt install libnotify4 + +# Fedora / RHEL +sudo dnf install libnotify + +# Arch +sudo pacman -S libnotify +``` + +A running D-Bus notification daemon is required (GNOME Shell, KDE Plasma, and most other +desktop environments provide one). Notifications will be silently dropped if no daemon is +running. `IsSupported` reflects whether `notify_init()` succeeded, not whether a daemon +is present. + +Image support via `gdk-pixbuf` requires `libgdk-pixbuf-2.0` to be installed, which is +typically included as a dependency of `libnotify4`. + +### macOS + +- Requires macOS 10.14 (Mojave) or later. +- On first use, macOS presents an authorisation dialog asking the user to allow + notifications. `MNW_Initialize` (called from the `MacOSNotificationService` constructor) + blocks until the user responds. `IsSupported` is `false` if permission was denied. +- The user can revoke permission at any time in System Settings > Notifications. Subsequent + calls to `ShowAsync` will fail silently (the OS will not display the notification and no + callback fires) until permission is re-granted. +- Action button callbacks and dismiss callbacks require the process to remain running after + the notification is shown, because `UNUserNotificationCenterDelegate` delivers responses + to the live process. If the process exits before the user interacts, the callbacks are + never fired. +- Non-bundled processes (bare `dotnet` CLI applications) can display banners but may not + always receive action callbacks on all OS versions. Wrap the application as an `.app` + bundle or sign it with an appropriate entitlement for reliable callback delivery in + production. +- The notification body-tap and button-tap events on macOS are terminal events — the + `OnDismissed` callback is not fired after the user activates a notification or clicks + a button (unlike Windows, where WinToastLib always fires the dismissed event after any + interaction). + +--- + +## Building the native libraries from source + +Pre-built native binaries are included in the NuGet package. You only need to build from +source if you are making changes to the native wrapper code. + +### Windows (WinToastWrapper.dll) + +Requires Visual Studio 2022 with the "Desktop development with C++" workload. + +``` +msbuild native\WinToastWrapper\WinToastWrapper.vcxproj /p:Configuration=Release /p:Platform=x64 +msbuild native\WinToastWrapper\WinToastWrapper.vcxproj /p:Configuration=Release /p:Platform=Win32 +msbuild native\WinToastWrapper\WinToastWrapper.vcxproj /p:Configuration=Release /p:Platform=ARM64 +``` + +Output is written to `runtimes\win-\native\WinToastWrapper.dll`. + +### macOS (libMacNotifyWrapper.dylib) + +Requires Xcode command-line tools (`xcode-select --install`). + +``` +make -C native/MacNotifyWrapper install +``` + +This cross-compiles both an `arm64` (Apple Silicon, macOS 11+) and an `x86_64` (Intel, +macOS 10.14+) slice and copies them to `runtimes/osx-arm64/native/` and +`runtimes/osx-x64/native/` respectively. + +To also produce a universal binary: + +``` +make -C native/MacNotifyWrapper universal +``` + +--- + +## License + +MIT. See [LICENSE](LICENSE) for details. +WinToastLib is copyright (c) mohabouje, also distributed under the MIT License. diff --git a/native/MacNotifyWrapper/MacNotifyWrapper.h b/native/MacNotifyWrapper/MacNotifyWrapper.h new file mode 100644 index 0000000..ec59e44 --- /dev/null +++ b/native/MacNotifyWrapper/MacNotifyWrapper.h @@ -0,0 +1,127 @@ +/** + * MacNotifyWrapper.h + * + * Flat C API for macOS User Notifications (UNUserNotificationCenter). + * Consumed by the Notify.NET managed library via P/Invoke. + * + * All strings are UTF-8, null-terminated. + * Callbacks fire on a background GCD thread managed by UNUserNotificationCenter. + * The caller must not free any memory passed to MNW_ShowNotification before it returns; + * the implementation copies all fields before returning. + */ + +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef MACNOTIFYWRAPPER_EXPORTS +# define MACNOTIFYAPI __attribute__((visibility("default"))) +#else +# define MACNOTIFYAPI +#endif + +/* ------------------------------------------------------------------------- + * Callback types — fired on a background thread from UNUserNotificationCenter. + * None of the callbacks should call back into MNW_* synchronously. + * ------------------------------------------------------------------------- */ +typedef void (*MNW_ActivatedCallback) (int64_t notifId); +typedef void (*MNW_ButtonActivatedCallback)(int64_t notifId, int buttonIndex); +typedef void (*MNW_DismissedCallback) (int64_t notifId, int reason); +typedef void (*MNW_FailedCallback) (int64_t notifId); + +/* ------------------------------------------------------------------------- + * Dismiss reasons (passed to MNW_DismissedCallback) + * ------------------------------------------------------------------------- */ +#define MNW_DISMISS_EXPIRED 0 /* Notification auto-expired (note: macOS does not fire this) */ +#define MNW_DISMISS_USER 1 /* User swiped or clicked "Close" */ +#define MNW_DISMISS_APP_REMOVED 2 /* Removed programmatically via MNW_HideNotification */ + +/* ------------------------------------------------------------------------- + * Audio options + * ------------------------------------------------------------------------- */ +#define MNW_AUDIO_DEFAULT 0 +#define MNW_AUDIO_SILENT 1 + +/* ------------------------------------------------------------------------- + * Interruption level (macOS 12+; silently ignored on earlier versions) + * ------------------------------------------------------------------------- */ +#define MNW_INTERRUPTION_ACTIVE 0 +#define MNW_INTERRUPTION_PASSIVE 1 +#define MNW_INTERRUPTION_TIME_SENSITIVE 2 +#define MNW_INTERRUPTION_CRITICAL 3 + +/* ------------------------------------------------------------------------- + * Handler — bundle of four callback function pointers, copied by value. + * Any pointer may be NULL to opt out of that event. + * ------------------------------------------------------------------------- */ +typedef struct { + MNW_ActivatedCallback onActivated; + MNW_ButtonActivatedCallback onButtonActivated; + MNW_DismissedCallback onDismissed; + MNW_FailedCallback onFailed; +} MNW_Handler; + +/* ------------------------------------------------------------------------- + * Notification descriptor. + * All pointer fields may be NULL / empty string where documented. + * The caller must keep all pointed-to memory valid until MNW_ShowNotification returns; + * the implementation deep-copies every string before returning. + * ------------------------------------------------------------------------- */ +typedef struct { + const char* title; /* Required, non-empty UTF-8 string */ + const char* body; /* Optional body text; NULL or "" → omitted */ + const char* imagePath; /* Optional absolute path to an image file */ + const char** buttonLabels; /* Optional array of buttonCount UTF-8 strings */ + int buttonCount; /* 0–5 */ + int64_t expirationMs; /* Reserved — UNUserNotificationCenter has no per-notification timeout API */ + int audioOption; /* MNW_AUDIO_* */ + int interruptionLevel; /* MNW_INTERRUPTION_* */ +} MNW_NotificationDescriptor; + +/* ------------------------------------------------------------------------- + * API + * ------------------------------------------------------------------------- */ + +/** + * Returns true if UNUserNotificationCenter is available (macOS 10.14+). + * Safe to call before MNW_Initialize. + */ +MACNOTIFYAPI bool MNW_IsSupported(void); + +/** + * Initialises the notification centre and requests authorisation (alert + sound + badge). + * Blocks until the user grants or denies the authorisation prompt (up to 30 s). + * Returns true if authorisation was granted; false if denied or unavailable. + * Safe to call multiple times; subsequent calls only re-check authorisation status. + */ +MACNOTIFYAPI bool MNW_Initialize(const char* appName); + +/** + * Removes all pending and delivered notifications posted by this process and frees + * all internal state. Call once before the process exits. + */ +MACNOTIFYAPI void MNW_Uninitialize(void); + +/** + * Posts a notification. Returns a positive opaque int64 identifier on success, + * or a negative value if the descriptor is invalid or the library is not initialised. + * The MNW_Handler callbacks will fire asynchronously from a background thread. + */ +MACNOTIFYAPI int64_t MNW_ShowNotification( + const MNW_NotificationDescriptor* descriptor, + const MNW_Handler* handler); + +/** + * Removes a pending or delivered notification by its ID. + * Fires onDismissed(MNW_DISMISS_APP_REMOVED) synchronously before returning. + * Returns true if the notification was found and removed. + */ +MACNOTIFYAPI bool MNW_HideNotification(int64_t notifId); + +#ifdef __cplusplus +} +#endif diff --git a/native/MacNotifyWrapper/MacNotifyWrapper.m b/native/MacNotifyWrapper/MacNotifyWrapper.m new file mode 100644 index 0000000..8fa09e9 --- /dev/null +++ b/native/MacNotifyWrapper/MacNotifyWrapper.m @@ -0,0 +1,444 @@ +/** + * MacNotifyWrapper.m + * + * Objective-C implementation of the flat C API declared in MacNotifyWrapper.h. + * Wraps UNUserNotificationCenter (macOS 10.14+). + * + * Compilation requirements: + * clang -fobjc-arc -fvisibility=hidden + * Frameworks: Foundation, UserNotifications + * Minimum deployment target: macOS 10.14 (for x86_64), macOS 11.0 (for arm64) + * + * Threading model: + * MNW_Initialize: blocks on a semaphore waiting for the authorisation dialog. + * MNW_ShowNotification / MNW_HideNotification: thread-safe; UNUserNotificationCenter + * internally serialises requests. + * Callbacks (onActivated, onButtonActivated, onDismissed, onFailed): invoked on a + * background GCD thread managed by UNUserNotificationCenter. Callers must not + * call back into MNW_* synchronously from these callbacks. + */ + +#define MACNOTIFYWRAPPER_EXPORTS +#import +#import +#include +#include +#include "MacNotifyWrapper.h" + +/* ------------------------------------------------------------------------- + * Per-notification heap state + * ------------------------------------------------------------------------- */ + +typedef struct { + int64_t notifId; + MNW_Handler handler; +} NotifState; + +/* ------------------------------------------------------------------------- + * Delegate + * ------------------------------------------------------------------------- */ + +@interface MNWDelegate : NSObject +@end + +/* ------------------------------------------------------------------------- + * Process-lifetime globals + * ------------------------------------------------------------------------- */ + +static MNWDelegate* g_delegate = nil; +static NSLock* g_lock = nil; +/* strId → NSValue wrapping NotifState* (heap-allocated) */ +static NSMutableDictionary* g_entries = nil; +/* int64 NSNumber → strId NSString */ +static NSMutableDictionary* g_idMap = nil; +/* category identifiers we have registered */ +static NSMutableSet* g_categoryIds = nil; +/* UNNotificationCategory objects corresponding to the above */ +static NSMutableSet* g_categoryObjs = nil; +static _Atomic(int64_t) g_counter = 1; +static bool g_initialized = false; + +/* ------------------------------------------------------------------------- + * Private helpers + * ------------------------------------------------------------------------- */ + +static NSString* StringIdFor(int64_t notifId) +{ + return [NSString stringWithFormat:@"mnw_%lld", (long long)notifId]; +} + +/* Stores a newly allocated NotifState in both lookup tables. */ +static void StoreEntry(NSString* strId, int64_t notifId, const MNW_Handler* handler) +{ + NotifState* s = (NotifState*)malloc(sizeof(NotifState)); + s->notifId = notifId; + s->handler = *handler; + NSValue* boxed = [NSValue valueWithPointer:s]; + [g_lock lock]; + g_entries[strId] = boxed; + g_idMap[@(notifId)] = strId; + [g_lock unlock]; +} + +/* + * Atomically removes the entry and returns a copy of its state. + * Returns NO if no entry exists (already removed by a prior event). + */ +static BOOL TakeEntry(NSString* strId, NotifState* outState) +{ + [g_lock lock]; + NSValue* val = g_entries[strId]; + if (!val) { [g_lock unlock]; return NO; } + + NotifState* s = (NotifState*)[val pointerValue]; + *outState = *s; + free(s); + [g_entries removeObjectForKey:strId]; + [g_idMap removeObjectForKey:@(outState->notifId)]; + [g_lock unlock]; + return YES; +} + +/* + * Builds a stable, reproducible category identifier from a set of button labels. + * The identifier must survive app restarts so that previously delivered notifications + * whose category was registered in an earlier session are still actionable. + */ +static NSString* BuildCategoryId(const char** labels, int count) +{ + if (count == 0) return @"mnw_default"; + + NSMutableString* s = [NSMutableString stringWithString:@"mnw"]; + for (int i = 0; i < count; i++) + [s appendFormat:@"_%s", labels[i]]; + + /* Hash long identifiers to keep the string short. */ + if (s.length > 80) + return [NSString stringWithFormat:@"mnw_%lu", (unsigned long)s.hash]; + + return [s copy]; +} + +/* + * Ensures a UNNotificationCategory with the given identifier is registered. + * No-op if the category was already registered this session. + * Thread-safe — uses g_lock. + */ +static NSString* EnsureCategory(const char** labels, int count) +{ + NSString* catId = BuildCategoryId(labels, count); + + [g_lock lock]; + BOOL known = [g_categoryIds containsObject:catId]; + [g_lock unlock]; + + if (known) return catId; + + /* Build the UNNotificationAction array. */ + NSMutableArray* actions = [NSMutableArray array]; + for (int i = 0; i < count; i++) { + NSString* actionId = [NSString stringWithFormat:@"btn_%d", i]; + NSString* title = [NSString stringWithUTF8String:labels[i]]; + [actions addObject:[UNNotificationAction + actionWithIdentifier:actionId + title:title + options:UNNotificationActionOptionNone]]; + } + + /* CustomDismissAction makes the delegate receive dismiss events. */ + UNNotificationCategory* category = [UNNotificationCategory + categoryWithIdentifier:catId + actions:actions + intentIdentifiers:@[] + options:UNNotificationCategoryOptionCustomDismissAction]; + + [g_lock lock]; + if (![g_categoryIds containsObject:catId]) { + [g_categoryIds addObject:catId]; + [g_categoryObjs addObject:category]; + /* setNotificationCategories replaces the full set; pass our accumulated set. */ + NSSet* snapshot = [g_categoryObjs copy]; + [[UNUserNotificationCenter currentNotificationCenter] + setNotificationCategories:snapshot]; + } + [g_lock unlock]; + + return catId; +} + +/* ------------------------------------------------------------------------- + * Delegate implementation + * ------------------------------------------------------------------------- */ + +@implementation MNWDelegate + +/* + * Called when a notification arrives while the app is in the foreground. + * Show the banner and play the sound so that console/background processes + * still see the notification visually. + */ +- (void)userNotificationCenter:(UNUserNotificationCenter*)center + willPresentNotification:(UNNotification*)notification + withCompletionHandler:(void(^)(UNNotificationPresentationOptions))completionHandler +{ + if (@available(macOS 12.0, *)) { + completionHandler(UNNotificationPresentationOptionBanner + | UNNotificationPresentationOptionSound + | UNNotificationPresentationOptionList); + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + completionHandler(UNNotificationPresentationOptionAlert + | UNNotificationPresentationOptionSound); +#pragma clang diagnostic pop + } +} + +/* + * Called when the user interacts with (or dismisses) a notification. + * This is the single delivery point for all notification responses; + * we route to the appropriate managed callback and then clean up. + * + * Note: on macOS, a body-tap or button-tap IS the terminal event. + * UNUserNotificationCenter does NOT subsequently fire a dismiss event + * after an action response. We therefore release after every interaction. + */ +- (void)userNotificationCenter:(UNUserNotificationCenter*)center + didReceiveNotificationResponse:(UNNotificationResponse*)response + withCompletionHandler:(void(^)(void))completionHandler +{ + NSString* strId = response.notification.request.identifier; + + NotifState state = {0}; + if (!TakeEntry(strId, &state)) { + /* Already handled (e.g. MNW_HideNotification was called first). */ + completionHandler(); + return; + } + + NSString* actionId = response.actionIdentifier; + + if ([actionId isEqualToString:UNNotificationDefaultActionIdentifier]) { + /* User tapped the notification body. */ + if (state.handler.onActivated) + state.handler.onActivated(state.notifId); + + } else if ([actionId isEqualToString:UNNotificationDismissActionIdentifier]) { + /* User dismissed (swipe/close). Requires CustomDismissAction on category. */ + if (state.handler.onDismissed) + state.handler.onDismissed(state.notifId, MNW_DISMISS_USER); + + } else if ([actionId hasPrefix:@"btn_"]) { + /* User tapped an action button. */ + int idx = (int)[[actionId substringFromIndex:4] integerValue]; + if (state.handler.onButtonActivated) + state.handler.onButtonActivated(state.notifId, idx); + + } else { + /* Unknown action — treat as activation. */ + if (state.handler.onActivated) + state.handler.onActivated(state.notifId); + } + + completionHandler(); +} + +@end + +/* ------------------------------------------------------------------------- + * API implementation + * ------------------------------------------------------------------------- */ + +bool MNW_IsSupported(void) +{ + if (@available(macOS 10.14, *)) return true; + return false; +} + +bool MNW_Initialize(const char* appName) +{ + (void)appName; /* appName is informational; the bundle identifier governs delivery. */ + + if (!MNW_IsSupported()) return false; + + /* One-time setup of global state. */ + static dispatch_once_t once; + dispatch_once(&once, ^{ + g_lock = [[NSLock alloc] init]; + g_entries = [NSMutableDictionary dictionary]; + g_idMap = [NSMutableDictionary dictionary]; + g_categoryIds = [NSMutableSet set]; + g_categoryObjs = [NSMutableSet set]; + g_delegate = [[MNWDelegate alloc] init]; + [[UNUserNotificationCenter currentNotificationCenter] setDelegate:g_delegate]; + + /* Pre-register the default (no-button) category. */ + EnsureCategory(NULL, 0); + }); + + if (g_initialized) { + /* On re-initialisation just re-check authorisation status. */ + __block bool ok = false; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + [[UNUserNotificationCenter currentNotificationCenter] + getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings* s) { + ok = (s.authorizationStatus == UNAuthorizationStatusAuthorized + || s.authorizationStatus == UNAuthorizationStatusProvisional); + dispatch_semaphore_signal(sem); + }]; + dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)); + return ok; + } + + /* Request authorisation. Blocks until the user responds (or times out). */ + __block bool granted = false; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + [[UNUserNotificationCenter currentNotificationCenter] + requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + | UNAuthorizationOptionSound + | UNAuthorizationOptionBadge) + completionHandler:^(BOOL g, NSError* __unused err) { + granted = (bool)g; + dispatch_semaphore_signal(sem); + }]; + dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + + g_initialized = granted; + return granted; +} + +void MNW_Uninitialize(void) +{ + if (!g_lock) return; + + /* Remove all notifications posted by this process. */ + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; + [center removeAllPendingNotificationRequests]; + [center removeAllDeliveredNotifications]; + + /* Free all live state without firing callbacks. */ + [g_lock lock]; + for (NSValue* val in g_entries.allValues) { + NotifState* s = (NotifState*)[val pointerValue]; + free(s); + } + [g_entries removeAllObjects]; + [g_idMap removeAllObjects]; + [g_lock unlock]; + + g_initialized = false; +} + +int64_t MNW_ShowNotification( + const MNW_NotificationDescriptor* descriptor, + const MNW_Handler* handler) +{ + if (!descriptor || !handler) return -1; + if (!descriptor->title || descriptor->title[0]=='\0') return -2; + if (!g_lock) return -3; /* Not initialised */ + + int64_t notifId = atomic_fetch_add_explicit(&g_counter, 1, memory_order_relaxed); + NSString* strId = StringIdFor(notifId); + + /* --- Content --------------------------------------------------------- */ + UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; + content.title = [NSString stringWithUTF8String:descriptor->title]; + + if (descriptor->body && descriptor->body[0] != '\0') + content.body = [NSString stringWithUTF8String:descriptor->body]; + + /* --- Sound ----------------------------------------------------------- */ + content.sound = (descriptor->audioOption == MNW_AUDIO_SILENT) + ? nil + : [UNNotificationSound defaultSound]; + + /* --- Interruption level (macOS 12+) ---------------------------------- */ + if (@available(macOS 12.0, *)) { + switch (descriptor->interruptionLevel) { + case MNW_INTERRUPTION_PASSIVE: + content.interruptionLevel = UNNotificationInterruptionLevelPassive; + break; + case MNW_INTERRUPTION_TIME_SENSITIVE: + content.interruptionLevel = UNNotificationInterruptionLevelTimeSensitive; + break; + case MNW_INTERRUPTION_CRITICAL: + content.interruptionLevel = UNNotificationInterruptionLevelCritical; + break; + default: + content.interruptionLevel = UNNotificationInterruptionLevelActive; + break; + } + } + + /* --- Image attachment ------------------------------------------------ */ + if (descriptor->imagePath && descriptor->imagePath[0] != '\0') { + NSString* path = [NSString stringWithUTF8String:descriptor->imagePath]; + NSURL* url = [NSURL fileURLWithPath:path]; + NSError* err = nil; + UNNotificationAttachment* att = [UNNotificationAttachment + attachmentWithIdentifier:@"image" URL:url options:nil error:&err]; + if (att) + content.attachments = @[att]; + /* If attachment fails (file missing, unsupported format), continue without it. */ + } + + /* --- Category / buttons --------------------------------------------- */ + content.categoryIdentifier = EnsureCategory(descriptor->buttonLabels, + descriptor->buttonCount); + + /* --- Trigger --------------------------------------------------------- */ + /* + * UNTimeIntervalNotificationTrigger requires timeInterval > 0. + * Use 0.1 s for "fire immediately" — imperceptible to the user. + */ + UNTimeIntervalNotificationTrigger* trigger = + [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.1 repeats:NO]; + + /* --- Register state BEFORE scheduling so no callback is missed ------- */ + StoreEntry(strId, notifId, handler); + + /* --- Schedule -------------------------------------------------------- */ + UNNotificationRequest* req = [UNNotificationRequest + requestWithIdentifier:strId + content:content + trigger:trigger]; + + [[UNUserNotificationCenter currentNotificationCenter] + addNotificationRequest:req + withCompletionHandler:^(NSError* error) { + if (!error) return; + /* Delivery failed — fire onFailed and clean up. */ + NotifState state = {0}; + if (TakeEntry(strId, &state)) { + if (state.handler.onFailed) + state.handler.onFailed(state.notifId); + } + }]; + + return notifId; +} + +bool MNW_HideNotification(int64_t notifId) +{ + if (!g_lock) return false; + + [g_lock lock]; + NSString* strId = g_idMap[@(notifId)]; + [g_lock unlock]; + + if (!strId) return false; + + NSArray* ids = @[strId]; + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; + [center removePendingNotificationRequestsWithIdentifiers:ids]; + [center removeDeliveredNotificationsWithIdentifiers:ids]; + + /* Fire dismissed callback synchronously before returning. */ + NotifState state = {0}; + if (TakeEntry(strId, &state)) { + if (state.handler.onDismissed) + state.handler.onDismissed(state.notifId, MNW_DISMISS_APP_REMOVED); + } + + return true; +} diff --git a/native/MacNotifyWrapper/Makefile b/native/MacNotifyWrapper/Makefile new file mode 100644 index 0000000..f7f922e --- /dev/null +++ b/native/MacNotifyWrapper/Makefile @@ -0,0 +1,57 @@ +# Makefile for MacNotifyWrapper +# +# Builds a thin Objective-C dylib wrapping UNUserNotificationCenter. +# Produces separate arm64 and x86_64 slices, plus an optional universal binary. +# +# Usage: +# make # build all slices +# make install # copy dylibs into ../../runtimes/ +# make universal # also build universal binary +# make clean + +SDK := $(shell xcrun --show-sdk-path) +CC := clang +SRC := MacNotifyWrapper.m +CFLAGS := -fobjc-arc -fvisibility=hidden -O2 -Wall -Wextra \ + -isysroot $(SDK) +LDFLAGS := -dynamiclib \ + -framework Foundation \ + -framework UserNotifications \ + -install_name @rpath/libMacNotifyWrapper.dylib + +ARM64_TARGET := arm64-apple-macos11.0 +X64_TARGET := x86_64-apple-macos10.14 + +OUT_ARM64 := osx-arm64/libMacNotifyWrapper.dylib +OUT_X64 := osx-x64/libMacNotifyWrapper.dylib +OUT_UNIVERSAL := osx-universal/libMacNotifyWrapper.dylib + +RUNTIME_ARM64 := ../../runtimes/osx-arm64/native +RUNTIME_X64 := ../../runtimes/osx-x64/native + +.PHONY: all universal install clean + +all: $(OUT_ARM64) $(OUT_X64) + +universal: $(OUT_UNIVERSAL) + +$(OUT_ARM64): $(SRC) MacNotifyWrapper.h + mkdir -p osx-arm64 + $(CC) $(CFLAGS) -arch arm64 -target $(ARM64_TARGET) $(LDFLAGS) -o $@ $< + +$(OUT_X64): $(SRC) MacNotifyWrapper.h + mkdir -p osx-x64 + $(CC) $(CFLAGS) -arch x86_64 -target $(X64_TARGET) $(LDFLAGS) -o $@ $< + +$(OUT_UNIVERSAL): $(OUT_ARM64) $(OUT_X64) + mkdir -p osx-universal + lipo -create -output $@ $(OUT_ARM64) $(OUT_X64) + +install: $(OUT_ARM64) $(OUT_X64) + mkdir -p $(RUNTIME_ARM64) $(RUNTIME_X64) + cp $(OUT_ARM64) $(RUNTIME_ARM64)/ + cp $(OUT_X64) $(RUNTIME_X64)/ + @echo "Installed to runtimes/" + +clean: + rm -rf osx-arm64 osx-x64 osx-universal diff --git a/native/WinToastWrapper/WinToastWrapper.cpp b/native/WinToastWrapper/WinToastWrapper.cpp new file mode 100644 index 0000000..dcf0097 --- /dev/null +++ b/native/WinToastWrapper/WinToastWrapper.cpp @@ -0,0 +1,332 @@ +/** + * WinToastWrapper.cpp + * + * Implementation of the flat C API declared in WinToastWrapper.h. + * Links against WinToastLib (wintoastlib.h / wintoastlib.cpp from mohabouje/WinToast). + * + * Compilation requirements: + * - MSVC 2019 or later, C++17 + * - /W3 /EHsc /MT (static CRT to avoid runtime dependency) + * - Link: Ole32.lib Shlwapi.lib Shell32.lib + * - _WIN32_WINNT >= 0x0602 (Windows 8) + * - WINTOASTWRAPPER_EXPORTS defined in the DLL project + * + * Threading model: + * WNT_Initialize and WNT_ShowToast MUST be called from an STA thread. + * Callbacks fire on a WinRT thread-pool thread — they must not call back into + * WNT_ShowToast or WNT_HideToast directly; the .NET side re-queues on the STA. + */ + +#define WINTOASTWRAPPER_EXPORTS +#define NOMINMAX +#include +#include +#include +#include +#include +#include "WinToastWrapper.h" +#include "vendor/wintoastlib.h" + +// WinToast is a class inside the WinToastLib namespace — not a nested namespace. +// "using namespace WinToastLib" brings WinToast, WinToastTemplate, IWinToastHandler, +// WinToastError, etc. into the global scope. +using namespace WinToastLib; + +/* ------------------------------------------------------------------------- + * Handler implementation + * ------------------------------------------------------------------------- + * WinToastLib wraps the IWinToastHandler* in a std::shared_ptr + * at the start of showToast (wintoastlib.cpp line ~721), adopting ownership. + * WinToastLib will delete the handler when the shared_ptr is destroyed. + * We must NEVER delete a handler ourselves — doing so would cause a double-free. + * + * g_handlers is kept only for lookup (e.g. WNT_HideToast needs the handler for + * the toast-ID-to-handler mapping that WinToastLib itself tracks). Entries are + * removed from g_handlers when a toast ends, but the pointed-to object is never + * freed here. + * ------------------------------------------------------------------------- */ + +class WinToastHandlerImpl : public IWinToastHandler +{ +public: + INT64 m_toastId; + WNT_Handler m_handler; // copied by value + + WinToastHandlerImpl(INT64 toastId, const WNT_Handler& handler) + : m_toastId(toastId), m_handler(handler) {} + + // WinToastDismissalReason is a nested enum of IWinToastHandler. + // It is accessible by unqualified name within this derived class. + + void toastActivated() const override + { + if (m_handler.onActivated) + m_handler.onActivated(m_toastId); + } + + void toastActivated(int actionIndex) const override + { + if (m_handler.onButtonActivated) + m_handler.onButtonActivated(m_toastId, actionIndex); + } + + // Input response (text box) — treat as a plain activation for our purposes. + void toastActivated(std::wstring /*response*/) const override + { + if (m_handler.onActivated) + m_handler.onActivated(m_toastId); + } + + void toastDismissed(WinToastDismissalReason state) const override + { + if (m_handler.onDismissed) + { + int reason = 0; + switch (state) + { + case WinToastDismissalReason::UserCanceled: reason = 0; break; + case WinToastDismissalReason::ApplicationHidden: reason = 1; break; + case WinToastDismissalReason::TimedOut: reason = 2; break; + } + m_handler.onDismissed(m_toastId, reason); + } + ScheduleDelete(m_toastId); + } + + void toastFailed() const override + { + if (m_handler.onFailed) + m_handler.onFailed(m_toastId); + ScheduleDelete(m_toastId); + } + +private: + // Removes this toast from the lookup table when its lifecycle ends. + // Does NOT delete the handler — WinToastLib owns it via shared_ptr. + static void ScheduleDelete(INT64 toastId); +}; + +/* ------------------------------------------------------------------------- + * Global state + * ------------------------------------------------------------------------- */ + +static std::mutex g_mutex; +static std::unordered_map g_handlers; // live toasts (no ownership) + +// Removes the entry for toastId from g_handlers. +// Does NOT delete the handler — WinToastLib owns it via shared_ptr. +void WinToastHandlerImpl::ScheduleDelete(INT64 toastId) +{ + std::lock_guard lock(g_mutex); + g_handlers.erase(toastId); +} + +/* ------------------------------------------------------------------------- + * Helpers + * ------------------------------------------------------------------------- */ + +static WinToastTemplate::WinToastTemplateType SelectTemplateType( + bool hasImage, bool hasBody) +{ + if (hasImage) + return hasBody + ? WinToastTemplate::ImageAndText02 // image + title + body + : WinToastTemplate::ImageAndText01; // image + title only + + return hasBody + ? WinToastTemplate::Text02 // title + body + : WinToastTemplate::Text01; // title only +} + +// --------------------------------------------------------------------------- +// SEH-safe wrappers for WinToastLib calls that can trigger AVs from WinRT. +// +// MSVC rule: __try cannot appear in a function that has local objects with +// destructors (WinToastTemplate, std::lock_guard, std::wstring, etc.). +// These helpers contain ONLY plain scalars and raw pointers so __try is legal. +// Any structured exception (AV, invalid handle, COM fault) is caught here and +// converted to an error-code return, preventing it from ever reaching the CLR +// where a corrupted-state exception would terminate the process. +// --------------------------------------------------------------------------- + +// Calls WinToast::showToast with full SEH protection. +// tmpl is passed as a const pointer to avoid any destructor in this frame. +static INT64 SafeShowToast( + const WinToastTemplate* tmpl, + IWinToastHandler* handler, + WinToast::WinToastError* error) +{ + INT64 result = static_cast(WinToast::WinToastError::UnknownError); + __try + { + result = WinToast::instance()->showToast(*tmpl, handler, error); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + // Structured exception (AV, etc.) from WinRT internals — return failure. + result = static_cast(WinToast::WinToastError::UnknownError); + } + return result; +} + +// Calls WinToast::hideToast with full SEH protection. +static BOOL SafeHideToast(INT64 toastId) +{ + BOOL result = FALSE; + __try + { + result = WinToast::instance()->hideToast(toastId) ? TRUE : FALSE; + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + result = FALSE; + } + return result; +} + +/* ------------------------------------------------------------------------- + * Exported API + * ------------------------------------------------------------------------- */ + +extern "C" { + +NOTIFYAPI BOOL WNT_IsCompatible(void) +{ + return WinToast::isCompatible() ? TRUE : FALSE; +} + +NOTIFYAPI BOOL WNT_Initialize(const wchar_t* appName, const wchar_t* appUserModelId) +{ + if (!WinToast::isCompatible()) + return FALSE; + + WinToast* instance = WinToast::instance(); + instance->setAppName(appName); + instance->setAppUserModelId(appUserModelId); + + // SHORTCUT_POLICY_REQUIRE_CREATE: create a Start-Menu shortcut with this AUMI + // automatically if one does not already exist. Required for unpackaged (Win32) + // apps so that toasts persist in the Action Centre between sessions. + instance->setShortcutPolicy(WinToast::SHORTCUT_POLICY_REQUIRE_CREATE); + + WinToast::WinToastError error = WinToast::WinToastError::NoError; + if (!instance->initialize(&error)) + return FALSE; + + { + std::lock_guard lock(g_mutex); + // Clear lookup table from a previous Initialize/Uninitialize cycle. + // Do NOT delete handlers — WinToastLib owns them via shared_ptr. + g_handlers.clear(); + } + + return TRUE; +} + +NOTIFYAPI void WNT_Uninitialize(void) +{ + std::lock_guard lock(g_mutex); + // Do NOT delete handlers — WinToastLib owns them via shared_ptr. + g_handlers.clear(); +} + +NOTIFYAPI INT64 WNT_ShowToast( + const WNT_ToastDescriptor* descriptor, + const WNT_Handler* handler) +{ + if (!descriptor || !handler) + return static_cast(WinToast::WinToastError::InvalidParameters); + + bool hasBody = descriptor->body != nullptr && descriptor->body[0] != L'\0'; + bool hasImage = descriptor->imagePath != nullptr && descriptor->imagePath[0] != L'\0'; + + WinToastTemplate tmpl(SelectTemplateType(hasImage, hasBody)); + + tmpl.setTextField(descriptor->title, WinToastTemplate::FirstLine); + if (hasBody) + tmpl.setTextField(descriptor->body, WinToastTemplate::SecondLine); + + if (hasImage) + tmpl.setImagePath(descriptor->imagePath); + + for (int i = 0; i < descriptor->buttonCount; ++i) + { + if (descriptor->buttonLabels && descriptor->buttonLabels[i]) + tmpl.addAction(descriptor->buttonLabels[i]); + } + + if (descriptor->expirationMs > 0) + tmpl.setExpiration(descriptor->expirationMs); + + switch (descriptor->audioOption) + { + case WNT_AUDIO_SILENT: + tmpl.setAudioOption(WinToastTemplate::AudioOption::Silent); + break; + case WNT_AUDIO_LOOP: + tmpl.setAudioOption(WinToastTemplate::AudioOption::Loop); + break; + default: + tmpl.setAudioOption(WinToastTemplate::AudioOption::Default); + break; + } + + switch (descriptor->scenario) + { + case WNT_SCENARIO_ALARM: + tmpl.setScenario(WinToastTemplate::Scenario::Alarm); + break; + case WNT_SCENARIO_REMINDER: + tmpl.setScenario(WinToastTemplate::Scenario::Reminder); + break; + case WNT_SCENARIO_INCOMING_CALL: + tmpl.setScenario(WinToastTemplate::Scenario::IncomingCall); + break; + default: + tmpl.setScenario(WinToastTemplate::Scenario::Default); + break; + } + + // Allocate the handler. Use nothrow so a failed allocation returns an error + // code rather than throwing std::bad_alloc through the extern "C" boundary. + auto* handlerImpl = new (std::nothrow) WinToastHandlerImpl(0 /* filled in below */, *handler); + if (!handlerImpl) + return static_cast(WinToast::WinToastError::UnknownError); + + WinToast::WinToastError error = WinToast::WinToastError::NoError; + INT64 toastId = SafeShowToast(&tmpl, handlerImpl, &error); + + if (toastId < 0) + { + // showToast failed — WinToastLib created a shared_ptr internally which + // will delete handlerImpl when it destructs. Do NOT delete it here. + return toastId; // already the error code cast from WinToastError + } + + handlerImpl->m_toastId = toastId; + + { + std::lock_guard lock(g_mutex); + g_handlers[toastId] = handlerImpl; + } + + return toastId; +} + +NOTIFYAPI BOOL WNT_HideToast(INT64 toastId) +{ + BOOL ok = SafeHideToast(toastId); + + if (ok) + { + // hideToast may fire the ApplicationHidden dismissal callback synchronously, + // which calls ScheduleDelete and removes from g_handlers. Erase here too in + // case the callback was skipped. Do NOT delete — WinToastLib owns the handler. + std::lock_guard lock(g_mutex); + g_handlers.erase(toastId); + } + + return ok ? TRUE : FALSE; +} + +} /* extern "C" */ diff --git a/native/WinToastWrapper/WinToastWrapper.h b/native/WinToastWrapper/WinToastWrapper.h new file mode 100644 index 0000000..034d77c --- /dev/null +++ b/native/WinToastWrapper/WinToastWrapper.h @@ -0,0 +1,142 @@ +/** + * WinToastWrapper.h + * + * Flat C API that wraps WinToastLib (https://github.com/mohabouje/WinToast). + * This header is the sole interface between the managed P/Invoke layer and + * the C++ WinToastLib implementation. + * + * ABI contract (must stay in sync with WinToastNative.cs): + * - All strings are wchar_t* (UTF-16LE, null-terminated). + * - Callbacks are called on a WinRT thread-pool thread, NOT the calling STA thread. + * - WNT_ShowToast must be called from the STA thread that called WNT_Initialize. + * - WNT_HideToast must be called from the same STA thread. + */ + +#pragma once + +#include + +#ifdef WINTOASTWRAPPER_EXPORTS + #define NOTIFYAPI __declspec(dllexport) +#else + #define NOTIFYAPI __declspec(dllimport) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------------------------------------------------------------- + * Callback function pointer types + * ------------------------------------------------------------------------- */ + +/** Called when the user clicks the notification body (no button). */ +typedef void (CALLBACK* WNT_ActivatedCallback)(INT64 toastId); + +/** Called when the user clicks one of the action buttons. */ +typedef void (CALLBACK* WNT_ButtonActivatedCallback)(INT64 toastId, int buttonIndex); + +/** + * Called when the notification is dismissed. + * reason: 0 = UserCancelled, 1 = ApplicationHidden, 2 = TimedOut + */ +typedef void (CALLBACK* WNT_DismissedCallback)(INT64 toastId, int reason); + +/** Called when the notification fails to display. */ +typedef void (CALLBACK* WNT_FailedCallback)(INT64 toastId); + +/* ------------------------------------------------------------------------- + * Structs (must match StructLayout in WinToastNative.cs exactly) + * ------------------------------------------------------------------------- */ + +typedef enum _WNT_Scenario { + WNT_SCENARIO_DEFAULT = 0, + WNT_SCENARIO_ALARM = 1, + WNT_SCENARIO_REMINDER = 2, + WNT_SCENARIO_INCOMING_CALL = 3 +} WNT_Scenario; + +typedef enum _WNT_AudioOption { + WNT_AUDIO_DEFAULT = 0, + WNT_AUDIO_SILENT = 1, + WNT_AUDIO_LOOP = 2 +} WNT_AudioOption; + +/** + * Describes the notification to display. + * All pointer fields may be NULL where noted. + * Callers must keep pointed-to memory valid for the duration of WNT_ShowToast. + */ +typedef struct _WNT_ToastDescriptor { + const wchar_t* title; /* required */ + const wchar_t* body; /* nullable */ + const wchar_t* imagePath; /* nullable — absolute path to an image file */ + const wchar_t** buttonLabels; /* nullable — array of buttonCount wchar_t* */ + int buttonCount; + long long expirationMs; /* 0 = platform default */ + WNT_Scenario scenario; + WNT_AudioOption audioOption; +} WNT_ToastDescriptor; + +/** + * Bundle of callback function pointers for one notification. + * The struct is copied by value inside WNT_ShowToast; callers need not keep it alive. + * Individual function pointers may be NULL to skip that event. + */ +typedef struct _WNT_Handler { + WNT_ActivatedCallback onActivated; + WNT_ButtonActivatedCallback onButtonActivated; + WNT_DismissedCallback onDismissed; + WNT_FailedCallback onFailed; +} WNT_Handler; + +/* ------------------------------------------------------------------------- + * API functions + * ------------------------------------------------------------------------- */ + +/** + * Initialises WinToastLib. Must be called once from an STA thread. + * + * @param appName Human-readable name shown in the Action Centre. + * @param appUserModelId AppUserModelId (AUMI). The wrapper creates a Start-Menu + * shortcut carrying this AUMI automatically if one does not + * already exist. + * @return TRUE on success. + */ +NOTIFYAPI BOOL WNT_Initialize(const wchar_t* appName, const wchar_t* appUserModelId); + +/** + * Releases all WinToastLib resources. Call from the same STA thread as WNT_Initialize. + */ +NOTIFYAPI void WNT_Uninitialize(void); + +/** + * Returns TRUE if WinToast is supported on the current Windows version (requires Win 8+). + * Check this before calling WNT_Initialize. + */ +NOTIFYAPI BOOL WNT_IsCompatible(void); + +/** + * Displays a toast notification. + * + * Must be called from the STA thread that called WNT_Initialize. + * + * @param descriptor Pointer to a WNT_ToastDescriptor (read-only during the call). + * @param handler Pointer to a WNT_Handler with callback function pointers. + * @return A positive INT64 toast ID on success, or a negative WinToastError code on failure. + */ +NOTIFYAPI INT64 WNT_ShowToast(const WNT_ToastDescriptor* descriptor, const WNT_Handler* handler); + +/** + * Programmatically dismisses a previously shown toast. + * + * Must be called from the same STA thread as WNT_ShowToast. + * + * @param toastId The ID returned by WNT_ShowToast. + * @return TRUE if the toast was successfully hidden. + */ +NOTIFYAPI BOOL WNT_HideToast(INT64 toastId); + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/native/WinToastWrapper/WinToastWrapper.vcxproj b/native/WinToastWrapper/WinToastWrapper.vcxproj new file mode 100644 index 0000000..3ca25a0 --- /dev/null +++ b/native/WinToastWrapper/WinToastWrapper.vcxproj @@ -0,0 +1,92 @@ + + + + + + Release + Win32 + + + Release + x64 + + + Release + ARM64 + + + Debug + x64 + + + + + 17.0 + {C1D2E3F4-A5B6-7890-CDEF-012345678901} + WinToastWrapper + 10.0 + + + + + + DynamicLibrary + false + v143 + true + Unicode + + + + DynamicLibrary + true + v143 + Unicode + + + + + + + ..\..\runtimes\win-x64\native\ + ..\..\runtimes\win-x86\native\ + ..\..\runtimes\win-arm64\native\ + WinToastWrapper + + + + + Level3 + stdcpp17 + + WINTOASTWRAPPER_EXPORTS; + _WINDOWS; + _WIN32_WINNT=0x0602; + WIN32_LEAN_AND_MEAN; + NOMINMAX; + %(PreprocessorDefinitions) + + MultiThreaded + MultiThreadedDebug + Sync + $(ProjectDir)vendor;%(AdditionalIncludeDirectories) + + + Windows + Ole32.lib;Shlwapi.lib;Shell32.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + + diff --git a/native/WinToastWrapper/vendor/wintoastlib.cpp b/native/WinToastWrapper/vendor/wintoastlib.cpp new file mode 100644 index 0000000..4317740 --- /dev/null +++ b/native/WinToastWrapper/vendor/wintoastlib.cpp @@ -0,0 +1,1492 @@ +/** + * MIT License + * + * Copyright (C) 2016-2023 WinToast v1.3.0 - Mohammed Boujemaoui + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "wintoastlib.h" + +#include +#include +#include +#include +#include + +#pragma comment(lib, "shlwapi") +#pragma comment(lib, "user32") + +#define DEFAULT_SHELL_LINKS_PATH L"\\Microsoft\\Windows\\Start Menu\\Programs\\" +#define DEFAULT_LINK_FORMAT L".lnk" +#define STATUS_SUCCESS (0x00000000) + +#ifdef NDEBUG +static bool DebugOutputEnabled = false; +#else +static bool DebugOutputEnabled = true; +#endif + +#define DEBUG_MSG(str) \ + do { \ + if (DebugOutputEnabled) { \ + std::wcout << str << std::endl; \ + } \ + } while (false) + +// Quickstart: Handling toast activations from Win32 apps in Windows 10 +// https://blogs.msdn.microsoft.com/tiles_and_toasts/2015/10/16/quickstart-handling-toast-activations-from-win32-apps-in-windows-10/ +using namespace WinToastLib; + +void WinToastLib::setDebugOutputEnabled(bool enabled) { + DebugOutputEnabled = enabled; +} + +namespace DllImporter { + + // Function load a function from library + template + HRESULT loadFunctionFromLibrary(HINSTANCE library, LPCSTR name, Function& func) { + if (!library) { + return E_INVALIDARG; + } + func = reinterpret_cast(GetProcAddress(library, name)); + return (func != nullptr) ? S_OK : E_FAIL; + } + + typedef HRESULT(FAR STDAPICALLTYPE* f_SetCurrentProcessExplicitAppUserModelID)(__in PCWSTR AppID); + typedef HRESULT(FAR STDAPICALLTYPE* f_PropVariantToString)(_In_ REFPROPVARIANT propvar, _Out_writes_(cch) PWSTR psz, _In_ UINT cch); + typedef HRESULT(FAR STDAPICALLTYPE* f_RoGetActivationFactory)(_In_ HSTRING activatableClassId, _In_ REFIID iid, + _COM_Outptr_ void** factory); + typedef HRESULT(FAR STDAPICALLTYPE* f_WindowsCreateStringReference)(_In_reads_opt_(length + 1) PCWSTR sourceString, UINT32 length, + _Out_ HSTRING_HEADER* hstringHeader, + _Outptr_result_maybenull_ _Result_nullonfailure_ HSTRING* string); + typedef PCWSTR(FAR STDAPICALLTYPE* f_WindowsGetStringRawBuffer)(_In_ HSTRING string, _Out_opt_ UINT32* length); + typedef HRESULT(FAR STDAPICALLTYPE* f_WindowsDeleteString)(_In_opt_ HSTRING string); + + static f_SetCurrentProcessExplicitAppUserModelID SetCurrentProcessExplicitAppUserModelID; + static f_PropVariantToString PropVariantToString; + static f_RoGetActivationFactory RoGetActivationFactory; + static f_WindowsCreateStringReference WindowsCreateStringReference; + static f_WindowsGetStringRawBuffer WindowsGetStringRawBuffer; + static f_WindowsDeleteString WindowsDeleteString; + + template + __inline _Check_return_ HRESULT _1_GetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ T** factory) { + return RoGetActivationFactory(activatableClassId, IID_INS_ARGS(factory)); + } + + template + inline HRESULT Wrap_GetActivationFactory(_In_ HSTRING activatableClassId, _Inout_ Details::ComPtrRef factory) noexcept { + return _1_GetActivationFactory(activatableClassId, factory.ReleaseAndGetAddressOf()); + } + + inline HRESULT initialize() { + HINSTANCE LibShell32 = LoadLibraryW(L"SHELL32.DLL"); + HRESULT hr = + loadFunctionFromLibrary(LibShell32, "SetCurrentProcessExplicitAppUserModelID", SetCurrentProcessExplicitAppUserModelID); + if (SUCCEEDED(hr)) { + HINSTANCE LibPropSys = LoadLibraryW(L"PROPSYS.DLL"); + hr = loadFunctionFromLibrary(LibPropSys, "PropVariantToString", PropVariantToString); + if (SUCCEEDED(hr)) { + HINSTANCE LibComBase = LoadLibraryW(L"COMBASE.DLL"); + bool const succeded = + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "RoGetActivationFactory", RoGetActivationFactory)) && + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsCreateStringReference", WindowsCreateStringReference)) && + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsGetStringRawBuffer", WindowsGetStringRawBuffer)) && + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsDeleteString", WindowsDeleteString)); + return succeded ? S_OK : E_FAIL; + } + } + return hr; + } +} // namespace DllImporter + +class WinToastStringWrapper { +public: + WinToastStringWrapper(_In_reads_(length) PCWSTR stringRef, _In_ UINT32 length) noexcept { + HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef, length, &_header, &_hstring); + if (!SUCCEEDED(hr)) { + RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + } + + WinToastStringWrapper(_In_ std::wstring const& stringRef) noexcept { + HRESULT hr = + DllImporter::WindowsCreateStringReference(stringRef.c_str(), static_cast(stringRef.length()), &_header, &_hstring); + if (FAILED(hr)) { + RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + } + + ~WinToastStringWrapper() { + DllImporter::WindowsDeleteString(_hstring); + } + + inline HSTRING Get() const noexcept { + return _hstring; + } + +private: + HSTRING _hstring; + HSTRING_HEADER _header; +}; + +class InternalDateTime : public IReference { +public: + static INT64 Now() { + FILETIME now; + GetSystemTimeAsFileTime(&now); + return ((((INT64) now.dwHighDateTime) << 32) | now.dwLowDateTime); + } + + InternalDateTime(DateTime dateTime) : _dateTime(dateTime) {} + + InternalDateTime(INT64 millisecondsFromNow) { + _dateTime.UniversalTime = Now() + millisecondsFromNow * 10000; + } + + virtual ~InternalDateTime() = default; + + operator INT64() { + return _dateTime.UniversalTime; + } + + HRESULT STDMETHODCALLTYPE get_Value(DateTime* dateTime) { + *dateTime = _dateTime; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryInterface(const IID& riid, void** ppvObject) { + if (!ppvObject) { + return E_POINTER; + } + if (riid == __uuidof(IUnknown) || riid == __uuidof(IReference)) { + *ppvObject = static_cast(static_cast*>(this)); + return S_OK; + } + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE Release() { + return 1; + } + + ULONG STDMETHODCALLTYPE AddRef() { + return 2; + } + + HRESULT STDMETHODCALLTYPE GetIids(ULONG*, IID**) { + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE GetRuntimeClassName(HSTRING*) { + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE GetTrustLevel(TrustLevel*) { + return E_NOTIMPL; + } + +protected: + DateTime _dateTime; +}; + +namespace Util { + + typedef LONG NTSTATUS, *PNTSTATUS; + typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); + inline RTL_OSVERSIONINFOW getRealOSVersion() { + HMODULE hMod = ::GetModuleHandleW(L"ntdll.dll"); + if (hMod) { + RtlGetVersionPtr fxPtr = (RtlGetVersionPtr)::GetProcAddress(hMod, "RtlGetVersion"); + if (fxPtr != nullptr) { + RTL_OSVERSIONINFOW rovi = {0}; + rovi.dwOSVersionInfoSize = sizeof(rovi); + if (STATUS_SUCCESS == fxPtr(&rovi)) { + return rovi; + } + } + } + RTL_OSVERSIONINFOW rovi = {0}; + return rovi; + } + + inline HRESULT defaultExecutablePath(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + DWORD written = GetModuleFileNameExW(GetCurrentProcess(), nullptr, path, nSize); + DEBUG_MSG("Default executable path: " << path); + return (written > 0) ? S_OK : E_FAIL; + } + + inline HRESULT defaultShellLinksDirectory(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + DWORD written = GetEnvironmentVariableW(L"APPDATA", path, nSize); + HRESULT hr = written > 0 ? S_OK : E_INVALIDARG; + if (SUCCEEDED(hr)) { + errno_t result = wcscat_s(path, nSize, DEFAULT_SHELL_LINKS_PATH); + hr = (result == 0) ? S_OK : E_INVALIDARG; + DEBUG_MSG("Default shell link path: " << path); + } + return hr; + } + + inline HRESULT defaultShellLinkPath(_In_ std::wstring const& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + HRESULT hr = defaultShellLinksDirectory(path, nSize); + if (SUCCEEDED(hr)) { + const std::wstring appLink(appname + DEFAULT_LINK_FORMAT); + errno_t result = wcscat_s(path, nSize, appLink.c_str()); + hr = (result == 0) ? S_OK : E_INVALIDARG; + DEBUG_MSG("Default shell link file path: " << path); + } + return hr; + } + + inline std::wstring parentDirectory(WCHAR* path, DWORD size) { + size_t lastSeparator = 0; + for (size_t i = 0; i < size; i++) { + if (path[i] == L'\\' || path[i] == L'/') { + lastSeparator = i; + } + } + return {path, lastSeparator}; + } + + inline PCWSTR AsString(_In_ ComPtr& xmlDocument) { + HSTRING xml; + ComPtr ser; + HRESULT hr = xmlDocument.As(&ser); + hr = ser->GetXml(&xml); + if (SUCCEEDED(hr)) { + return DllImporter::WindowsGetStringRawBuffer(xml, nullptr); + } + return nullptr; + } + + inline PCWSTR AsString(_In_ HSTRING hstring) { + return DllImporter::WindowsGetStringRawBuffer(hstring, nullptr); + } + + inline HRESULT setNodeStringValue(_In_ std::wstring const& string, _Out_opt_ IXmlNode* node, _Out_ IXmlDocument* xml) { + ComPtr textNode; + HRESULT hr = xml->CreateTextNode(WinToastStringWrapper(string).Get(), &textNode); + if (SUCCEEDED(hr)) { + ComPtr stringNode; + hr = textNode.As(&stringNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = node->AppendChild(stringNode.Get(), &appendedChild); + } + } + return hr; + } + + template + inline HRESULT setEventHandlers(_In_ IToastNotification* notification, _In_ std::shared_ptr eventHandler, + _In_ INT64 expirationTime, _Out_ EventRegistrationToken& activatedToken, + _Out_ EventRegistrationToken& dismissedToken, _Out_ EventRegistrationToken& failedToken, + _In_ FunctorT&& markAsReadyForDeletionFunc) { + HRESULT hr = notification->add_Activated( + Callback, ITypedEventHandler>>( + [eventHandler, markAsReadyForDeletionFunc](IToastNotification* notify, IInspectable* inspectable) + { + ComPtr activatedEventArgs; + HRESULT hr = inspectable->QueryInterface(activatedEventArgs.GetAddressOf()); + if (SUCCEEDED(hr)) { + HSTRING argumentsHandle; + hr = activatedEventArgs->get_Arguments(&argumentsHandle); + if (SUCCEEDED(hr)) { + PCWSTR arguments = Util::AsString(argumentsHandle); + + if(wcscmp(arguments, L"action=reply") == 0) + { + ComPtr inputBoxActivatedEventArgs; + HRESULT hr2 = inspectable->QueryInterface(inputBoxActivatedEventArgs.GetAddressOf()); + + if(SUCCEEDED(hr2)) + { + ComPtr replyHandle; + inputBoxActivatedEventArgs->get_UserInput(&replyHandle); + + ComPtr<__FIMap_2_HSTRING_IInspectable> replyMap; + hr = replyHandle.As(&replyMap); + + if(SUCCEEDED(hr)) + { + IInspectable* propertySet; + hr = replyMap.Get()->Lookup(WinToastStringWrapper(L"textBox").Get(), &propertySet); + if (SUCCEEDED(hr)) + { + ComPtr propertyValue; + hr = propertySet->QueryInterface(IID_PPV_ARGS(&propertyValue)); + + if (SUCCEEDED(hr)) + { + // Successfully queried IPropertyValue, now extract the value + HSTRING userInput; + hr = propertyValue->GetString(&userInput); + + // Convert the HSTRING to a wide string + PCWSTR strValue = AsString(userInput); + + if (SUCCEEDED(hr)) + { + eventHandler->toastActivated(std::wstring(strValue)); + return S_OK; + } + } + } + } + } + } + + if (arguments && *arguments) { + eventHandler->toastActivated(static_cast(wcstol(arguments, nullptr, 10))); + DllImporter::WindowsDeleteString(argumentsHandle); + markAsReadyForDeletionFunc(); + return S_OK; + } + DllImporter::WindowsDeleteString(argumentsHandle); + } + } + eventHandler->toastActivated(); + markAsReadyForDeletionFunc(); + return S_OK; + }) + .Get(), + &activatedToken); + + if (SUCCEEDED(hr)) { + hr = notification->add_Dismissed( + Callback, ITypedEventHandler>>( + [eventHandler, expirationTime, markAsReadyForDeletionFunc](IToastNotification* notify, IToastDismissedEventArgs* e) { + ToastDismissalReason reason; + if (SUCCEEDED(e->get_Reason(&reason))) { + if (reason == ToastDismissalReason_UserCanceled && expirationTime && + InternalDateTime::Now() >= expirationTime) { + reason = ToastDismissalReason_TimedOut; + } + eventHandler->toastDismissed(static_cast(reason)); + } + markAsReadyForDeletionFunc(); + return S_OK; + }) + .Get(), + &dismissedToken); + if (SUCCEEDED(hr)) { + hr = notification->add_Failed( + Callback, ITypedEventHandler>>( + [eventHandler, markAsReadyForDeletionFunc](IToastNotification* notify, IToastFailedEventArgs* e) { + eventHandler->toastFailed(); + markAsReadyForDeletionFunc(); + return S_OK; + }) + .Get(), + &failedToken); + } + } + return hr; + } + + inline HRESULT addAttribute(_In_ IXmlDocument* xml, std::wstring const& name, IXmlNamedNodeMap* attributeMap) { + ComPtr srcAttribute; + HRESULT hr = xml->CreateAttribute(WinToastStringWrapper(name).Get(), &srcAttribute); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = srcAttribute.As(&node); + if (SUCCEEDED(hr)) { + ComPtr pNode; + hr = attributeMap->SetNamedItem(node.Get(), &pNode); + } + } + return hr; + } + + inline HRESULT createElement(_In_ IXmlDocument* xml, _In_ std::wstring const& root_node, _In_ std::wstring const& element_name, + _In_ std::vector const& attribute_names) { + ComPtr rootList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(root_node).Get(), &rootList); + if (SUCCEEDED(hr)) { + ComPtr root; + hr = rootList->Item(0, &root); + if (SUCCEEDED(hr)) { + ComPtr audioElement; + hr = xml->CreateElement(WinToastStringWrapper(element_name).Get(), &audioElement); + if (SUCCEEDED(hr)) { + ComPtr audioNodeTmp; + hr = audioElement.As(&audioNodeTmp); + if (SUCCEEDED(hr)) { + ComPtr audioNode; + hr = root->AppendChild(audioNodeTmp.Get(), &audioNode); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = audioNode->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + for (auto const& it : attribute_names) { + hr = addAttribute(xml, it, attributes.Get()); + } + } + } + } + } + } + } + return hr; + } +} // namespace Util + +WinToast* WinToast::instance() { + thread_local static WinToast instance; + return &instance; +} + +WinToast::WinToast() : _isInitialized(false), _hasCoInitialized(false) { + if (!isCompatible()) { + DEBUG_MSG(L"Warning: Your system is not compatible with this library "); + } +} + +WinToast::~WinToast() { + clear(); + + if (_hasCoInitialized) { + CoUninitialize(); + } +} + +void WinToast::setAppName(_In_ std::wstring const& appName) { + _appName = appName; +} + +void WinToast::setAppUserModelId(_In_ std::wstring const& aumi) { + _aumi = aumi; + DEBUG_MSG(L"Default App User Model Id: " << _aumi.c_str()); +} + +void WinToast::setShortcutPolicy(_In_ ShortcutPolicy shortcutPolicy) { + _shortcutPolicy = shortcutPolicy; +} + +bool WinToast::isCompatible() { + DllImporter::initialize(); + return !((DllImporter::SetCurrentProcessExplicitAppUserModelID == nullptr) || (DllImporter::PropVariantToString == nullptr) || + (DllImporter::RoGetActivationFactory == nullptr) || (DllImporter::WindowsCreateStringReference == nullptr) || + (DllImporter::WindowsDeleteString == nullptr)); +} + +bool WinToastLib::WinToast::isSupportingModernFeatures() { + constexpr auto MinimumSupportedVersion = 6; + return Util::getRealOSVersion().dwMajorVersion > MinimumSupportedVersion; +} + +bool WinToastLib::WinToast::isWin10AnniversaryOrHigher() { + return Util::getRealOSVersion().dwBuildNumber >= 14393; +} + +std::wstring WinToast::configureAUMI(_In_ std::wstring const& companyName, _In_ std::wstring const& productName, + _In_ std::wstring const& subProduct, _In_ std::wstring const& versionInformation) { + std::wstring aumi = companyName; + aumi += L"." + productName; + if (subProduct.length() > 0) { + aumi += L"." + subProduct; + if (versionInformation.length() > 0) { + aumi += L"." + versionInformation; + } + } + + if (aumi.length() > SCHAR_MAX) { + DEBUG_MSG("Error: max size allowed for AUMI: 128 characters."); + } + return aumi; +} + +std::wstring const& WinToast::strerror(WinToastError error) { + static const std::unordered_map Labels = { + {WinToastError::NoError, L"No error. The process was executed correctly" }, + {WinToastError::NotInitialized, L"The library has not been initialized" }, + {WinToastError::SystemNotSupported, L"The OS does not support WinToast" }, + {WinToastError::ShellLinkNotCreated, L"The library was not able to create a Shell Link for the app" }, + {WinToastError::InvalidAppUserModelID, L"The AUMI is not a valid one" }, + {WinToastError::InvalidParameters, L"Invalid parameters, please double-check the AUMI or App Name" }, + {WinToastError::NotDisplayed, L"The toast was created correctly but WinToast was not able to display the toast"}, + {WinToastError::UnknownError, L"Unknown error" } + }; + + auto const iter = Labels.find(error); + assert(iter != Labels.end()); + return iter->second; +} + +enum WinToast::ShortcutResult WinToast::createShortcut() { + if (_aumi.empty() || _appName.empty()) { + DEBUG_MSG(L"Error: App User Model Id or Appname is empty!"); + return SHORTCUT_MISSING_PARAMETERS; + } + + if (!isCompatible()) { + DEBUG_MSG(L"Your OS is not compatible with this library! =("); + return SHORTCUT_INCOMPATIBLE_OS; + } + + if (!_hasCoInitialized) { + HRESULT initHr = CoInitializeEx(nullptr, COINIT::COINIT_MULTITHREADED); + if (initHr != RPC_E_CHANGED_MODE) { + if (FAILED(initHr) && initHr != S_FALSE) { + DEBUG_MSG(L"Error on COM library initialization!"); + return SHORTCUT_COM_INIT_FAILURE; + } else { + _hasCoInitialized = true; + } + } + } + + bool wasChanged; + HRESULT hr = validateShellLinkHelper(wasChanged); + if (SUCCEEDED(hr)) { + return wasChanged ? SHORTCUT_WAS_CHANGED : SHORTCUT_UNCHANGED; + } + + hr = createShellLinkHelper(); + return SUCCEEDED(hr) ? SHORTCUT_WAS_CREATED : SHORTCUT_CREATE_FAILED; +} + +bool WinToast::initialize(_Out_opt_ WinToastError* error) { + _isInitialized = false; + setError(error, WinToastError::NoError); + + if (!isCompatible()) { + setError(error, WinToastError::SystemNotSupported); + DEBUG_MSG(L"Error: system not supported."); + return false; + } + + if (_aumi.empty() || _appName.empty()) { + setError(error, WinToastError::InvalidParameters); + DEBUG_MSG(L"Error while initializing, did you set up a valid AUMI and App name?"); + return false; + } + + if (_shortcutPolicy != SHORTCUT_POLICY_IGNORE) { + if (createShortcut() < 0) { + setError(error, WinToastError::ShellLinkNotCreated); + DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =("); + return false; + } + } + + if (FAILED(DllImporter::SetCurrentProcessExplicitAppUserModelID(_aumi.c_str()))) { + setError(error, WinToastError::InvalidAppUserModelID); + DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =("); + return false; + } + + _isInitialized = true; + return _isInitialized; +} + +bool WinToast::isInitialized() const { + return _isInitialized; +} + +std::wstring const& WinToast::appName() const { + return _appName; +} + +std::wstring const& WinToast::appUserModelId() const { + return _aumi; +} + +HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) { + WCHAR path[MAX_PATH] = {L'\0'}; + Util::defaultShellLinkPath(_appName, path); + // Check if the file exist + DWORD attr = GetFileAttributesW(path); + if (attr >= 0xFFFFFFF) { + DEBUG_MSG("Error, shell link not found. Try to create a new one in: " << path); + return E_FAIL; + } + + // Let's load the file as shell link to validate. + // - Create a shell link + // - Create a persistant file + // - Load the path as data for the persistant file + // - Read the property AUMI and validate with the current + // - Review if AUMI is equal. + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (SUCCEEDED(hr)) { + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (SUCCEEDED(hr)) { + hr = persistFile->Load(path, STGM_READWRITE); + if (SUCCEEDED(hr)) { + ComPtr propertyStore; + hr = shellLink.As(&propertyStore); + if (SUCCEEDED(hr)) { + PROPVARIANT appIdPropVar; + hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &appIdPropVar); + if (SUCCEEDED(hr)) { + WCHAR AUMI[MAX_PATH]; + hr = DllImporter::PropVariantToString(appIdPropVar, AUMI, MAX_PATH); + wasChanged = false; + if (FAILED(hr) || _aumi != AUMI) { + if (_shortcutPolicy == SHORTCUT_POLICY_REQUIRE_CREATE) { + // AUMI Changed for the same app, let's update the current value! =) + wasChanged = true; + PropVariantClear(&appIdPropVar); + hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->Commit(); + if (SUCCEEDED(hr) && SUCCEEDED(persistFile->IsDirty())) { + hr = persistFile->Save(path, TRUE); + } + } + } + } else { + // Not allowed to touch the shortcut to fix the AUMI + hr = E_FAIL; + } + } + PropVariantClear(&appIdPropVar); + } + } + } + } + } + return hr; +} + +HRESULT WinToast::createShellLinkHelper() { + if (_shortcutPolicy != SHORTCUT_POLICY_REQUIRE_CREATE) { + return E_FAIL; + } + + WCHAR exePath[MAX_PATH]{L'\0'}; + WCHAR slPath[MAX_PATH]{L'\0'}; + Util::defaultShellLinkPath(_appName, slPath); + Util::defaultExecutablePath(exePath); + std::wstring exeDir = Util::parentDirectory(exePath, sizeof(exePath) / sizeof(exePath[0])); + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (SUCCEEDED(hr)) { + hr = shellLink->SetPath(exePath); + if (SUCCEEDED(hr)) { + hr = shellLink->SetArguments(L""); + if (SUCCEEDED(hr)) { + hr = shellLink->SetWorkingDirectory(exeDir.c_str()); + if (SUCCEEDED(hr)) { + ComPtr propertyStore; + hr = shellLink.As(&propertyStore); + if (SUCCEEDED(hr)) { + PROPVARIANT appIdPropVar; + hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->Commit(); + if (SUCCEEDED(hr)) { + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (SUCCEEDED(hr)) { + hr = persistFile->Save(slPath, TRUE); + } + } + } + PropVariantClear(&appIdPropVar); + } + } + } + } + } + } + return hr; +} + +INT64 WinToast::showToast(_In_ WinToastTemplate const& toast, _In_ IWinToastHandler* eventHandler, _Out_ WinToastError* error) { + std::shared_ptr handler(eventHandler); + setError(error, WinToastError::NoError); + INT64 id = -1; + if (!isInitialized()) { + setError(error, WinToastError::NotInitialized); + DEBUG_MSG("Error when launching the toast. WinToast is not initialized."); + return id; + } + if (!handler) { + setError(error, WinToastError::InvalidHandler); + DEBUG_MSG("Error when launching the toast. Handler cannot be nullptr."); + return id; + } + + ComPtr notificationManager; + HRESULT hr = DllImporter::Wrap_GetActivationFactory( + WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); + if (SUCCEEDED(hr)) { + ComPtr notifier; + hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); + if (SUCCEEDED(hr)) { + ComPtr notificationFactory; + hr = DllImporter::Wrap_GetActivationFactory( + WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), ¬ificationFactory); + if (SUCCEEDED(hr)) { + ComPtr xmlDocument; + hr = notificationManager->GetTemplateContent(ToastTemplateType(toast.type()), &xmlDocument); + if (SUCCEEDED(hr) && toast.isToastGeneric()) { + hr = setBindToastGenericHelper(xmlDocument.Get()); + } + if (SUCCEEDED(hr)) { + for (UINT32 i = 0, fieldsCount = static_cast(toast.textFieldsCount()); i < fieldsCount && SUCCEEDED(hr); i++) { + hr = setTextFieldHelper(xmlDocument.Get(), toast.textField(WinToastTemplate::TextField(i)), i); + } + + // Modern feature are supported Windows > Windows 10 + if (SUCCEEDED(hr) && isSupportingModernFeatures()) { + // Note that we do this *after* using toast.textFieldsCount() to + // iterate/fill the template's text fields, since we're adding yet another text field. + if (SUCCEEDED(hr) && !toast.attributionText().empty()) { + hr = setAttributionTextFieldHelper(xmlDocument.Get(), toast.attributionText()); + } + + std::array buf; + for (std::size_t i = 0, actionsCount = toast.actionsCount(); i < actionsCount && SUCCEEDED(hr); i++) { + _snwprintf_s(buf.data(), buf.size(), _TRUNCATE, L"%zd", i); + hr = addActionHelper(xmlDocument.Get(), toast.actionLabel(i), buf.data()); + } + + if (SUCCEEDED(hr)) { + hr = (toast.audioPath().empty() && toast.audioOption() == WinToastTemplate::AudioOption::Default) + ? hr + : setAudioFieldHelper(xmlDocument.Get(), toast.audioPath(), toast.audioOption()); + } + + if (SUCCEEDED(hr) && toast.duration() != WinToastTemplate::Duration::System) { + hr = addDurationHelper(xmlDocument.Get(), + (toast.duration() == WinToastTemplate::Duration::Short) ? L"short" : L"long"); + } + + if(SUCCEEDED(hr) && toast.isInput()) { + hr = addInputHelper(xmlDocument.Get()); + } + + if (SUCCEEDED(hr)) { + hr = addScenarioHelper(xmlDocument.Get(), toast.scenario()); + } + + } else { + DEBUG_MSG("Modern features (Actions/Sounds/Attributes) not supported in this os version"); + } + + if (SUCCEEDED(hr)) { + bool isWin10AnniversaryOrAbove = WinToast::isWin10AnniversaryOrHigher(); + bool isCircleCropHint = isWin10AnniversaryOrAbove ? toast.isCropHintCircle() : false; + hr = toast.hasImage() + ? setImageFieldHelper(xmlDocument.Get(), toast.imagePath(), toast.isToastGeneric(), isCircleCropHint) + : hr; + if (SUCCEEDED(hr) && isWin10AnniversaryOrAbove && toast.hasHeroImage()) { + hr = setHeroImageHelper(xmlDocument.Get(), toast.heroImagePath(), toast.isInlineHeroImage()); + } + if (SUCCEEDED(hr)) { + ComPtr notification; + hr = notificationFactory->CreateToastNotification(xmlDocument.Get(), ¬ification); + if (SUCCEEDED(hr)) { + INT64 expiration = 0, relativeExpiration = toast.expiration(); + if (relativeExpiration > 0) { + InternalDateTime expirationDateTime(relativeExpiration); + expiration = expirationDateTime; + hr = notification->put_ExpirationTime(&expirationDateTime); + } + + EventRegistrationToken activatedToken, dismissedToken, failedToken; + + GUID guid; + HRESULT hrGuid = CoCreateGuid(&guid); + id = guid.Data1; + if (SUCCEEDED(hr) && SUCCEEDED(hrGuid)) { + hr = Util::setEventHandlers(notification.Get(), handler, expiration, activatedToken, dismissedToken, + failedToken, [this, id]() { markAsReadyForDeletion(id); }); + if (FAILED(hr)) { + setError(error, WinToastError::InvalidHandler); + } + } + + if (SUCCEEDED(hr)) { + if (SUCCEEDED(hr)) { + _buffer.emplace(id, NotifyData(notification, activatedToken, dismissedToken, failedToken)); + DEBUG_MSG("xml: " << Util::AsString(xmlDocument)); + hr = notifier->Show(notification.Get()); + if (FAILED(hr)) { + setError(error, WinToastError::NotDisplayed); + } + } + } + } + } + } + } + } + } + } + return FAILED(hr) ? -1 : id; +} + +ComPtr WinToast::notifier(_In_ bool* succeded) const { + ComPtr notificationManager; + ComPtr notifier; + HRESULT hr = DllImporter::Wrap_GetActivationFactory( + WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); + if (SUCCEEDED(hr)) { + hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); + } + *succeded = SUCCEEDED(hr); + return notifier; +} + +void WinToast::markAsReadyForDeletion(_In_ INT64 id) { + // Flush the buffer by removing all the toasts that are ready for deletion + for (auto it = _buffer.begin(); it != _buffer.end();) { + if (it->second.isReadyForDeletion()) { + it->second.RemoveTokens(); + it = _buffer.erase(it); + } else { + ++it; + } + } + + // Mark the toast as ready for deletion (if it exists) so that it will be removed from the buffer in the next iteration + auto const iter = _buffer.find(id); + if (iter != _buffer.end()) { + _buffer[id].markAsReadyForDeletion(); + } +} + +bool WinToast::hideToast(_In_ INT64 id) { + if (!isInitialized()) { + DEBUG_MSG("Error when hiding the toast. WinToast is not initialized."); + return false; + } + + auto iter = _buffer.find(id); + if (iter == _buffer.end()) { + return false; + } + + auto succeded = false; + auto notify = notifier(&succeded); + if (!succeded) { + return false; + } + + auto& notifyData = iter->second; + auto result = notify->Hide(notifyData.notification()); + if (FAILED(result)) { + DEBUG_MSG("Error when hiding the toast. Error code: " << result); + return false; + } + + notifyData.RemoveTokens(); + _buffer.erase(iter); + return SUCCEEDED(result); +} + +void WinToast::clear() { + auto succeded = false; + auto notify = notifier(&succeded); + if (!succeded) { + return; + } + + auto safeCopy = _buffer; + for (auto& data : safeCopy) { + auto& notifyData = data.second; + notify->Hide(notifyData.notification()); + notifyData.RemoveTokens(); + } + _buffer.clear(); +} + +// +// Available as of Windows 10 Anniversary Update +// Ref: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts +// +// NOTE: This will add a new text field, so be aware when iterating over +// the toast's text fields or getting a count of them. +// +HRESULT WinToast::setAttributionTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text) { + Util::createElement(xml, L"binding", L"text", {L"placement"}); + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 nodeListLength; + hr = nodeList->get_Length(&nodeListLength); + if (SUCCEEDED(hr)) { + for (UINT32 i = 0; i < nodeListLength; i++) { + ComPtr textNode; + hr = nodeList->Item(i, &textNode); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = textNode->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + if (SUCCEEDED(hr)) { + hr = attributes->GetNamedItem(WinToastStringWrapper(L"placement").Get(), &editedNode); + if (FAILED(hr) || !editedNode) { + continue; + } + hr = Util::setNodeStringValue(L"attribution", editedNode.Get(), xml); + if (SUCCEEDED(hr)) { + return setTextFieldHelper(xml, text, i); + } + } + } + } + } + } + } + return hr; +} + +HRESULT WinToast::addDurationHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& duration) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), WinToastStringWrapper(duration).Get()); + } + } + } + } + return hr; +} + +HRESULT WinToast::addScenarioHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& scenario) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"scenario").Get(), WinToastStringWrapper(scenario).Get()); + } + } + } + } + return hr; +} + +HRESULT WinToast::addInputHelper(_In_ IXmlDocument* xml) +{ + std::vector attrbs; + attrbs.push_back(L"id"); + attrbs.push_back(L"type"); + attrbs.push_back(L"placeHolderContent"); + + std::vector attrbs2; + attrbs2.push_back(L"content"); + attrbs2.push_back(L"arguments"); + + Util::createElement(xml, L"toast", L"actions", {}); + + Util::createElement(xml, L"actions", L"input",attrbs); + Util::createElement(xml, L"actions", L"action",attrbs2); + + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"input").Get(), &nodeList); + if (SUCCEEDED(hr)) + { + ComPtr inputNode; + hr = nodeList->Item(0, &inputNode); + if (SUCCEEDED(hr)) + { + ComPtr toastElement; + hr = inputNode.As(&toastElement); + if(SUCCEEDED(hr)){ + toastElement->SetAttribute(WinToastStringWrapper(L"id").Get(), WinToastStringWrapper(L"textBox").Get()); + toastElement->SetAttribute(WinToastStringWrapper(L"type").Get(), WinToastStringWrapper(L"text").Get()); + hr = toastElement->SetAttribute(WinToastStringWrapper(L"placeHolderContent").Get(), WinToastStringWrapper(L"...").Get()); + } + } + } + + ComPtr nodeList2; + hr = xml->GetElementsByTagName(WinToastStringWrapper(L"action").Get(), &nodeList2); + if (SUCCEEDED(hr)) + { + ComPtr actionNode; + hr = nodeList2->Item(0, &actionNode); + if (SUCCEEDED(hr)) + { + ComPtr actionElement; + hr = actionNode.As(&actionElement); + if(SUCCEEDED(hr)){ + actionElement->SetAttribute(WinToastStringWrapper(L"content").Get(), WinToastStringWrapper(L"Reply").Get()); + actionElement->SetAttribute(WinToastStringWrapper(L"arguments").Get(), WinToastStringWrapper(L"action=reply").Get()); + actionElement->SetAttribute(WinToastStringWrapper(L"hint-inputId").Get(), WinToastStringWrapper(L"textBox").Get()); + } + } + } + return hr; +} + +HRESULT WinToast::setTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text, _In_ UINT32 pos) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(pos, &node); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(text, node.Get(), xml); + } + } + return hr; +} + +HRESULT WinToast::setBindToastGenericHelper(_In_ IXmlDocument* xml) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"binding").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"template").Get(), WinToastStringWrapper(L"ToastGeneric").Get()); + } + } + } + } + return hr; +} + +HRESULT WinToast::setImageFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isToastGeneric, + _In_ bool isCropHintCircle) { + assert(path.size() < MAX_PATH); + + wchar_t imagePath[MAX_PATH] = L"file:///"; + HRESULT hr = StringCchCatW(imagePath, MAX_PATH, path.c_str()); + if (SUCCEEDED(hr)) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"image").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(0, &node); + + ComPtr imageElement; + HRESULT hrImage = node.As(&imageElement); + if (SUCCEEDED(hr) && SUCCEEDED(hrImage) && isToastGeneric) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"placement").Get(), WinToastStringWrapper(L"appLogoOverride").Get()); + if (SUCCEEDED(hr) && isCropHintCircle) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"hint-crop").Get(), WinToastStringWrapper(L"circle").Get()); + } + } + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = node->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode); + if (SUCCEEDED(hr)) { + Util::setNodeStringValue(imagePath, editedNode.Get(), xml); + } + } + } + } + } + return hr; +} + +HRESULT WinToast::setAudioFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, + _In_opt_ WinToastTemplate::AudioOption option) { + std::vector attrs; + if (!path.empty()) { + attrs.push_back(L"src"); + } + if (option == WinToastTemplate::AudioOption::Loop) { + attrs.push_back(L"loop"); + } + if (option == WinToastTemplate::AudioOption::Silent) { + attrs.push_back(L"silent"); + } + Util::createElement(xml, L"toast", L"audio", attrs); + + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"audio").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(0, &node); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = node->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + if (!path.empty()) { + if (SUCCEEDED(hr)) { + hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(path, editedNode.Get(), xml); + } + } + } + + if (SUCCEEDED(hr)) { + switch (option) { + case WinToastTemplate::AudioOption::Loop: + hr = attributes->GetNamedItem(WinToastStringWrapper(L"loop").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); + } + break; + case WinToastTemplate::AudioOption::Silent: + hr = attributes->GetNamedItem(WinToastStringWrapper(L"silent").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); + } + default: + break; + } + } + } + } + } + return hr; +} + +HRESULT WinToast::addActionHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& content, _In_ std::wstring const& arguments) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"actions").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr actionsNode; + if (length > 0) { + hr = nodeList->Item(0, &actionsNode); + } else { + hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); + if (SUCCEEDED(hr)) { + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"template").Get(), + WinToastStringWrapper(L"ToastGeneric").Get()); + } + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), + WinToastStringWrapper(L"long").Get()); + } + if (SUCCEEDED(hr)) { + ComPtr actionsElement; + hr = xml->CreateElement(WinToastStringWrapper(L"actions").Get(), &actionsElement); + if (SUCCEEDED(hr)) { + hr = actionsElement.As(&actionsNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = toastNode->AppendChild(actionsNode.Get(), &appendedChild); + } + } + } + } + } + } + } + if (SUCCEEDED(hr)) { + ComPtr actionElement; + hr = xml->CreateElement(WinToastStringWrapper(L"action").Get(), &actionElement); + if (SUCCEEDED(hr)) { + hr = actionElement->SetAttribute(WinToastStringWrapper(L"content").Get(), WinToastStringWrapper(content).Get()); + } + if (SUCCEEDED(hr)) { + hr = actionElement->SetAttribute(WinToastStringWrapper(L"arguments").Get(), WinToastStringWrapper(arguments).Get()); + } + if (SUCCEEDED(hr)) { + ComPtr actionNode; + hr = actionElement.As(&actionNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild); + } + } + } + } + } + return hr; +} + +HRESULT WinToast::setHeroImageHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isInlineImage) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"binding").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr bindingNode; + if (length > 0) { + hr = nodeList->Item(0, &bindingNode); + } + if (SUCCEEDED(hr)) { + ComPtr imageElement; + hr = xml->CreateElement(WinToastStringWrapper(L"image").Get(), &imageElement); + if (SUCCEEDED(hr) && isInlineImage == false) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"placement").Get(), WinToastStringWrapper(L"hero").Get()); + } + if (SUCCEEDED(hr)) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"src").Get(), WinToastStringWrapper(path).Get()); + } + if (SUCCEEDED(hr)) { + ComPtr actionNode; + hr = imageElement.As(&actionNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = bindingNode->AppendChild(actionNode.Get(), &appendedChild); + } + } + } + } + } + return hr; +} + +void WinToast::setError(_Out_opt_ WinToastError* error, _In_ WinToastError value) { + if (error) { + *error = value; + } +} + +WinToastTemplate::WinToastTemplate(_In_ WinToastTemplateType type) : _type(type) { + constexpr static std::size_t TextFieldsCount[] = {1, 2, 2, 3, 1, 2, 2, 3}; + _textFields = std::vector(TextFieldsCount[type], L""); +} + +WinToastTemplate::~WinToastTemplate() { + _textFields.clear(); +} + +void WinToastTemplate::setTextField(_In_ std::wstring const& txt, _In_ WinToastTemplate::TextField pos) { + auto const position = static_cast(pos); + if (position >= _textFields.size()) { + DEBUG_MSG("The selected template type supports only " << _textFields.size() << " text lines"); + return; + } + _textFields[position] = txt; +} + +void WinToastTemplate::setImagePath(_In_ std::wstring const& imgPath, _In_ CropHint cropHint) { + _imagePath = imgPath; + _cropHint = cropHint; +} + +void WinToastTemplate::setHeroImagePath(_In_ std::wstring const& imgPath, _In_ bool inlineImage) { + _heroImagePath = imgPath; + _inlineHeroImage = inlineImage; +} + +void WinToastTemplate::setAudioPath(_In_ std::wstring const& audioPath) { + _audioPath = audioPath; +} + +void WinToastTemplate::setAudioPath(_In_ AudioSystemFile file) { + static const std::unordered_map Files = { + {AudioSystemFile::DefaultSound, L"ms-winsoundevent:Notification.Default" }, + {AudioSystemFile::IM, L"ms-winsoundevent:Notification.IM" }, + {AudioSystemFile::Mail, L"ms-winsoundevent:Notification.Mail" }, + {AudioSystemFile::Reminder, L"ms-winsoundevent:Notification.Reminder" }, + {AudioSystemFile::SMS, L"ms-winsoundevent:Notification.SMS" }, + {AudioSystemFile::Alarm, L"ms-winsoundevent:Notification.Looping.Alarm" }, + {AudioSystemFile::Alarm2, L"ms-winsoundevent:Notification.Looping.Alarm2" }, + {AudioSystemFile::Alarm3, L"ms-winsoundevent:Notification.Looping.Alarm3" }, + {AudioSystemFile::Alarm4, L"ms-winsoundevent:Notification.Looping.Alarm4" }, + {AudioSystemFile::Alarm5, L"ms-winsoundevent:Notification.Looping.Alarm5" }, + {AudioSystemFile::Alarm6, L"ms-winsoundevent:Notification.Looping.Alarm6" }, + {AudioSystemFile::Alarm7, L"ms-winsoundevent:Notification.Looping.Alarm7" }, + {AudioSystemFile::Alarm8, L"ms-winsoundevent:Notification.Looping.Alarm8" }, + {AudioSystemFile::Alarm9, L"ms-winsoundevent:Notification.Looping.Alarm9" }, + {AudioSystemFile::Alarm10, L"ms-winsoundevent:Notification.Looping.Alarm10"}, + {AudioSystemFile::Call, L"ms-winsoundevent:Notification.Looping.Call" }, + {AudioSystemFile::Call1, L"ms-winsoundevent:Notification.Looping.Call1" }, + {AudioSystemFile::Call2, L"ms-winsoundevent:Notification.Looping.Call2" }, + {AudioSystemFile::Call3, L"ms-winsoundevent:Notification.Looping.Call3" }, + {AudioSystemFile::Call4, L"ms-winsoundevent:Notification.Looping.Call4" }, + {AudioSystemFile::Call5, L"ms-winsoundevent:Notification.Looping.Call5" }, + {AudioSystemFile::Call6, L"ms-winsoundevent:Notification.Looping.Call6" }, + {AudioSystemFile::Call7, L"ms-winsoundevent:Notification.Looping.Call7" }, + {AudioSystemFile::Call8, L"ms-winsoundevent:Notification.Looping.Call8" }, + {AudioSystemFile::Call9, L"ms-winsoundevent:Notification.Looping.Call9" }, + {AudioSystemFile::Call10, L"ms-winsoundevent:Notification.Looping.Call10" }, + }; + auto const iter = Files.find(file); + assert(iter != Files.end()); + _audioPath = iter->second; +} + +void WinToastTemplate::setAudioOption(_In_ WinToastTemplate::AudioOption audioOption) { + _audioOption = audioOption; +} + +void WinToastTemplate::setFirstLine(_In_ std::wstring const& text) { + setTextField(text, WinToastTemplate::FirstLine); +} + +void WinToastTemplate::setSecondLine(_In_ std::wstring const& text) { + setTextField(text, WinToastTemplate::SecondLine); +} + +void WinToastTemplate::setThirdLine(_In_ std::wstring const& text) { + setTextField(text, WinToastTemplate::ThirdLine); +} + +void WinToastTemplate::setDuration(_In_ Duration duration) { + _duration = duration; +} + +void WinToastTemplate::setExpiration(_In_ INT64 millisecondsFromNow) { + _expiration = millisecondsFromNow; +} + +void WinToastLib::WinToastTemplate::setScenario(_In_ Scenario scenario) { + switch (scenario) { + case Scenario::Default: + _scenario = L"Default"; + break; + case Scenario::Alarm: + _scenario = L"Alarm"; + break; + case Scenario::IncomingCall: + _scenario = L"IncomingCall"; + break; + case Scenario::Reminder: + _scenario = L"Reminder"; + break; + } +} + +void WinToastTemplate::setAttributionText(_In_ std::wstring const& attributionText) { + _attributionText = attributionText; +} + +void WinToastTemplate::addAction(_In_ std::wstring const& label) { + _actions.push_back(label); +} + +void WinToastTemplate::addInput() +{ + _hasInput = true; +} + +std::size_t WinToastTemplate::textFieldsCount() const { + return _textFields.size(); +} + +std::size_t WinToastTemplate::actionsCount() const { + return _actions.size(); +} + +bool WinToastTemplate::hasImage() const { + return _type < WinToastTemplateType::Text01; +} + +bool WinToastTemplate::hasHeroImage() const { + return hasImage() && !_heroImagePath.empty(); +} + +std::vector const& WinToastTemplate::textFields() const { + return _textFields; +} + +std::wstring const& WinToastTemplate::textField(_In_ TextField pos) const { + auto const position = static_cast(pos); + assert(position < _textFields.size()); + return _textFields[position]; +} + +std::wstring const& WinToastTemplate::actionLabel(_In_ std::size_t position) const { + assert(position < _actions.size()); + return _actions[position]; +} + +std::wstring const& WinToastTemplate::imagePath() const { + return _imagePath; +} + +std::wstring const& WinToastTemplate::heroImagePath() const { + return _heroImagePath; +} + +std::wstring const& WinToastTemplate::audioPath() const { + return _audioPath; +} + +std::wstring const& WinToastTemplate::attributionText() const { + return _attributionText; +} + +std::wstring const& WinToastLib::WinToastTemplate::scenario() const { + return _scenario; +} + +INT64 WinToastTemplate::expiration() const { + return _expiration; +} + +WinToastTemplate::WinToastTemplateType WinToastTemplate::type() const { + return _type; +} + +WinToastTemplate::AudioOption WinToastTemplate::audioOption() const { + return _audioOption; +} + +WinToastTemplate::Duration WinToastTemplate::duration() const { + return _duration; +} + +bool WinToastTemplate::isToastGeneric() const { + return hasHeroImage() || _cropHint == WinToastTemplate::Circle; +} + +bool WinToastTemplate::isInlineHeroImage() const { + return _inlineHeroImage; +} + +bool WinToastTemplate::isCropHintCircle() const { + return _cropHint == CropHint::Circle; +} + +bool WinToastTemplate::isInput() const{ + return _hasInput; +} diff --git a/native/WinToastWrapper/vendor/wintoastlib.h b/native/WinToastWrapper/vendor/wintoastlib.h new file mode 100644 index 0000000..475c5b3 --- /dev/null +++ b/native/WinToastWrapper/vendor/wintoastlib.h @@ -0,0 +1,318 @@ +/** + * MIT License + * + * Copyright (C) 2016-2023 WinToast v1.3.0 - Mohammed Boujemaoui + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef WINTOASTLIB_H +#define WINTOASTLIB_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Microsoft::WRL; +using namespace ABI::Windows::Data::Xml::Dom; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::UI::Notifications; +using namespace Windows::Foundation; + +namespace WinToastLib { + + void setDebugOutputEnabled(bool enabled); + + class IWinToastHandler { + public: + enum WinToastDismissalReason { + UserCanceled = ToastDismissalReason::ToastDismissalReason_UserCanceled, + ApplicationHidden = ToastDismissalReason::ToastDismissalReason_ApplicationHidden, + TimedOut = ToastDismissalReason::ToastDismissalReason_TimedOut + }; + + virtual ~IWinToastHandler() = default; + virtual void toastActivated() const = 0; + virtual void toastActivated(int actionIndex) const = 0; + virtual void toastActivated(std::wstring response) const = 0; + virtual void toastDismissed(WinToastDismissalReason state) const = 0; + virtual void toastFailed() const = 0; + }; + + class WinToastTemplate { + public: + enum class Scenario { Default, Alarm, IncomingCall, Reminder }; + enum Duration { System, Short, Long }; + enum AudioOption { Default = 0, Silent, Loop }; + enum TextField { FirstLine = 0, SecondLine, ThirdLine }; + + enum WinToastTemplateType { + ImageAndText01 = ToastTemplateType::ToastTemplateType_ToastImageAndText01, + ImageAndText02 = ToastTemplateType::ToastTemplateType_ToastImageAndText02, + ImageAndText03 = ToastTemplateType::ToastTemplateType_ToastImageAndText03, + ImageAndText04 = ToastTemplateType::ToastTemplateType_ToastImageAndText04, + Text01 = ToastTemplateType::ToastTemplateType_ToastText01, + Text02 = ToastTemplateType::ToastTemplateType_ToastText02, + Text03 = ToastTemplateType::ToastTemplateType_ToastText03, + Text04 = ToastTemplateType::ToastTemplateType_ToastText04 + }; + + enum AudioSystemFile { + DefaultSound, + IM, + Mail, + Reminder, + SMS, + Alarm, + Alarm2, + Alarm3, + Alarm4, + Alarm5, + Alarm6, + Alarm7, + Alarm8, + Alarm9, + Alarm10, + Call, + Call1, + Call2, + Call3, + Call4, + Call5, + Call6, + Call7, + Call8, + Call9, + Call10, + }; + + enum CropHint { + Square, + Circle, + }; + + WinToastTemplate(_In_ WinToastTemplateType type = WinToastTemplateType::ImageAndText02); + ~WinToastTemplate(); + + void setFirstLine(_In_ std::wstring const& text); + void setSecondLine(_In_ std::wstring const& text); + void setThirdLine(_In_ std::wstring const& text); + void setTextField(_In_ std::wstring const& txt, _In_ TextField pos); + void setAttributionText(_In_ std::wstring const& attributionText); + void setImagePath(_In_ std::wstring const& imgPath, _In_ CropHint cropHint = CropHint::Square); + void setHeroImagePath(_In_ std::wstring const& imgPath, _In_ bool inlineImage = false); + void setAudioPath(_In_ WinToastTemplate::AudioSystemFile audio); + void setAudioPath(_In_ std::wstring const& audioPath); + void setAudioOption(_In_ WinToastTemplate::AudioOption audioOption); + void setDuration(_In_ Duration duration); + void setExpiration(_In_ INT64 millisecondsFromNow); + void setScenario(_In_ Scenario scenario); + void addAction(_In_ std::wstring const& label); + void addInput(); + + std::size_t textFieldsCount() const; + std::size_t actionsCount() const; + bool hasImage() const; + bool hasHeroImage() const; + std::vector const& textFields() const; + std::wstring const& textField(_In_ TextField pos) const; + std::wstring const& actionLabel(_In_ std::size_t pos) const; + std::wstring const& imagePath() const; + std::wstring const& heroImagePath() const; + std::wstring const& audioPath() const; + std::wstring const& attributionText() const; + std::wstring const& scenario() const; + INT64 expiration() const; + WinToastTemplateType type() const; + WinToastTemplate::AudioOption audioOption() const; + Duration duration() const; + bool isToastGeneric() const; + bool isInlineHeroImage() const; + bool isCropHintCircle() const; + bool isInput() const; + + private: + bool _hasInput{false}; + + std::vector _textFields{}; + std::vector _actions{}; + std::wstring _imagePath{}; + std::wstring _heroImagePath{}; + bool _inlineHeroImage{false}; + std::wstring _audioPath{}; + std::wstring _attributionText{}; + std::wstring _scenario{L"Default"}; + INT64 _expiration{0}; + AudioOption _audioOption{WinToastTemplate::AudioOption::Default}; + WinToastTemplateType _type{WinToastTemplateType::Text01}; + Duration _duration{Duration::System}; + CropHint _cropHint{CropHint::Square}; + }; + + class WinToast { + public: + enum WinToastError { + NoError = 0, + NotInitialized, + SystemNotSupported, + ShellLinkNotCreated, + InvalidAppUserModelID, + InvalidParameters, + InvalidHandler, + NotDisplayed, + UnknownError + }; + + enum ShortcutResult { + SHORTCUT_UNCHANGED = 0, + SHORTCUT_WAS_CHANGED = 1, + SHORTCUT_WAS_CREATED = 2, + + SHORTCUT_MISSING_PARAMETERS = -1, + SHORTCUT_INCOMPATIBLE_OS = -2, + SHORTCUT_COM_INIT_FAILURE = -3, + SHORTCUT_CREATE_FAILED = -4 + }; + + enum ShortcutPolicy { + /* Don't check, create, or modify a shortcut. */ + SHORTCUT_POLICY_IGNORE = 0, + /* Require a shortcut with matching AUMI, don't create or modify an existing one. */ + SHORTCUT_POLICY_REQUIRE_NO_CREATE = 1, + /* Require a shortcut with matching AUMI, create if missing, modify if not matching. This is the default. */ + SHORTCUT_POLICY_REQUIRE_CREATE = 2, + }; + + WinToast(void); + virtual ~WinToast(); + static WinToast* instance(); + static bool isCompatible(); + static bool isSupportingModernFeatures(); + static bool isWin10AnniversaryOrHigher(); + static std::wstring configureAUMI(_In_ std::wstring const& companyName, _In_ std::wstring const& productName, + _In_ std::wstring const& subProduct = std::wstring(), + _In_ std::wstring const& versionInformation = std::wstring()); + static std::wstring const& strerror(_In_ WinToastError error); + virtual bool initialize(_Out_opt_ WinToastError* error = nullptr); + virtual bool isInitialized() const; + virtual bool hideToast(_In_ INT64 id); + virtual INT64 showToast(_In_ WinToastTemplate const& toast, _In_ IWinToastHandler* eventHandler, + _Out_opt_ WinToastError* error = nullptr); + virtual void clear(); + virtual enum ShortcutResult createShortcut(); + + std::wstring const& appName() const; + std::wstring const& appUserModelId() const; + void setAppUserModelId(_In_ std::wstring const& aumi); + void setAppName(_In_ std::wstring const& appName); + void setShortcutPolicy(_In_ ShortcutPolicy policy); + + protected: + struct NotifyData { + NotifyData(){}; + NotifyData(_In_ ComPtr notify, _In_ EventRegistrationToken activatedToken, + _In_ EventRegistrationToken dismissedToken, _In_ EventRegistrationToken failedToken) : + _notify(notify), _activatedToken(activatedToken), _dismissedToken(dismissedToken), _failedToken(failedToken) {} + + ~NotifyData() { + RemoveTokens(); + } + + void RemoveTokens() { + if (!_readyForDeletion) { + return; + } + + if (_previouslyTokenRemoved) { + return; + } + + if (!_notify.Get()) { + return; + } + + _notify->remove_Activated(_activatedToken); + _notify->remove_Dismissed(_dismissedToken); + _notify->remove_Failed(_failedToken); + _previouslyTokenRemoved = true; + } + + void markAsReadyForDeletion() { + _readyForDeletion = true; + } + + bool isReadyForDeletion() const { + return _readyForDeletion; + } + + IToastNotification* notification() { + return _notify.Get(); + } + + private: + ComPtr _notify{nullptr}; + EventRegistrationToken _activatedToken{}; + EventRegistrationToken _dismissedToken{}; + EventRegistrationToken _failedToken{}; + bool _readyForDeletion{false}; + bool _previouslyTokenRemoved{false}; + }; + + bool _isInitialized{false}; + bool _hasCoInitialized{false}; + ShortcutPolicy _shortcutPolicy{SHORTCUT_POLICY_REQUIRE_CREATE}; + std::wstring _appName{}; + std::wstring _aumi{}; + std::map _buffer{}; + + void markAsReadyForDeletion(_In_ INT64 id); + HRESULT validateShellLinkHelper(_Out_ bool& wasChanged); + HRESULT createShellLinkHelper(); + HRESULT setImageFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isToastGeneric, bool isCropHintCircle); + HRESULT setHeroImageHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isInlineImage); + HRESULT setBindToastGenericHelper(_In_ IXmlDocument* xml); + HRESULT + setAudioFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, + _In_opt_ WinToastTemplate::AudioOption option = WinToastTemplate::AudioOption::Default); + HRESULT setTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text, _In_ UINT32 pos); + HRESULT setAttributionTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text); + HRESULT addActionHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& action, _In_ std::wstring const& arguments); + HRESULT addDurationHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& duration); + HRESULT addScenarioHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& scenario); + HRESULT addInputHelper(_In_ IXmlDocument* xml); + ComPtr notifier(_In_ bool* succeded) const; + void setError(_Out_opt_ WinToastError* error, _In_ WinToastError value); + }; +} // namespace WinToastLib +#endif // WINTOASTLIB_H diff --git a/samples/Notify.NET.Sample/Notify.NET.Sample.csproj b/samples/Notify.NET.Sample/Notify.NET.Sample.csproj new file mode 100644 index 0000000..2961686 --- /dev/null +++ b/samples/Notify.NET.Sample/Notify.NET.Sample.csproj @@ -0,0 +1,20 @@ + + + + Exe + + net6.0 + 10 + enable + Notify.NET.Sample + + + + + + + + + + + diff --git a/samples/Notify.NET.Sample/Program.cs b/samples/Notify.NET.Sample/Program.cs new file mode 100644 index 0000000..221d705 --- /dev/null +++ b/samples/Notify.NET.Sample/Program.cs @@ -0,0 +1,153 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Notify.NET.Abstractions; +using Notify.NET.Builder; +using Notify.NET.Extensions; + +// ============================================================================ +// Notify.NET Sample — demonstrates fluent builder + DI registration +// ============================================================================ + +// --- Option A: use the factory helper directly (no DI container) --- +using var service = ServiceCollectionExtensions.CreateNotificationService(opts => +{ + opts.AppName = "Notify.NET Sample"; + opts.AppUserModelId = "NotifyNET.Sample"; +}); + +Console.WriteLine($"Notification service supported: {service.IsSupported}"); + +if (!service.IsSupported) +{ + Console.WriteLine("Native notifications are not available on this platform. Exiting."); + return; +} + +var done = new ManualResetEventSlim(false); + +// ------ 1. Simple notification (title + body) -------------------------------- +Console.WriteLine("\n[1] Showing a simple notification..."); + +long id1 = await service.ShowAsync( + NotificationBuilder.Create("Hello from Notify.NET") + .WithBody("This is a simple cross-platform OS notification.") + .OnActivated(id => Console.WriteLine($" [1] Notification {id} activated")) + .OnDismissed((id, reason) => Console.WriteLine($" [1] Notification {id} dismissed: {reason}")) + .Build()); + +Console.WriteLine($" Shown with id={id1}"); +await Task.Delay(3000); + +// ------ 2. Notification with buttons ---------------------------------------- +Console.WriteLine("\n[2] Showing a notification with action buttons..."); + +var buttonDone = new ManualResetEventSlim(false); + +long id2 = await service.ShowAsync( + NotificationBuilder.Create("Update Available") + .WithBody("Version 2.0 is ready to install.") + .AddButton("Install Now", id => { Console.WriteLine($" [2] Install Now clicked (id={id})"); buttonDone.Set(); }) + .AddButton("Remind Me", id => { Console.WriteLine($" [2] Remind Me clicked (id={id})"); buttonDone.Set(); }) + .AddButton("Skip Version", id => { Console.WriteLine($" [2] Skip Version clicked (id={id})"); buttonDone.Set(); }) + .OnDismissed((id, reason) => { Console.WriteLine($" [2] Dismissed: {reason}"); buttonDone.Set(); }) + .OnFailed(id => { Console.WriteLine($" [2] Failed for id={id}"); buttonDone.Set(); }) + .Build()); + +Console.WriteLine($" Shown with id={id2}. Waiting up to 15 s for interaction..."); +buttonDone.Wait(TimeSpan.FromSeconds(15)); + +// ------ 3. Notification with an image ---------------------------------------- +Console.WriteLine("\n[3] Showing a notification with an image..."); + +// Provide any PNG/JPG path; the sample gracefully handles a missing file +// because the native service falls back to no image when gdk_pixbuf_new_from_file fails. +string imagePath = "image.jpg"; + +long id3 = -1; +try +{ + id3 = await service.ShowAsync( + NotificationBuilder.Create("Picture Notification") + .WithBody("This notification includes an image.") + .WithImage(imagePath) + .WithUrgency(NotificationUrgency.Low) + .OnActivated(id => Console.WriteLine($" [3] Activated id={id}")) + .Build()); + Console.WriteLine($" Shown with id={id3}"); +} +catch (Exception ex) +{ + Console.WriteLine($" [3] Failed: {ex.GetType().Name}: {ex.Message}"); +} +await Task.Delay(3000); + +// ------ 4. Programmatic dismiss ---------------------------------------------- +Console.WriteLine("\n[4] Showing a notification and dismissing it programmatically after 2 s..."); + +long id4 = await service.ShowAsync( + NotificationBuilder.Create("I will disappear in 2 seconds") + .WithBody("Programmatically dismissed.") + .OnDismissed((id, reason) => Console.WriteLine($" [4] Dismissed: {reason}")) + .Build()); + +Console.WriteLine($" Shown with id={id4}. Hiding in 2 s..."); +await Task.Delay(2000); +await service.HideAsync(id4); +Console.WriteLine(" Hidden."); + +// ------ 5. Interface-based handler ------------------------------------------- +Console.WriteLine("\n[5] Showing a notification using INotificationHandler..."); + +long id5 = await service.ShowAsync( + NotificationBuilder.Create("Handler-based Notification") + .WithBody("Uses a custom INotificationHandler implementation.") + .AddButton("Acknowledge", null) + .WithHandler(new SampleHandler()) + .Build()); + +Console.WriteLine($" Shown with id={id5}. Waiting 10 s..."); +await Task.Delay(10_000); + +// ------ 6. DI container usage ------------------------------------------------ +Console.WriteLine("\n[6] Demonstrating DI container registration..."); + +var services = new ServiceCollection(); +services.AddNotifications(opts => +{ + opts.AppName = "Notify.NET Sample (DI)"; + opts.AppUserModelId = "NotifyNET.Sample.DI"; +}); + +await using var provider = services.BuildServiceProvider(); +var diService = provider.GetRequiredService(); + +long id6 = await diService.ShowAsync( + NotificationBuilder.Create("DI-registered Service") + .WithBody("This notification was shown via an IServiceProvider-resolved service.") + .Build()); + +Console.WriteLine($" Shown with id={id6}"); +await Task.Delay(3000); + +Console.WriteLine("\nAll done."); + +// ============================================================================ +// Sample INotificationHandler implementation +// ============================================================================ + +sealed class SampleHandler : INotificationHandler +{ + public void OnActivated(long id) + => Console.WriteLine($" [5] SampleHandler.OnActivated(id={id})"); + + public void OnButtonActivated(long id, int buttonIndex) + => Console.WriteLine($" [5] SampleHandler.OnButtonActivated(id={id}, button={buttonIndex})"); + + public void OnDismissed(long id, DismissReason reason) + => Console.WriteLine($" [5] SampleHandler.OnDismissed(id={id}, reason={reason})"); + + public void OnFailed(long id) + => Console.WriteLine($" [5] SampleHandler.OnFailed(id={id})"); +} diff --git a/samples/Notify.NET.Sample/image.jpg b/samples/Notify.NET.Sample/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5bfee8fbc962e31d62feb813a575bf28732d0c95 GIT binary patch literal 168087 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 z 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 z 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 z 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 + /// Receives lifecycle events for a single notification. + /// Implement this interface or use the delegate-based callbacks on + /// to respond to user interactions. + /// + public interface INotificationHandler + { + /// Called when the user clicks the notification body (not a button). + void OnActivated(long notificationId); + + /// Called when the user clicks one of the action buttons. + /// The notification's platform ID. + /// Zero-based index matching the order buttons were added via the builder. + void OnButtonActivated(long notificationId, int buttonIndex); + + /// Called when the notification is dismissed (by the user, system, or expiration). + void OnDismissed(long notificationId, DismissReason reason); + + /// Called when the platform fails to display the notification. + void OnFailed(long notificationId); + } +} diff --git a/src/Notify.NET/Abstractions/INotificationService.cs b/src/Notify.NET/Abstractions/INotificationService.cs new file mode 100644 index 0000000..10f4e19 --- /dev/null +++ b/src/Notify.NET/Abstractions/INotificationService.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Notify.NET.Abstractions +{ + /// + /// Dispatches OS notifications to the native notification subsystem for the current platform. + /// + public interface INotificationService : IDisposable + { + /// + /// Whether the native notification subsystem is available and initialised on this platform. + /// If false, will throw . + /// + bool IsSupported { get; } + + /// + /// Displays a notification and returns a platform-specific ID that can be used to hide it later. + /// + /// The notification to display, constructed via . + /// Optional cancellation token. + /// A non-negative notification ID on success. + Task ShowAsync(NotificationRequest request, CancellationToken cancellationToken = default); + + /// + /// Programmatically dismisses a previously shown notification. + /// + /// The ID returned by . + /// Optional cancellation token. + Task HideAsync(long notificationId, CancellationToken cancellationToken = default); + } +} diff --git a/src/Notify.NET/Abstractions/NotificationRequest.cs b/src/Notify.NET/Abstractions/NotificationRequest.cs new file mode 100644 index 0000000..9cce3fa --- /dev/null +++ b/src/Notify.NET/Abstractions/NotificationRequest.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using Notify.NET.Builder; + +namespace Notify.NET.Abstractions +{ + /// + /// Immutable description of a notification to be displayed. + /// Construct instances via . + /// + public sealed class NotificationRequest + { + /// The primary heading of the notification. + public string Title { get; } + + /// Optional body text shown beneath the title. + public string? Body { get; } + + /// Absolute path to an image file to display in the notification. + public string? ImagePath { get; } + + /// Action buttons to display. Maximum platform limits apply (typically 5 on Windows, varies on Linux). + public IReadOnlyList Buttons { get; } + + /// Optional interface-based handler for notification lifecycle events. + public INotificationHandler? Handler { get; } + + /// How long to display the notification before it expires automatically. Null means use the platform default. + public TimeSpan? Expiration { get; } + + /// Audio behaviour when the notification appears. + public NotificationAudio Audio { get; } + + /// The urgency/scenario of the notification, which may affect how the platform presents it. + public NotificationUrgency Urgency { get; } + + internal NotificationRequest( + string title, + string? body, + string? imagePath, + IReadOnlyList buttons, + INotificationHandler? handler, + TimeSpan? expiration, + NotificationAudio audio, + NotificationUrgency urgency) + { + if (string.IsNullOrWhiteSpace(title)) + throw new ArgumentException("Notification title must not be empty.", nameof(title)); + + Title = title; + Body = body; + ImagePath = imagePath; + Buttons = buttons; + Handler = handler; + Expiration = expiration; + Audio = audio; + Urgency = urgency; + } + } + + /// Controls the audio played when the notification is shown (Windows only; Linux ignores this). + public enum NotificationAudio + { + /// Play the platform default notification sound. + Default, + /// Display silently with no sound. + Silent, + /// Loop the notification sound until the notification is dismissed. + Loop + } + + /// Maps to the notification urgency/scenario on each platform. + public enum NotificationUrgency + { + /// Standard informational notification. + Normal, + /// Low-priority; the platform may suppress or delay it. + Low, + /// High-priority; may bypass Do Not Disturb on some platforms. + Critical, + /// Alarm scenario (Windows) — may produce a full-screen interrupt. + Alarm, + /// Reminder scenario (Windows). + Reminder + } + + /// Reason a notification was dismissed. + public enum DismissReason + { + /// The user explicitly dismissed the notification. + UserCancelled, + /// The notification timed out / expired. + TimedOut, + /// The application programmatically hid the notification. + ApplicationHidden, + /// Dismissed for an unspecified or platform-specific reason. + Unknown + } +} diff --git a/src/Notify.NET/Builder/NotificationBuilder.cs b/src/Notify.NET/Builder/NotificationBuilder.cs new file mode 100644 index 0000000..6117fc6 --- /dev/null +++ b/src/Notify.NET/Builder/NotificationBuilder.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using Notify.NET.Abstractions; + +namespace Notify.NET.Builder +{ + /// + /// Fluent builder for constructing a . + /// + /// + /// + /// var request = NotificationBuilder.Create("Update available") + /// .WithBody("Version 2.0 is ready to install.") + /// .WithImage("/usr/share/icons/my-app.png") + /// .AddButton("Install now", id => Installer.Run()) + /// .AddButton("Remind me later", id => Snooze(id)) + /// .OnActivated(id => Console.WriteLine($"Notification {id} clicked")) + /// .OnDismissed((id, reason) => Console.WriteLine($"Dismissed: {reason}")) + /// .Build(); + /// + /// long id = await notificationService.ShowAsync(request); + /// + /// + public sealed class NotificationBuilder + { + private string _title = string.Empty; + private string? _body; + private string? _imagePath; + private readonly List _buttons = new List(); + private INotificationHandler? _handler; + private TimeSpan? _expiration; + private NotificationAudio _audio = NotificationAudio.Default; + private NotificationUrgency _urgency = NotificationUrgency.Normal; + + // Delegate-based callbacks (converted to INotificationHandler in Build()) + private Action? _onActivated; + private Action? _onButtonActivated; + private Action? _onDismissed; + private Action? _onFailed; + + private NotificationBuilder() { } + + /// Creates a new builder with the specified notification title. + public static NotificationBuilder Create(string title) + => new NotificationBuilder { _title = title }; + + /// Sets the notification title. + public NotificationBuilder WithTitle(string title) + { + _title = title ?? throw new ArgumentNullException(nameof(title)); + return this; + } + + /// Sets the notification body text. + public NotificationBuilder WithBody(string body) + { + _body = body; + return this; + } + + /// Sets the absolute path of an image to display in the notification. + public NotificationBuilder WithImage(string imagePath) + { + _imagePath = imagePath; + return this; + } + + /// Adds an action button with an optional click callback. + /// Text shown on the button. + /// Called with the notification ID when the button is clicked. + /// Optional machine-readable action identifier. + public NotificationBuilder AddButton(string label, Action? callback = null, string? actionId = null) + { + _buttons.Add(new NotificationButton(label, callback, actionId)); + return this; + } + + /// Adds a pre-constructed button. + public NotificationBuilder AddButton(NotificationButton button) + { + _buttons.Add(button ?? throw new ArgumentNullException(nameof(button))); + return this; + } + + /// + /// Attaches an interface-based handler for all notification lifecycle events. + /// This takes priority over any delegate-based callbacks registered via + /// , , or . + /// + public NotificationBuilder WithHandler(INotificationHandler handler) + { + _handler = handler; + return this; + } + + /// Registers a callback for when the notification body is clicked. + public NotificationBuilder OnActivated(Action callback) + { + _onActivated = callback; + return this; + } + + /// Registers a callback for when a specific action button is clicked. + public NotificationBuilder OnButtonActivated(Action callback) + { + _onButtonActivated = callback; + return this; + } + + /// Registers a callback for when the notification is dismissed. + public NotificationBuilder OnDismissed(Action callback) + { + _onDismissed = callback; + return this; + } + + /// Registers a callback for when the notification fails to display. + public NotificationBuilder OnFailed(Action callback) + { + _onFailed = callback; + return this; + } + + /// + /// Sets how long the notification remains visible before auto-dismissal. + /// Pass or null to use the platform default. + /// + public NotificationBuilder WithExpiration(TimeSpan expiration) + { + _expiration = expiration == TimeSpan.Zero ? (TimeSpan?)null : expiration; + return this; + } + + /// Controls the sound played when the notification appears (Windows only). + public NotificationBuilder WithAudio(NotificationAudio audio) + { + _audio = audio; + return this; + } + + /// Sets the urgency/scenario which may affect how the platform presents the notification. + public NotificationBuilder WithUrgency(NotificationUrgency urgency) + { + _urgency = urgency; + return this; + } + + /// + /// Constructs the immutable . + /// Throws if has not been set. + /// + public NotificationRequest Build() + { + if (string.IsNullOrWhiteSpace(_title)) + throw new InvalidOperationException("Notification title must be set before calling Build()."); + + // If an explicit INotificationHandler was provided, use it directly. + // Otherwise, if any delegate callbacks were registered, wrap them. + INotificationHandler? handler = _handler; + if (handler == null && (_onActivated != null || _onButtonActivated != null || _onDismissed != null || _onFailed != null)) + { + handler = new DelegateNotificationHandler(_onActivated, _onButtonActivated, _onDismissed, _onFailed); + } + + return new NotificationRequest( + title: _title, + body: _body, + imagePath: _imagePath, + buttons: _buttons.AsReadOnly(), + handler: handler, + expiration: _expiration, + audio: _audio, + urgency: _urgency); + } + + // Internal adapter that bridges the delegate callbacks to INotificationHandler. + private sealed class DelegateNotificationHandler : INotificationHandler + { + private readonly Action? _activated; + private readonly Action? _buttonActivated; + private readonly Action? _dismissed; + private readonly Action? _failed; + + public DelegateNotificationHandler( + Action? activated, + Action? buttonActivated, + Action? dismissed, + Action? failed) + { + _activated = activated; + _buttonActivated = buttonActivated; + _dismissed = dismissed; + _failed = failed; + } + + public void OnActivated(long id) => _activated?.Invoke(id); + public void OnButtonActivated(long id, int idx) => _buttonActivated?.Invoke(id, idx); + public void OnDismissed(long id, DismissReason reason) => _dismissed?.Invoke(id, reason); + public void OnFailed(long id) => _failed?.Invoke(id); + } + } +} diff --git a/src/Notify.NET/Builder/NotificationButton.cs b/src/Notify.NET/Builder/NotificationButton.cs new file mode 100644 index 0000000..27acc2c --- /dev/null +++ b/src/Notify.NET/Builder/NotificationButton.cs @@ -0,0 +1,36 @@ +using System; + +namespace Notify.NET.Builder +{ + /// + /// An action button displayed inside a notification. + /// + public sealed class NotificationButton + { + /// The label shown on the button. + public string Label { get; } + + /// + /// Callback invoked when the user clicks this button. + /// The argument is the platform notification ID. + /// The callback is invoked on a background thread; marshal to the UI thread if required. + /// + public Action? Callback { get; } + + /// + /// An optional machine-readable identifier for this action (used internally by libnotify). + /// Defaults to a sanitised version of when not specified. + /// + public string ActionId { get; } + + public NotificationButton(string label, Action? callback = null, string? actionId = null) + { + if (string.IsNullOrWhiteSpace(label)) + throw new ArgumentException("Button label must not be empty.", nameof(label)); + + Label = label; + Callback = callback; + ActionId = actionId ?? label.ToLowerInvariant().Replace(' ', '-'); + } + } +} diff --git a/src/Notify.NET/Exceptions/NotificationException.cs b/src/Notify.NET/Exceptions/NotificationException.cs new file mode 100644 index 0000000..64f3ab0 --- /dev/null +++ b/src/Notify.NET/Exceptions/NotificationException.cs @@ -0,0 +1,22 @@ +using System; + +namespace Notify.NET.Exceptions +{ + /// Thrown when the native notification subsystem returns an error. + public sealed class NotificationException : Exception + { + /// Platform-specific error code, if available. + public int? NativeErrorCode { get; } + + public NotificationException(string message) : base(message) { } + + public NotificationException(string message, int nativeErrorCode) + : base(message) + { + NativeErrorCode = nativeErrorCode; + } + + public NotificationException(string message, Exception innerException) + : base(message, innerException) { } + } +} diff --git a/src/Notify.NET/Exceptions/PlatformNotSupportedException.cs b/src/Notify.NET/Exceptions/PlatformNotSupportedException.cs new file mode 100644 index 0000000..0e89897 --- /dev/null +++ b/src/Notify.NET/Exceptions/PlatformNotSupportedException.cs @@ -0,0 +1,20 @@ +using System; + +namespace Notify.NET.Exceptions +{ + /// + /// Thrown when is called on a platform + /// where the native notification subsystem is unavailable or could not be initialised. + /// Check before calling Show. + /// + public sealed class PlatformNotSupportedException : Exception + { + public PlatformNotSupportedException() + : base("Native notifications are not supported or could not be initialised on this platform.") { } + + public PlatformNotSupportedException(string message) : base(message) { } + + public PlatformNotSupportedException(string message, Exception innerException) + : base(message, innerException) { } + } +} diff --git a/src/Notify.NET/Extensions/ServiceCollectionExtensions.cs b/src/Notify.NET/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..6bb705f --- /dev/null +++ b/src/Notify.NET/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,117 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Notify.NET.Abstractions; +using Notify.NET.Platform.Windows; +using Notify.NET.Platform.Linux; +using Notify.NET.Platform.MacOS; + +namespace Notify.NET.Extensions +{ + /// + /// Extension methods for registering with an + /// . The correct platform implementation is selected + /// automatically at runtime. + /// + public static class ServiceCollectionExtensions + { + /// + /// Registers as a singleton, using the + /// platform-appropriate backend: + /// + /// Windows → (WinToastLib) + /// Linux → (libnotify) + /// macOS → (UNUserNotificationCenter) + /// Other → ( = false) + /// + /// + /// The service collection to add to. + /// Optional delegate to configure . + public static IServiceCollection AddNotifications( + this IServiceCollection services, + Action? configure = null) + { + var options = new NotificationOptions(); + configure?.Invoke(options); + + services.AddSingleton(options); + + services.AddSingleton(sp => + { + var opts = sp.GetRequiredService(); + return CreateService(opts); + }); + + return services; + } + + /// + /// Creates the platform-appropriate directly + /// (without a DI container), for use in simple console applications. + /// + public static INotificationService CreateNotificationService( + Action? configure = null) + { + var opts = new NotificationOptions(); + configure?.Invoke(opts); + return CreateService(opts); + } + + private static INotificationService CreateService(NotificationOptions opts) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return new WindowsNotificationService(opts.AppName, opts.AppUserModelId); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return new LinuxNotificationService(opts.AppName); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return new MacOSNotificationService(opts.AppName); + + return new NullNotificationService(); + } + } + + /// + /// Configuration options for the notification service. + /// Pass to via the configure delegate. + /// + public sealed class NotificationOptions + { + /// + /// Human-readable application name shown in the notification and Action Centre. + /// Defaults to the process name. + /// + public string AppName { get; set; } = + System.Diagnostics.Process.GetCurrentProcess().ProcessName; + + /// + /// Windows AppUserModelId (AUMI), e.g. "MyCompany.MyApp". + /// Required for notifications to persist in the Windows Action Centre. + /// The native wrapper creates a Start-Menu shortcut with this AUMI automatically. + /// Ignored on non-Windows platforms. + /// + public string AppUserModelId { get; set; } = + System.Diagnostics.Process.GetCurrentProcess().ProcessName; + } + + /// + /// No-op implementation used when the current platform has no supported notification backend. + /// is always false; calling throws + /// . + /// + internal sealed class NullNotificationService : INotificationService + { + public bool IsSupported => false; + + public Task ShowAsync(NotificationRequest request, CancellationToken cancellationToken = default) + => throw new Exceptions.PlatformNotSupportedException(); + + public Task HideAsync(long notificationId, CancellationToken cancellationToken = default) + => throw new Exceptions.PlatformNotSupportedException(); + + public void Dispose() { } + } +} diff --git a/src/Notify.NET/Notify.NET.csproj b/src/Notify.NET/Notify.NET.csproj new file mode 100644 index 0000000..5b4a755 --- /dev/null +++ b/src/Notify.NET/Notify.NET.csproj @@ -0,0 +1,49 @@ + + + + netstandard2.1 + 9.0 + true + enable + Notify.NET + Notify.NET + Notify.NET + 1.0.0 + Cross-platform OS notification library for .NET Standard with WinToast (Windows), libnotify (Linux), and UNUserNotificationCenter (macOS) backends. + + + + + + + + + + + + runtimes/win-x64/native/WinToastWrapper.dll + PreserveNewest + + + runtimes/win-x86/native/WinToastWrapper.dll + PreserveNewest + + + runtimes/win-arm64/native/WinToastWrapper.dll + PreserveNewest + + + + + + + runtimes/osx-x64/native/libMacNotifyWrapper.dylib + PreserveNewest + + + runtimes/osx-arm64/native/libMacNotifyWrapper.dylib + PreserveNewest + + + + diff --git a/src/Notify.NET/Platform/Linux/GLibMainLoopRunner.cs b/src/Notify.NET/Platform/Linux/GLibMainLoopRunner.cs new file mode 100644 index 0000000..c7de570 --- /dev/null +++ b/src/Notify.NET/Platform/Linux/GLibMainLoopRunner.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Notify.NET.Platform.Linux +{ + /// + /// Owns and manages a GLib GMainLoop on a dedicated background thread. + /// + /// libnotify delivers notification signals (action-invoked, closed) through the GLib + /// event system and requires a running GMainLoop to dispatch them. Console applications + /// and ASP.NET hosts do not have one by default, so this class creates and owns one. + /// + /// All calls to libnotify that create/show/close notifications MUST be executed on the + /// GMainLoop thread to ensure proper GObject signal wiring. Use + /// to marshal work onto that thread. + /// + internal sealed class GLibMainLoopRunner : IDisposable + { + private readonly Thread _loopThread; + private readonly ManualResetEventSlim _started = new ManualResetEventSlim(false); + + // Pinned GSourceFunc delegate — static, never collected. + private static readonly LibNotifyNative.GSourceFunc _dispatchSourceFunc = DispatchSourceFuncStatic; + private static readonly IntPtr _dispatchFuncPtr = + Marshal.GetFunctionPointerForDelegate(_dispatchSourceFunc); + + // Work items posted from external threads via g_main_context_invoke. + // We use a ConcurrentQueue keyed by a GCHandle to the WorkItem so we can pass a + // single IntPtr through the GLib userData parameter. + private IntPtr _mainLoop; + private bool _disposed; + + internal GLibMainLoopRunner() + { + _loopThread = new Thread(LoopThreadProc) + { + Name = "Notify.NET GMainLoop", + IsBackground = true + }; + _loopThread.Start(); + _started.Wait(); + } + + // ------------------------------------------------------------------ + // Public API + // ------------------------------------------------------------------ + + /// + /// Posts to be executed on the GMainLoop thread and + /// returns a task that completes when the action finishes. + /// + internal System.Threading.Tasks.Task InvokeAsync(Action action) + { + var tcs = new System.Threading.Tasks.TaskCompletionSource(); + var item = new WorkItem(action, tcs); + + // Allocate a GCHandle to keep the WorkItem alive from unmanaged code. + GCHandle handle = GCHandle.Alloc(item, GCHandleType.Normal); + + // g_main_context_invoke(null) posts to the default context, which is owned + // by our GMainLoop thread. + LibNotifyNative.g_main_context_invoke(IntPtr.Zero, _dispatchFuncPtr, GCHandle.ToIntPtr(handle)); + + return tcs.Task; + } + + // ------------------------------------------------------------------ + // IDisposable + // ------------------------------------------------------------------ + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (_mainLoop != IntPtr.Zero) + { + LibNotifyNative.g_main_loop_quit(_mainLoop); + if (_loopThread.IsAlive) + _loopThread.Join(TimeSpan.FromSeconds(5)); + + LibNotifyNative.g_main_loop_unref(_mainLoop); + _mainLoop = IntPtr.Zero; + } + + _started.Dispose(); + } + + // ------------------------------------------------------------------ + // Private + // ------------------------------------------------------------------ + + private void LoopThreadProc() + { + _mainLoop = LibNotifyNative.g_main_loop_new(IntPtr.Zero, false); + _started.Set(); + LibNotifyNative.g_main_loop_run(_mainLoop); + // g_main_loop_unref is called in Dispose, not here, to avoid double-unref. + } + + // Static GSourceFunc — invoked on the GMainLoop thread by GLib. + // Returns false so GLib removes the source after one invocation. + private static bool DispatchSourceFuncStatic(IntPtr userData) + { + GCHandle handle = GCHandle.FromIntPtr(userData); + var item = (WorkItem)handle.Target!; + handle.Free(); + + try { item.Action(); item.Tcs.TrySetResult(true); } + catch (Exception ex) { item.Tcs.TrySetException(ex); } + + return false; // one-shot + } + + private sealed class WorkItem + { + internal readonly Action Action; + internal readonly System.Threading.Tasks.TaskCompletionSource Tcs; + internal WorkItem(Action action, System.Threading.Tasks.TaskCompletionSource tcs) + { Action = action; Tcs = tcs; } + } + } +} diff --git a/src/Notify.NET/Platform/Linux/LibNotifyCallbackBridge.cs b/src/Notify.NET/Platform/Linux/LibNotifyCallbackBridge.cs new file mode 100644 index 0000000..1620de3 --- /dev/null +++ b/src/Notify.NET/Platform/Linux/LibNotifyCallbackBridge.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using Notify.NET.Abstractions; + +namespace Notify.NET.Platform.Linux +{ + /// + /// Bridges the unmanaged libnotify GObject signal callbacks back to the managed + /// for each in-flight notification. + /// + /// Threading: GLib delivers both the action and closed signals on the GMainLoop thread. + /// Handler invocations therefore happen on the GMainLoop background thread; + /// consumers are responsible for marshalling to a UI thread if required. + /// + /// Lifetime rules (mirror the Windows bridge): + /// 1. Static delegates are pinned permanently — their function pointers are valid forever. + /// 2. Per-notification bridges are held in a static ConcurrentDictionary keyed by the + /// native NotifyNotification* pointer (cast to long). + /// 3. The bridge's GCHandle prevents GC collection until the "closed" signal fires. + /// 4. Release() is called once from the closed callback; it frees the GCHandle, + /// unrefs the GObject, and removes the dictionary entry. + /// + internal sealed class LibNotifyCallbackBridge + { + // ------------------------------------------------------------------ + // Static callbacks — one instance shared across all notifications + // ------------------------------------------------------------------ + private static readonly LibNotifyNative.NotifyActionCallback _staticAction; + private static readonly LibNotifyNative.NotifyClosedCallback _staticClosed; + + internal static readonly IntPtr PtrAction; + internal static readonly IntPtr PtrClosed; + + // ------------------------------------------------------------------ + // Live bridge registry + // ------------------------------------------------------------------ + private static readonly ConcurrentDictionary _live + = new ConcurrentDictionary(); + + // ------------------------------------------------------------------ + // Per-instance state + // ------------------------------------------------------------------ + private readonly INotificationHandler? _handler; + private readonly System.Collections.Generic.IReadOnlyList _buttons; + private GCHandle _gcHandle; + + static LibNotifyCallbackBridge() + { + _staticAction = OnActionStatic; + _staticClosed = OnClosedStatic; + + PtrAction = Marshal.GetFunctionPointerForDelegate(_staticAction); + PtrClosed = Marshal.GetFunctionPointerForDelegate(_staticClosed); + } + + private LibNotifyCallbackBridge( + INotificationHandler? handler, + System.Collections.Generic.IReadOnlyList buttons) + { + _handler = handler; + _buttons = buttons; + _gcHandle = GCHandle.Alloc(this, GCHandleType.Normal); + } + + /// + /// Registers a bridge for the native notification pointer. + /// Must be called on the GMainLoop thread BEFORE connecting GObject signals, + /// so the dictionary entry exists before any signal can fire. + /// + internal static LibNotifyCallbackBridge Register( + IntPtr notificationPtr, + INotificationHandler? handler, + System.Collections.Generic.IReadOnlyList buttons) + { + var bridge = new LibNotifyCallbackBridge(handler, buttons); + _live[(long)notificationPtr] = bridge; + return bridge; + } + + /// + /// Removes the bridge and releases all resources. + /// Called from the "closed" signal handler — do not call from application code. + /// + internal static void Release(IntPtr notificationPtr) + { + long key = (long)notificationPtr; + if (_live.TryRemove(key, out var bridge)) + { + if (bridge._gcHandle.IsAllocated) + bridge._gcHandle.Free(); + + // Release the libnotify GObject reference. + LibNotifyNative.g_object_unref(notificationPtr); + } + } + + // ------------------------------------------------------------------ + // Static GLib signal handlers — called on the GMainLoop thread + // ------------------------------------------------------------------ + + private static void OnActionStatic(IntPtr notification, string action, IntPtr userData) + { + try + { + long key = (long)notification; + if (!_live.TryGetValue(key, out var bridge)) return; + + for (int i = 0; i < bridge._buttons.Count; i++) + { + if (string.Equals(bridge._buttons[i].ActionId, action, StringComparison.Ordinal)) + { + bridge._buttons[i].Callback?.Invoke(key); + bridge._handler?.OnButtonActivated(key, i); + return; + } + } + + bridge._handler?.OnActivated(key); + } + catch { } + } + + private static void OnClosedStatic(IntPtr notification, IntPtr userData) + { + try + { + long key = (long)notification; + if (_live.TryGetValue(key, out var bridge)) + { + int reason = LibNotifyNative.notify_notification_get_closed_reason(notification); + bridge._handler?.OnDismissed(key, MapCloseReason(reason)); + } + } + catch { } + finally { Release(notification); } + } + + private static DismissReason MapCloseReason(int reason) + { + // freedesktop.org notification spec close reasons: + // 1 = expired, 2 = dismissed by user, 3 = closed by app, 4 = undefined + switch (reason) + { + case 1: return DismissReason.TimedOut; + case 2: return DismissReason.UserCancelled; + case 3: return DismissReason.ApplicationHidden; + default: return DismissReason.Unknown; + } + } + } +} diff --git a/src/Notify.NET/Platform/Linux/LibNotifyNative.cs b/src/Notify.NET/Platform/Linux/LibNotifyNative.cs new file mode 100644 index 0000000..2565768 --- /dev/null +++ b/src/Notify.NET/Platform/Linux/LibNotifyNative.cs @@ -0,0 +1,172 @@ +using System; +using System.Runtime.InteropServices; + +namespace Notify.NET.Platform.Linux +{ + /// + /// P/Invoke declarations for libnotify.so.4 and the GLib/GObject functions + /// needed to manage signals and the main event loop. + /// + /// All GLib string parameters use ANSI (UTF-8) encoding, which matches GLib's + /// internal string convention on Linux. + /// + internal static class LibNotifyNative + { + private const string LibNotify = "libnotify.so.4"; + private const string LibGLib = "libglib-2.0.so.0"; + private const string LibGObj = "libgobject-2.0.so.0"; + private const string LibGdkPB = "libgdk_pixbuf-2.0.so.0"; + + // ------------------------------------------------------------------------- + // Unmanaged callback delegate types + // ------------------------------------------------------------------------- + + /// Callback fired when the user clicks an action button on the notification. + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + internal delegate void NotifyActionCallback(IntPtr notification, string action, IntPtr userData); + + /// Callback fired when the notification is closed (any reason). + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NotifyClosedCallback(IntPtr notification, IntPtr userData); + + /// Function posted to the GMainContext via g_main_context_invoke. + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate bool GSourceFunc(IntPtr userData); + + // ------------------------------------------------------------------------- + // libnotify + // ------------------------------------------------------------------------- + + /// Initialises libnotify. Must be called before any other notify_ function. + [DllImport(LibNotify, EntryPoint = "notify_init", CharSet = CharSet.Ansi)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool notify_init(string appName); + + /// Returns true if notify_init() has been called successfully. + [DllImport(LibNotify, EntryPoint = "notify_is_initted")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool notify_is_initted(); + + /// Releases all libnotify resources. + [DllImport(LibNotify, EntryPoint = "notify_uninit")] + internal static extern void notify_uninit(); + + /// + /// Creates a new notification object. The returned pointer is a GObject reference + /// with a ref-count of 1. Callers must eventually call g_object_unref. + /// + [DllImport(LibNotify, EntryPoint = "notify_notification_new", CharSet = CharSet.Ansi)] + internal static extern IntPtr notify_notification_new(string summary, string? body, string? icon); + + /// Shows the notification. Returns false and sets on failure. + [DllImport(LibNotify, EntryPoint = "notify_notification_show")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool notify_notification_show(IntPtr notification, ref IntPtr error); + + /// Programmatically closes the notification. + [DllImport(LibNotify, EntryPoint = "notify_notification_close")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool notify_notification_close(IntPtr notification, ref IntPtr error); + + /// + /// Adds an action button to the notification. + /// must be a pinned function pointer; see . + /// + [DllImport(LibNotify, EntryPoint = "notify_notification_add_action", CharSet = CharSet.Ansi)] + internal static extern void notify_notification_add_action( + IntPtr notification, + string action, // machine-readable action ID + string label, // human-readable label + IntPtr callback, // NotifyActionCallback function pointer + IntPtr userData, + IntPtr freeFunc); // GFreeFunc, pass IntPtr.Zero + + /// Sets a display hint on the notification (e.g., urgency level). + [DllImport(LibNotify, EntryPoint = "notify_notification_set_hint", CharSet = CharSet.Ansi)] + internal static extern void notify_notification_set_hint( + IntPtr notification, string key, IntPtr value /* GVariant* */); + + /// Sets the notification's image from a GdkPixbuf. + [DllImport(LibNotify, EntryPoint = "notify_notification_set_image_from_pixbuf")] + internal static extern void notify_notification_set_image_from_pixbuf( + IntPtr notification, IntPtr pixbuf /* GdkPixbuf* */); + + /// Returns the reason the notification was closed (call after the "closed" signal). + [DllImport(LibNotify, EntryPoint = "notify_notification_get_closed_reason")] + internal static extern int notify_notification_get_closed_reason(IntPtr notification); + + // ------------------------------------------------------------------------- + // GLib / GObject + // ------------------------------------------------------------------------- + + /// Creates a new GMainLoop. + [DllImport(LibGLib, EntryPoint = "g_main_loop_new")] + internal static extern IntPtr g_main_loop_new(IntPtr context /* null = default */, bool isRunning); + + /// Runs the GMainLoop, blocking until g_main_loop_quit is called. + [DllImport(LibGLib, EntryPoint = "g_main_loop_run")] + internal static extern void g_main_loop_run(IntPtr loop); + + /// Signals the GMainLoop to stop its run() and return. + [DllImport(LibGLib, EntryPoint = "g_main_loop_quit")] + internal static extern void g_main_loop_quit(IntPtr loop); + + /// Releases a GMainLoop reference. + [DllImport(LibGLib, EntryPoint = "g_main_loop_unref")] + internal static extern void g_main_loop_unref(IntPtr loop); + + /// + /// Posts a function to be called on the default GMainContext from any thread. + /// The function is invoked on the GMainLoop thread. + /// + [DllImport(LibGLib, EntryPoint = "g_main_context_invoke")] + internal static extern void g_main_context_invoke(IntPtr context, IntPtr func, IntPtr userData); + + /// + /// Connects a callback to a GObject signal. + /// Returns the handler ID (used to disconnect later if needed). + /// + [DllImport(LibGObj, EntryPoint = "g_signal_connect_data", CharSet = CharSet.Ansi)] + internal static extern ulong g_signal_connect_data( + IntPtr instance, + string detailedSignal, + IntPtr cHandler, + IntPtr data, + IntPtr destroyData, + int connectFlags); + + /// Releases one reference on a GObject. The object is destroyed when the ref-count reaches 0. + [DllImport(LibGObj, EntryPoint = "g_object_unref")] + internal static extern void g_object_unref(IntPtr obj); + + /// Frees a GError and sets the pointer to null. + [DllImport(LibGLib, EntryPoint = "g_error_free")] + internal static extern void g_error_free(IntPtr error); + + // ------------------------------------------------------------------------- + // GLib GVariant helpers (needed for urgency hints) + // ------------------------------------------------------------------------- + + /// Creates a GVariant holding a byte value (used for the urgency hint). + [DllImport(LibGLib, EntryPoint = "g_variant_new_byte")] + internal static extern IntPtr g_variant_new_byte(byte value); + + // ------------------------------------------------------------------------- + // GdkPixbuf (optional — for loading images from disk paths) + // ------------------------------------------------------------------------- + + /// + /// Loads an image from disk into a GdkPixbuf. + /// Returns IntPtr.Zero on failure; callers should fall back gracefully. + /// + [DllImport(LibGdkPB, EntryPoint = "gdk_pixbuf_new_from_file", CharSet = CharSet.Ansi)] + internal static extern IntPtr gdk_pixbuf_new_from_file(string filename, ref IntPtr error); + + // ------------------------------------------------------------------------- + // Urgency level constants (freedesktop.org spec) + // ------------------------------------------------------------------------- + internal const byte URGENCY_LOW = 0; + internal const byte URGENCY_NORMAL = 1; + internal const byte URGENCY_CRITICAL = 2; + } +} diff --git a/src/Notify.NET/Platform/Linux/LinuxNotificationService.cs b/src/Notify.NET/Platform/Linux/LinuxNotificationService.cs new file mode 100644 index 0000000..331a50a --- /dev/null +++ b/src/Notify.NET/Platform/Linux/LinuxNotificationService.cs @@ -0,0 +1,274 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Notify.NET.Abstractions; +using Notify.NET.Exceptions; + +namespace Notify.NET.Platform.Linux +{ + /// + /// implementation backed by libnotify. + /// + /// Threading model: + /// All libnotify calls must be made from the GLib GMainLoop thread to ensure correct + /// signal wiring. marshals work onto + /// that thread. Callbacks (action-invoked, closed) are delivered on the same thread. + /// + public sealed class LinuxNotificationService : INotificationService + { + private readonly string _appName; + private readonly GLibMainLoopRunner _loopRunner; + private volatile bool _disposed; + + /// + public bool IsSupported { get; private set; } + + /// Application name passed to notify_init(). + public LinuxNotificationService(string appName) + { + _appName = appName ?? throw new ArgumentNullException(nameof(appName)); + _loopRunner = new GLibMainLoopRunner(); + + // Initialise libnotify on the GMainLoop thread. + // Detect a missing libnotify gracefully so IsSupported is false rather than throwing. + try + { + _loopRunner.InvokeAsync(() => + { + if (!LibNotifyNative.notify_is_initted()) + { + bool ok = LibNotifyNative.notify_init(_appName); + IsSupported = ok; + } + else + { + IsSupported = true; + } + }).GetAwaiter().GetResult(); + } + catch (DllNotFoundException) + { + IsSupported = false; + } + } + + // ------------------------------------------------------------------ + // INotificationService + // ------------------------------------------------------------------ + + /// + public async Task ShowAsync(NotificationRequest request, CancellationToken cancellationToken = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + ThrowIfDisposedOrUnsupported(); + + cancellationToken.ThrowIfCancellationRequested(); + + long notificationId = 0; + + await _loopRunner.InvokeAsync(() => + { + notificationId = ShowOnLoopThread(request); + }).ConfigureAwait(false); + + return notificationId; + } + + /// + public async Task HideAsync(long notificationId, CancellationToken cancellationToken = default) + { + ThrowIfDisposedOrUnsupported(); + cancellationToken.ThrowIfCancellationRequested(); + + await _loopRunner.InvokeAsync(() => + { + var ptr = (IntPtr)notificationId; + IntPtr error = IntPtr.Zero; + bool ok = LibNotifyNative.notify_notification_close(ptr, ref error); + + if (!ok) + { + string msg = MarshalGError(ref error); + throw new NotificationException($"notify_notification_close failed: {msg}"); + } + + // Release is normally triggered by the "closed" signal, but call it here + // too in case the signal doesn't fire (some notification daemons omit it). + LibNotifyCallbackBridge.Release(ptr); + }).ConfigureAwait(false); + } + + // ------------------------------------------------------------------ + // IDisposable + // ------------------------------------------------------------------ + + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + try + { + _loopRunner.InvokeAsync(() => + { + if (LibNotifyNative.notify_is_initted()) + LibNotifyNative.notify_uninit(); + }).GetAwaiter().GetResult(); + } + catch { /* best-effort cleanup */ } + + _loopRunner.Dispose(); + } + + // ------------------------------------------------------------------ + // Private helpers + // ------------------------------------------------------------------ + + private void ThrowIfDisposedOrUnsupported() + { + if (_disposed) throw new ObjectDisposedException(nameof(LinuxNotificationService)); + if (!IsSupported) throw new Exceptions.PlatformNotSupportedException(); + } + + /// Called on the GMainLoop thread to create and show a notification. + private long ShowOnLoopThread(NotificationRequest request) + { + IntPtr notification = LibNotifyNative.notify_notification_new( + request.Title, + request.Body, + null /* icon — we set it from imagePath below if provided */); + + if (notification == IntPtr.Zero) + throw new NotificationException("notify_notification_new returned null."); + + // --- Image --- + string? resolvedImage = ResolveImagePath(request.ImagePath); + if (resolvedImage != null) + ApplyImage(notification, resolvedImage); + + // --- Urgency hint --- + byte urgency = MapUrgency(request.Urgency); + IntPtr urgencyVariant = LibNotifyNative.g_variant_new_byte(urgency); + LibNotifyNative.notify_notification_set_hint(notification, "urgency", urgencyVariant); + + // --- Expiration --- + if (request.Expiration.HasValue) + { + int ms = (int)request.Expiration.Value.TotalMilliseconds; + // notify_notification_set_timeout is available in newer libnotify versions; + // set as a hint for compatibility with older versions too. + LibNotifyNative.notify_notification_set_hint( + notification, "x-canonical-snap-decisions-timeout", + LibNotifyNative.g_variant_new_byte((byte)Math.Clamp(ms / 1000, 0, 255))); + } + + // --- Register bridge BEFORE connecting signals --- + LibNotifyCallbackBridge.Register(notification, request.Handler, request.Buttons); + + // --- Action buttons --- + for (int i = 0; i < request.Buttons.Count; i++) + { + var btn = request.Buttons[i]; + LibNotifyNative.notify_notification_add_action( + notification, + btn.ActionId, + btn.Label, + LibNotifyCallbackBridge.PtrAction, + notification, // userData = the notification pointer (our lookup key) + IntPtr.Zero); + } + + // --- "closed" signal --- + LibNotifyNative.g_signal_connect_data( + notification, + "closed", + LibNotifyCallbackBridge.PtrClosed, + notification, // data = lookup key + IntPtr.Zero, + 0); + + // --- Show --- + IntPtr error = IntPtr.Zero; + bool ok = LibNotifyNative.notify_notification_show(notification, ref error); + if (!ok) + { + string msg = MarshalGError(ref error); + LibNotifyCallbackBridge.Release(notification); + throw new NotificationException($"notify_notification_show failed: {msg}"); + } + + return (long)notification; + } + + private static string? ResolveImagePath(string? path) + { + if (string.IsNullOrEmpty(path)) return null; + try + { + string absolute = Path.IsPathRooted(path) ? path : Path.GetFullPath(path); + return File.Exists(absolute) ? absolute : null; + } + catch (Exception) { return null; } + } + + private static void ApplyImage(IntPtr notification, string imagePath) + { + IntPtr pixbufError = IntPtr.Zero; + IntPtr pixbuf = IntPtr.Zero; + + try + { + pixbuf = LibNotifyNative.gdk_pixbuf_new_from_file(imagePath, ref pixbufError); + if (pixbuf != IntPtr.Zero) + { + LibNotifyNative.notify_notification_set_image_from_pixbuf(notification, pixbuf); + } + else + { + // Image loading failed — clear the error and continue without an image. + if (pixbufError != IntPtr.Zero) + { + LibNotifyNative.g_error_free(pixbufError); + pixbufError = IntPtr.Zero; + } + } + } + finally + { + // Unref the pixbuf — the notification holds its own reference after set_image. + if (pixbuf != IntPtr.Zero) + LibNotifyNative.g_object_unref(pixbuf); + if (pixbufError != IntPtr.Zero) + LibNotifyNative.g_error_free(pixbufError); + } + } + + private static string MarshalGError(ref IntPtr error) + { + if (error == IntPtr.Zero) return "(unknown error)"; + + // GError layout: domain (uint32) | code (int32) | message (char*) + // On 64-bit Linux: domain at 0, code at 4, message pointer at 8. + IntPtr messagePtr = Marshal.ReadIntPtr(error, 8); + string message = Marshal.PtrToStringAnsi(messagePtr) ?? "(null)"; + + LibNotifyNative.g_error_free(error); + error = IntPtr.Zero; + return message; + } + + private static byte MapUrgency(NotificationUrgency urgency) + { + switch (urgency) + { + case NotificationUrgency.Low: return LibNotifyNative.URGENCY_LOW; + case NotificationUrgency.Critical: + case NotificationUrgency.Alarm: return LibNotifyNative.URGENCY_CRITICAL; + default: return LibNotifyNative.URGENCY_NORMAL; + } + } + } +} diff --git a/src/Notify.NET/Platform/MacOS/MacNotifyCallbackBridge.cs b/src/Notify.NET/Platform/MacOS/MacNotifyCallbackBridge.cs new file mode 100644 index 0000000..582cfc0 --- /dev/null +++ b/src/Notify.NET/Platform/MacOS/MacNotifyCallbackBridge.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using Notify.NET.Abstractions; + +namespace Notify.NET.Platform.MacOS +{ + /// + /// Bridges unmanaged callbacks from libMacNotifyWrapper.dylib back to the + /// managed for each in-flight notification. + /// + /// Design rules — identical to the Windows and Linux bridges: + /// + /// 1. The four static delegates are stored in static readonly fields and + /// their function pointers obtained once; they are permanently valid. + /// + /// 2. Per-notification state is held in instances + /// tracked in . A prevents GC collection. + /// + /// 3. is called from every terminal callback. + /// On macOS, body-tap, button-tap, dismiss and failure are all terminal: + /// UNUserNotificationCenter fires exactly one response per notification + /// and does NOT separately fire a dismiss event after an action response. + /// + internal sealed class MacNotifyCallbackBridge + { + // ------------------------------------------------------------------ + // Static delegates — one set for the entire process lifetime + // ------------------------------------------------------------------ + private static readonly MacNotifyNative.ActivatedCallback _staticActivated; + private static readonly MacNotifyNative.ButtonActivatedCallback _staticButtonActivated; + private static readonly MacNotifyNative.DismissedCallback _staticDismissed; + private static readonly MacNotifyNative.FailedCallback _staticFailed; + + internal static readonly IntPtr PtrActivated; + internal static readonly IntPtr PtrButtonActivated; + internal static readonly IntPtr PtrDismissed; + internal static readonly IntPtr PtrFailed; + + // ------------------------------------------------------------------ + // Live bridge registry: notifId → bridge + // ------------------------------------------------------------------ + private static readonly ConcurrentDictionary _live + = new ConcurrentDictionary(); + + // ------------------------------------------------------------------ + // Per-instance state + // ------------------------------------------------------------------ + private readonly INotificationHandler? _handler; + private GCHandle _gcHandle; + + static MacNotifyCallbackBridge() + { + _staticActivated = OnActivatedStatic; + _staticButtonActivated = OnButtonActivatedStatic; + _staticDismissed = OnDismissedStatic; + _staticFailed = OnFailedStatic; + + PtrActivated = Marshal.GetFunctionPointerForDelegate(_staticActivated); + PtrButtonActivated = Marshal.GetFunctionPointerForDelegate(_staticButtonActivated); + PtrDismissed = Marshal.GetFunctionPointerForDelegate(_staticDismissed); + PtrFailed = Marshal.GetFunctionPointerForDelegate(_staticFailed); + } + + private MacNotifyCallbackBridge(INotificationHandler? handler) + { + _handler = handler; + _gcHandle = GCHandle.Alloc(this, GCHandleType.Normal); + } + + /// + /// Registers a bridge for . + /// Call immediately after returns + /// a positive ID. + /// + internal static MacNotifyCallbackBridge Register(long notifId, INotificationHandler? handler) + { + var bridge = new MacNotifyCallbackBridge(handler); + _live[notifId] = bridge; + return bridge; + } + + /// + /// Removes the bridge and frees its . + /// Safe to call multiple times; subsequent calls are no-ops. + /// + internal static void Release(long notifId) + { + if (_live.TryRemove(notifId, out var bridge) && bridge._gcHandle.IsAllocated) + bridge._gcHandle.Free(); + } + + // ------------------------------------------------------------------ + // Static routing callbacks — invoked on a background GCD thread + // ------------------------------------------------------------------ + + private static void OnActivatedStatic(long notifId) + { + try + { + if (_live.TryGetValue(notifId, out var bridge)) + bridge._handler?.OnActivated(notifId); + } + catch { /* must not propagate into native code */ } + finally { Release(notifId); } + } + + private static void OnButtonActivatedStatic(long notifId, int buttonIndex) + { + try + { + if (_live.TryGetValue(notifId, out var bridge)) + bridge._handler?.OnButtonActivated(notifId, buttonIndex); + } + catch { } + finally { Release(notifId); } + } + + private static void OnDismissedStatic(long notifId, int reason) + { + try + { + if (_live.TryGetValue(notifId, out var bridge)) + bridge._handler?.OnDismissed(notifId, MapDismissReason(reason)); + } + catch { } + finally { Release(notifId); } + } + + private static void OnFailedStatic(long notifId) + { + try + { + if (_live.TryGetValue(notifId, out var bridge)) + bridge._handler?.OnFailed(notifId); + } + catch { } + finally { Release(notifId); } + } + + private static DismissReason MapDismissReason(int reason) + { + // MNW_DISMISS_EXPIRED = 0, MNW_DISMISS_USER = 1, MNW_DISMISS_APP_REMOVED = 2 + switch (reason) + { + case 0: return DismissReason.TimedOut; + case 1: return DismissReason.UserCancelled; + case 2: return DismissReason.ApplicationHidden; + default: return DismissReason.Unknown; + } + } + } +} diff --git a/src/Notify.NET/Platform/MacOS/MacNotifyNative.cs b/src/Notify.NET/Platform/MacOS/MacNotifyNative.cs new file mode 100644 index 0000000..4308157 --- /dev/null +++ b/src/Notify.NET/Platform/MacOS/MacNotifyNative.cs @@ -0,0 +1,91 @@ +using System; +using System.Runtime.InteropServices; + +namespace Notify.NET.Platform.MacOS +{ + /// + /// P/Invoke declarations for libMacNotifyWrapper.dylib. + /// + /// All strings in structs are marshalled as UTF-8 via and + /// . On macOS the ANSI code page is UTF-8, + /// so this is a faithful UTF-8 round-trip. + /// + /// Every exported function uses the C calling convention (cdecl), which is the + /// platform default for all architectures on macOS. + /// + internal static class MacNotifyNative + { + internal const string LibName = "MacNotifyWrapper"; + + // ------------------------------------------------------------------ + // Unmanaged callback delegate types + // ------------------------------------------------------------------ + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void ActivatedCallback(long notifId); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void ButtonActivatedCallback(long notifId, int buttonIndex); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void DismissedCallback(long notifId, int reason); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void FailedCallback(long notifId); + + // ------------------------------------------------------------------ + // MNW_Handler — bundle of four function pointers + // ------------------------------------------------------------------ + + [StructLayout(LayoutKind.Sequential)] + internal struct MNW_Handler + { + public IntPtr onActivated; + public IntPtr onButtonActivated; + public IntPtr onDismissed; + public IntPtr onFailed; + } + + // ------------------------------------------------------------------ + // MNW_NotificationDescriptor — all string fields are raw pointers + // ------------------------------------------------------------------ + + [StructLayout(LayoutKind.Sequential)] + internal struct MNW_NotificationDescriptor + { + public IntPtr title; // const char* UTF-8, required + public IntPtr body; // const char* UTF-8, may be Zero + public IntPtr imagePath; // const char* UTF-8, may be Zero + public IntPtr buttonLabels; // const char** array, may be Zero + public int buttonCount; + public long expirationMs; // reserved — not used by UNUserNotificationCenter + public int audioOption; + public int interruptionLevel; + } + + // ------------------------------------------------------------------ + // Exported functions + // ------------------------------------------------------------------ + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + internal static extern bool MNW_IsSupported(); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + internal static extern bool MNW_Initialize( + [MarshalAs(UnmanagedType.LPStr)] string appName); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void MNW_Uninitialize(); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern long MNW_ShowNotification( + ref MNW_NotificationDescriptor descriptor, + ref MNW_Handler handler); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + internal static extern bool MNW_HideNotification(long notifId); + } +} diff --git a/src/Notify.NET/Platform/MacOS/MacOSNativeLibraryLoader.cs b/src/Notify.NET/Platform/MacOS/MacOSNativeLibraryLoader.cs new file mode 100644 index 0000000..9d80775 --- /dev/null +++ b/src/Notify.NET/Platform/MacOS/MacOSNativeLibraryLoader.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using Notify.NET.Exceptions; + +namespace Notify.NET.Platform.MacOS +{ + /// + /// Ensures libMacNotifyWrapper.dylib is loaded before the first P/Invoke call. + /// + /// Resolution order: + /// 1. Alongside the executing assembly (typical for published apps). + /// 2. NuGet runtimes/<rid>/native/ layout relative to the executing assembly. + /// 3. NuGet layout relative to the entry assembly. + /// + internal static class MacOSNativeLibraryLoader + { + private const string DylibName = "libMacNotifyWrapper.dylib"; + + private static volatile bool _loaded; + private static readonly object _lock = new object(); + + /// + /// Loads the dylib if it has not been loaded yet. + /// Throws if the file cannot be found or opened. + /// + internal static void EnsureLoaded() + { + if (_loaded) return; + lock (_lock) + { + if (_loaded) return; + LoadDylib(); + _loaded = true; + } + } + + private static void LoadDylib() + { + string rid = GetRuntimeIdentifier(); + string relativeSubPath = Path.Combine("runtimes", rid, "native", DylibName); + + string? assemblyDir = Path.GetDirectoryName( + typeof(MacOSNativeLibraryLoader).Assembly.Location); + + // Search locations in priority order. + string[] candidates = assemblyDir != null + ? new[] + { + Path.Combine(assemblyDir, DylibName), + Path.Combine(assemblyDir, relativeSubPath), + Path.Combine(AppContext.BaseDirectory, DylibName), + Path.Combine(AppContext.BaseDirectory, relativeSubPath), + } + : new[] + { + Path.Combine(AppContext.BaseDirectory, DylibName), + Path.Combine(AppContext.BaseDirectory, relativeSubPath), + }; + + foreach (string candidate in candidates) + { + if (!File.Exists(candidate)) continue; + IntPtr handle = dlopen(candidate, RTLD_NOW | RTLD_GLOBAL); + if (handle != IntPtr.Zero) return; + } + + throw new DllNotFoundException( + $"Could not load {DylibName}. " + + $"Ensure the macOS native dylib is present in the output directory or " + + $"runtimes/{rid}/native/. " + + $"Build with: cd native/MacNotifyWrapper && make install"); + } + + private static string GetRuntimeIdentifier() + { + switch (RuntimeInformation.ProcessArchitecture) + { + case Architecture.X64: return "osx-x64"; + case Architecture.Arm64: return "osx-arm64"; + default: + throw new Exceptions.PlatformNotSupportedException( + $"Unsupported macOS architecture: {RuntimeInformation.ProcessArchitecture}"); + } + } + + // RTLD_NOW=2, RTLD_GLOBAL=8 on macOS. + private const int RTLD_NOW = 2; + private const int RTLD_GLOBAL = 8; + + [DllImport("libSystem.B.dylib")] + private static extern IntPtr dlopen(string path, int mode); + } +} diff --git a/src/Notify.NET/Platform/MacOS/MacOSNotificationService.cs b/src/Notify.NET/Platform/MacOS/MacOSNotificationService.cs new file mode 100644 index 0000000..b79f440 --- /dev/null +++ b/src/Notify.NET/Platform/MacOS/MacOSNotificationService.cs @@ -0,0 +1,302 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Notify.NET.Abstractions; +using Notify.NET.Exceptions; + +namespace Notify.NET.Platform.MacOS +{ + /// + /// implementation backed by macOS + /// UNUserNotificationCenter (macOS 10.14+) via a thin native Objective-C + /// wrapper (libMacNotifyWrapper.dylib). + /// + /// Threading model: + /// UNUserNotificationCenter is internally thread-safe; all P/Invoke calls + /// may be made from any thread. Callbacks arrive on a background GCD thread managed + /// by the framework; consumers are responsible for marshalling to a UI thread if + /// required. + /// + /// macOS-specific behaviour: + /// - A user authorisation prompt is shown on the first call (Alert + Sound + Badge). + /// - Body-tap and button-tap are terminal events; onDismissed is NOT fired + /// after an action response (unlike Windows where WinToastLib always fires it). + /// - Auto-expiry (timed out) does not produce a callback — macOS does not expose + /// this event to UNUserNotificationCenterDelegate. + /// - Non-bundled processes (e.g. bare dotnet CLI) receive the banner but may not + /// receive action callbacks depending on the OS version and app entitlements. + /// + public sealed class MacOSNotificationService : INotificationService + { + private volatile bool _disposed; + + /// + public bool IsSupported { get; private set; } + + /// + /// Application name used for logging. The OS uses the bundle identifier for + /// notification attribution; pass a descriptive name for diagnostic purposes. + /// + public MacOSNotificationService(string appName) + { + if (appName == null) throw new ArgumentNullException(nameof(appName)); + + try + { + MacOSNativeLibraryLoader.EnsureLoaded(); + + if (!MacNotifyNative.MNW_IsSupported()) + { + IsSupported = false; + return; + } + + IsSupported = MacNotifyNative.MNW_Initialize(appName); + } + catch (DllNotFoundException) + { + IsSupported = false; + } + } + + // ------------------------------------------------------------------ + // INotificationService + // ------------------------------------------------------------------ + + /// + public Task ShowAsync(NotificationRequest request, + CancellationToken cancellationToken = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + ThrowIfDisposedOrUnsupported(); + cancellationToken.ThrowIfCancellationRequested(); + + long notifId = ShowNative(request); + return Task.FromResult(notifId); + } + + /// + public Task HideAsync(long notificationId, + CancellationToken cancellationToken = default) + { + ThrowIfDisposedOrUnsupported(); + cancellationToken.ThrowIfCancellationRequested(); + + bool ok = MacNotifyNative.MNW_HideNotification(notificationId); + + // MNW_HideNotification fires onDismissed synchronously via the native + // layer; release the managed bridge entry too. + MacNotifyCallbackBridge.Release(notificationId); + + if (!ok) + throw new NotificationException( + $"MNW_HideNotification failed for id {notificationId}."); + + return Task.CompletedTask; + } + + // ------------------------------------------------------------------ + // IDisposable + // ------------------------------------------------------------------ + + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (IsSupported) + MacNotifyNative.MNW_Uninitialize(); + } + + // ------------------------------------------------------------------ + // Private helpers + // ------------------------------------------------------------------ + + private void ThrowIfDisposedOrUnsupported() + { + if (_disposed) throw new ObjectDisposedException(nameof(MacOSNotificationService)); + if (!IsSupported) throw new Exceptions.PlatformNotSupportedException(); + } + + private static long ShowNative(NotificationRequest request) + { + // Marshal all strings to unmanaged UTF-8 memory for the duration of the call. + // Marshal.StringToHGlobalAnsi uses the system ANSI encoding; on macOS that is UTF-8. + using var titlePin = new PinnedStringAnsi(request.Title); + using var bodyPin = new PinnedStringAnsi(request.Body); + using var imagePin = new PinnedStringAnsi(ResolveImagePath(request.ImagePath)); + + // Build array of pinned button label pointers. + int btnCount = request.Buttons.Count; + var btnPins = new PinnedStringAnsi[btnCount]; + var btnPtrs = new IntPtr[btnCount]; + + for (int i = 0; i < btnCount; i++) + { + btnPins[i] = new PinnedStringAnsi(request.Buttons[i].Label); + btnPtrs[i] = btnPins[i].Pointer; + } + + try + { + // Pin the button pointer array so its address is stable during the call. + GCHandle btnArrayHandle = default; + IntPtr btnArrayPtr = IntPtr.Zero; + + if (btnCount > 0) + { + btnArrayHandle = GCHandle.Alloc(btnPtrs, GCHandleType.Pinned); + btnArrayPtr = btnArrayHandle.AddrOfPinnedObject(); + } + + var descriptor = new MacNotifyNative.MNW_NotificationDescriptor + { + title = titlePin.Pointer, + body = bodyPin.Pointer, + imagePath = imagePin.Pointer, + buttonLabels = btnArrayPtr, + buttonCount = btnCount, + expirationMs = request.Expiration.HasValue + ? (long)request.Expiration.Value.TotalMilliseconds + : 0L, + audioOption = MapAudio(request.Audio), + interruptionLevel = MapInterruptionLevel(request.Urgency) + }; + + var handler = new MacNotifyNative.MNW_Handler + { + onActivated = MacNotifyCallbackBridge.PtrActivated, + onButtonActivated = MacNotifyCallbackBridge.PtrButtonActivated, + onDismissed = MacNotifyCallbackBridge.PtrDismissed, + onFailed = MacNotifyCallbackBridge.PtrFailed + }; + + long notifId = MacNotifyNative.MNW_ShowNotification(ref descriptor, ref handler); + + if (btnArrayHandle.IsAllocated) + btnArrayHandle.Free(); + + if (notifId < 0) + throw new NotificationException( + $"MNW_ShowNotification failed with code {notifId}."); + + // Register managed bridge before any callback can fire. + MacNotifyCallbackBridge.Register(notifId, BuildCompositeHandler(request)); + + return notifId; + } + finally + { + foreach (var pin in btnPins) + pin.Dispose(); + } + } + + private static INotificationHandler? BuildCompositeHandler(NotificationRequest request) + { + bool hasButtonCallbacks = false; + foreach (var btn in request.Buttons) + if (btn.Callback != null) { hasButtonCallbacks = true; break; } + + if (!hasButtonCallbacks) + return request.Handler; + + return new CompositeHandler(request.Handler, request.Buttons); + } + + private static int MapAudio(NotificationAudio audio) + { + return audio == NotificationAudio.Silent + ? 1 /* MNW_AUDIO_SILENT */ + : 0 /* MNW_AUDIO_DEFAULT */; + } + + private static int MapInterruptionLevel(NotificationUrgency urgency) + { + switch (urgency) + { + case NotificationUrgency.Low: return 1; // MNW_INTERRUPTION_PASSIVE + case NotificationUrgency.Critical: + case NotificationUrgency.Alarm: return 3; // MNW_INTERRUPTION_CRITICAL + default: return 0; // MNW_INTERRUPTION_ACTIVE + } + } + + /// + /// Resolves a (possibly relative) image path to an absolute path and verifies the file + /// exists. Returns null if the path is empty, unresolvable, or missing so that the + /// native layer skips the attachment rather than failing the whole notification. + /// + private static string? ResolveImagePath(string? path) + { + if (string.IsNullOrEmpty(path)) return null; + try + { + string absolute = Path.IsPathRooted(path) ? path : Path.GetFullPath(path); + return File.Exists(absolute) ? absolute : null; + } + catch (Exception) { return null; } + } + + // ------------------------------------------------------------------ + // Inner helpers + // ------------------------------------------------------------------ + + /// + /// Copies a .NET string to unmanaged ANSI (UTF-8 on macOS) memory. + /// The allocation is freed on . + /// + private sealed class PinnedStringAnsi : IDisposable + { + public IntPtr Pointer { get; } + + public PinnedStringAnsi(string? value) + { + Pointer = value != null + ? Marshal.StringToHGlobalAnsi(value) + : IntPtr.Zero; + } + + public void Dispose() + { + if (Pointer != IntPtr.Zero) + Marshal.FreeHGlobal(Pointer); + } + } + + /// + /// Combines a top-level with per-button callbacks + /// stored in . + /// + private sealed class CompositeHandler : INotificationHandler + { + private readonly INotificationHandler? _inner; + private readonly System.Collections.Generic.IReadOnlyList _buttons; + + public CompositeHandler( + INotificationHandler? inner, + System.Collections.Generic.IReadOnlyList buttons) + { + _inner = inner; + _buttons = buttons; + } + + public void OnActivated(long id) => _inner?.OnActivated(id); + + public void OnButtonActivated(long id, int index) + { + if (index >= 0 && index < _buttons.Count) + _buttons[index].Callback?.Invoke(id); + _inner?.OnButtonActivated(id, index); + } + + public void OnDismissed(long id, DismissReason reason) => + _inner?.OnDismissed(id, reason); + + public void OnFailed(long id) => _inner?.OnFailed(id); + } + } +} diff --git a/src/Notify.NET/Platform/Windows/NativeLibraryLoader.cs b/src/Notify.NET/Platform/Windows/NativeLibraryLoader.cs new file mode 100644 index 0000000..faac169 --- /dev/null +++ b/src/Notify.NET/Platform/Windows/NativeLibraryLoader.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Notify.NET.Platform.Windows +{ + /// + /// Loads WinToastWrapper.dll from the correct runtime-identifier sub-folder before + /// the first P/Invoke call is made. This ensures the x64/x86/arm64 variant that + /// matches the current process architecture is used. + /// + /// Once LoadLibraryW succeeds, subsequent DllImport("WinToastWrapper") resolutions + /// find the already-loaded module in the process module list automatically. + /// + internal static class NativeLibraryLoader + { + private static volatile bool _loaded; + private static readonly object _lock = new object(); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr LoadLibraryW(string lpLibFileName); + + internal static void EnsureLoaded() + { + if (_loaded) return; + + lock (_lock) + { + if (_loaded) return; + + string rid = GetRuntimeIdentifier(); + string dllPath = ResolveNativePath(rid); + + IntPtr handle = LoadLibraryW(dllPath); + if (handle == IntPtr.Zero) + { + int err = Marshal.GetLastWin32Error(); + throw new DllNotFoundException( + $"Failed to load WinToastWrapper.dll from '{dllPath}' (Win32 error {err}). " + + "Ensure the native DLL for your platform architecture is present in the " + + $"runtimes/{rid}/native/ directory relative to the assembly."); + } + + _loaded = true; + } + } + + private static string GetRuntimeIdentifier() + { + switch (RuntimeInformation.ProcessArchitecture) + { + case Architecture.X64: return "win-x64"; + case Architecture.X86: return "win-x86"; + case Architecture.Arm64: return "win-arm64"; + default: + throw new PlatformNotSupportedException( + $"No WinToastWrapper.dll is available for architecture {RuntimeInformation.ProcessArchitecture}."); + } + } + + private static string ResolveNativePath(string rid) + { + // Search order: + // 1. Alongside the executing assembly (output directory, typical for app projects) + // 2. Relative to the assembly's location using the NuGet runtimes layout + string assemblyDir = Path.GetDirectoryName( + new Uri(typeof(NativeLibraryLoader).Assembly.CodeBase!).LocalPath)!; + + // Typical publish output: /WinToastWrapper.dll (copied by MSBuild) + string flat = Path.Combine(assemblyDir, "WinToastWrapper.dll"); + if (File.Exists(flat)) return flat; + + // NuGet runtimes layout: /runtimes//native/WinToastWrapper.dll + string runtimePath = Path.Combine(assemblyDir, "runtimes", rid, "native", "WinToastWrapper.dll"); + if (File.Exists(runtimePath)) return runtimePath; + + // Fallback: relative to the entry assembly location + string? entryDir = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); + if (entryDir != null) + { + string entryRuntime = Path.Combine(entryDir, "runtimes", rid, "native", "WinToastWrapper.dll"); + if (File.Exists(entryRuntime)) return entryRuntime; + + string entryFlat = Path.Combine(entryDir, "WinToastWrapper.dll"); + if (File.Exists(entryFlat)) return entryFlat; + } + + // Return the NuGet path even if not found — LoadLibraryW will fail with a useful error + return Path.Combine(assemblyDir, "runtimes", rid, "native", "WinToastWrapper.dll"); + } + } +} diff --git a/src/Notify.NET/Platform/Windows/WinToastHandlerBridge.cs b/src/Notify.NET/Platform/Windows/WinToastHandlerBridge.cs new file mode 100644 index 0000000..deeba81 --- /dev/null +++ b/src/Notify.NET/Platform/Windows/WinToastHandlerBridge.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using Notify.NET.Abstractions; + +namespace Notify.NET.Platform.Windows +{ + /// + /// Bridges the unmanaged WinToastWrapper callbacks back to the managed + /// for each in-flight toast. + /// + /// Design rules that MUST be maintained to avoid memory-safety bugs: + /// + /// 1. The four static delegates ( etc.) are kept alive + /// for the process lifetime because they are stored in static fields. Their function + /// pointers are therefore permanently valid for unmanaged code to call. + /// + /// 2. Per-notification state is held in instances + /// tracked in the static dictionary. Each instance's GCHandle + /// prevents the GC from collecting it while the toast is alive. + /// + /// 3. Routing works by passing the toast ID (long) back through the static callbacks, + /// which look up the matching bridge instance in . + /// + /// 4. is called exactly once, from whichever callback fires last + /// (dismissed or failed). It removes the entry and frees the GCHandle. + /// + internal sealed class WinToastHandlerBridge + { + // ------------------------------------------------------------------ + // Static callback function pointers — allocated once, never collected + // ------------------------------------------------------------------ + private static readonly WinToastNative.ActivatedCallback _staticActivated; + private static readonly WinToastNative.ButtonActivatedCallback _staticButtonActivated; + private static readonly WinToastNative.DismissedCallback _staticDismissed; + private static readonly WinToastNative.FailedCallback _staticFailed; + + // Pointer-sized function pointers stored in the WNT_Handler struct + internal static readonly IntPtr PtrActivated; + internal static readonly IntPtr PtrButtonActivated; + internal static readonly IntPtr PtrDismissed; + internal static readonly IntPtr PtrFailed; + + // ------------------------------------------------------------------ + // Static dictionary: toastId → live bridge instance + // ------------------------------------------------------------------ + private static readonly ConcurrentDictionary _live + = new ConcurrentDictionary(); + + // ------------------------------------------------------------------ + // Per-instance state + // ------------------------------------------------------------------ + private readonly INotificationHandler? _handler; + private GCHandle _gcHandle; // keeps this bridge alive from unmanaged side + + static WinToastHandlerBridge() + { + // Create static delegates and pin their function pointers permanently. + _staticActivated = OnActivatedStatic; + _staticButtonActivated = OnButtonActivatedStatic; + _staticDismissed = OnDismissedStatic; + _staticFailed = OnFailedStatic; + + PtrActivated = Marshal.GetFunctionPointerForDelegate(_staticActivated); + PtrButtonActivated = Marshal.GetFunctionPointerForDelegate(_staticButtonActivated); + PtrDismissed = Marshal.GetFunctionPointerForDelegate(_staticDismissed); + PtrFailed = Marshal.GetFunctionPointerForDelegate(_staticFailed); + } + + private WinToastHandlerBridge(INotificationHandler? handler) + { + _handler = handler; + // Allocate a GCHandle so the GC cannot collect this instance. + _gcHandle = GCHandle.Alloc(this, GCHandleType.Normal); + } + + /// + /// Creates a bridge and registers it under . + /// Call this immediately after returns a positive ID. + /// + internal static WinToastHandlerBridge Register(long toastId, INotificationHandler? handler) + { + var bridge = new WinToastHandlerBridge(handler); + _live[toastId] = bridge; + return bridge; + } + + /// + /// Removes the bridge for and releases its GCHandle. + /// Safe to call multiple times; subsequent calls are no-ops. + /// + internal static void Release(long toastId) + { + if (_live.TryRemove(toastId, out var bridge) && bridge._gcHandle.IsAllocated) + bridge._gcHandle.Free(); + } + + // ------------------------------------------------------------------ + // Static routing callbacks — invoked by unmanaged code on a WinRT thread + // ------------------------------------------------------------------ + + private static void OnActivatedStatic(long toastId) + { + // Must not let exceptions escape to native code — unhandled exceptions + // on WinRT callback threads crash the process with no useful error. + try + { + if (_live.TryGetValue(toastId, out var bridge)) + bridge._handler?.OnActivated(toastId); + } + catch { /* swallow — caller cannot handle managed exceptions */ } + } + + private static void OnButtonActivatedStatic(long toastId, int buttonIndex) + { + try + { + if (_live.TryGetValue(toastId, out var bridge)) + bridge._handler?.OnButtonActivated(toastId, buttonIndex); + } + catch { } + } + + private static void OnDismissedStatic(long toastId, int reason) + { + try + { + if (_live.TryGetValue(toastId, out var bridge)) + bridge._handler?.OnDismissed(toastId, MapDismissReason(reason)); + } + catch { } + finally { Release(toastId); } + } + + private static void OnFailedStatic(long toastId) + { + try + { + if (_live.TryGetValue(toastId, out var bridge)) + bridge._handler?.OnFailed(toastId); + } + catch { } + finally { Release(toastId); } + } + + private static DismissReason MapDismissReason(int native) + { + // WinToastDismissalReason: 0=UserCancelled, 1=ApplicationHidden, 2=TimedOut + switch (native) + { + case 0: return DismissReason.UserCancelled; + case 1: return DismissReason.ApplicationHidden; + case 2: return DismissReason.TimedOut; + default: return DismissReason.Unknown; + } + } + } +} diff --git a/src/Notify.NET/Platform/Windows/WinToastNative.cs b/src/Notify.NET/Platform/Windows/WinToastNative.cs new file mode 100644 index 0000000..e78be98 --- /dev/null +++ b/src/Notify.NET/Platform/Windows/WinToastNative.cs @@ -0,0 +1,120 @@ +using System; +using System.Runtime.InteropServices; + +namespace Notify.NET.Platform.Windows +{ + /// + /// P/Invoke declarations for WinToastWrapper.dll — the native C DLL that wraps WinToastLib. + /// All strings are UTF-16 (Unicode) to match the wchar_t* ABI of the wrapper. + /// The DLL must be loaded via before these are called. + /// + internal static class WinToastNative + { + private const string DllName = "WinToastWrapper"; + + // ------------------------------------------------------------------------- + // Unmanaged callback delegate types. + // IMPORTANT: These must be static fields — never pass instance delegates as + // unmanaged function pointers. The GC does not see unmanaged references and + // will collect instance delegates, producing an AccessViolationException. + // ------------------------------------------------------------------------- + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void ActivatedCallback(long toastId); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void ButtonActivatedCallback(long toastId, int buttonIndex); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void DismissedCallback(long toastId, int reason); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void FailedCallback(long toastId); + + // ------------------------------------------------------------------------- + // Structs matching the C ABI of WinToastWrapper.h + // ------------------------------------------------------------------------- + + /// + /// Plain-data descriptor passed to . + /// String fields are pointers into pinned managed memory — callers must + /// keep the pinned handles alive for the duration of the call. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct WNT_ToastDescriptor + { + public IntPtr title; // wchar_t* + public IntPtr body; // wchar_t* (may be IntPtr.Zero) + public IntPtr imagePath; // wchar_t* (may be IntPtr.Zero) + public IntPtr buttonLabels; // wchar_t** (array of pointers, may be IntPtr.Zero) + public int buttonCount; + public long expirationMs; // 0 = platform default + public int scenario; // WNT_Scenario enum value + public int audioOption; // WNT_AudioOption enum value + } + + /// + /// Struct of four function pointers passed to . + /// Must be pinned for the lifetime of the toast (until dismissed or failed). + /// + [StructLayout(LayoutKind.Sequential)] + internal struct WNT_Handler + { + public IntPtr onActivated; // ActivatedCallback + public IntPtr onButtonActivated; // ButtonActivatedCallback + public IntPtr onDismissed; // DismissedCallback + public IntPtr onFailed; // FailedCallback + } + + // WNT_Scenario values (must match enum in WinToastWrapper.h) + internal const int WNT_SCENARIO_DEFAULT = 0; + internal const int WNT_SCENARIO_ALARM = 1; + internal const int WNT_SCENARIO_REMINDER = 2; + internal const int WNT_SCENARIO_INCOMING_CALL = 3; + + // WNT_AudioOption values (must match enum in WinToastWrapper.h) + internal const int WNT_AUDIO_DEFAULT = 0; + internal const int WNT_AUDIO_SILENT = 1; + internal const int WNT_AUDIO_LOOP = 2; + + // ------------------------------------------------------------------------- + // Exported functions + // ------------------------------------------------------------------------- + + /// + /// Initialises WinToastLib. Must be called once from an STA thread before any other function. + /// + /// Human-readable application name shown in the Action Centre. + /// + /// The AppUserModelId (AUMI) — must match the shortcut in the Start Menu. + /// The wrapper creates the shortcut automatically if it doesn't exist. + /// + /// true on success. + [DllImport(DllName, EntryPoint = "WNT_Initialize", CharSet = CharSet.Unicode, SetLastError = false)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool WNT_Initialize(string appName, string appUserModelId); + + /// Uninitialises WinToastLib and releases all internal resources. + [DllImport(DllName, EntryPoint = "WNT_Uninitialize")] + internal static extern void WNT_Uninitialize(); + + /// Returns true if WinToast is supported on this version of Windows (requires Win 8+). + [DllImport(DllName, EntryPoint = "WNT_IsCompatible")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool WNT_IsCompatible(); + + /// + /// Shows a toast notification. Must be called from the STA thread. + /// + /// Pointer to a with notification data. + /// Pointer to a with callback function pointers. + /// A positive toast ID on success, or a negative error code on failure. + [DllImport(DllName, EntryPoint = "WNT_ShowToast")] + internal static extern long WNT_ShowToast(ref WNT_ToastDescriptor descriptor, ref WNT_Handler handler); + + /// Programmatically dismisses a previously shown toast. Must be called from the STA thread. + [DllImport(DllName, EntryPoint = "WNT_HideToast")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool WNT_HideToast(long toastId); + } +} diff --git a/src/Notify.NET/Platform/Windows/WindowsNotificationService.cs b/src/Notify.NET/Platform/Windows/WindowsNotificationService.cs new file mode 100644 index 0000000..a3e3a5d --- /dev/null +++ b/src/Notify.NET/Platform/Windows/WindowsNotificationService.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Notify.NET.Abstractions; +using Notify.NET.Exceptions; + +namespace Notify.NET.Platform.Windows +{ + /// + /// implementation backed by WinToastLib via a thin + /// native C wrapper DLL (WinToastWrapper.dll). + /// + /// Threading model: + /// WinRT toast APIs require a Single-Threaded Apartment (STA). This service owns a + /// dedicated STA background thread that runs a Win32 message pump. All P/Invoke calls + /// are marshalled onto that thread via a work-item queue. Callbacks from WinToast arrive + /// on a WinRT thread-pool thread (NOT the STA thread) and are safe to dispatch directly. + /// + public sealed class WindowsNotificationService : INotificationService + { + private readonly string _appName; + private readonly string _appUserModelId; + + private readonly Thread _staThread; + private readonly BlockingCollection _workQueue = new BlockingCollection(); + private readonly ManualResetEventSlim _initialised = new ManualResetEventSlim(false); + private volatile bool _isSupported; + private volatile bool _disposed; + private Exception? _initException; + + /// + public bool IsSupported => _isSupported; + + /// Human-readable application name (shown in Action Centre). + /// + /// Your application's AppUserModelId, e.g. "MyCompany.MyApp". + /// A Start-Menu shortcut carrying this AUMI is required for notifications to persist in + /// the Action Centre. The native wrapper creates the shortcut automatically when missing. + /// + public WindowsNotificationService(string appName, string appUserModelId) + { + _appName = appName ?? throw new ArgumentNullException(nameof(appName)); + _appUserModelId = appUserModelId ?? throw new ArgumentNullException(nameof(appUserModelId)); + + _staThread = new Thread(StaThreadProc) + { + Name = "Notify.NET STA", + IsBackground = true + }; + _staThread.SetApartmentState(ApartmentState.STA); + _staThread.Start(); + + // Block until the STA thread has finished initialising (or failed). + _initialised.Wait(); + if (_initException != null) + throw new NotificationException("WinToastLib initialisation failed.", _initException); + } + + // ------------------------------------------------------------------ + // INotificationService + // ------------------------------------------------------------------ + + /// + public Task ShowAsync(NotificationRequest request, CancellationToken cancellationToken = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + ThrowIfDisposedOrUnsupported(); + + var tcs = new TaskCompletionSource(); + cancellationToken.Register(() => tcs.TrySetCanceled()); + + EnqueueOnSta(() => + { + try + { + long id = ShowOnSta(request); + tcs.TrySetResult(id); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }); + + return tcs.Task; + } + + /// + public Task HideAsync(long notificationId, CancellationToken cancellationToken = default) + { + ThrowIfDisposedOrUnsupported(); + + var tcs = new TaskCompletionSource(); + cancellationToken.Register(() => tcs.TrySetCanceled()); + + EnqueueOnSta(() => + { + try + { + bool ok = WinToastNative.WNT_HideToast(notificationId); + if (!ok) + tcs.TrySetException(new NotificationException($"WNT_HideToast failed for id {notificationId}.")); + else + { + WinToastHandlerBridge.Release(notificationId); + tcs.TrySetResult(true); + } + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }); + + return tcs.Task; + } + + // ------------------------------------------------------------------ + // IDisposable + // ------------------------------------------------------------------ + + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + // Signal the STA thread to shut down by completing the queue. + _workQueue.CompleteAdding(); + + // Wait for the STA thread to finish its message pump and uninitialise. + if (_staThread.IsAlive) + _staThread.Join(TimeSpan.FromSeconds(5)); + + _workQueue.Dispose(); + _initialised.Dispose(); + } + + // ------------------------------------------------------------------ + // Private helpers + // ------------------------------------------------------------------ + + private void ThrowIfDisposedOrUnsupported() + { + if (_disposed) throw new ObjectDisposedException(nameof(WindowsNotificationService)); + if (!_isSupported) throw new Exceptions.PlatformNotSupportedException(); + } + + private void EnqueueOnSta(Action action) + { + try { _workQueue.Add(action); } + catch (InvalidOperationException) { /* queue completed — service is disposed */ } + } + + /// + /// The STA thread entry point. Runs a simple work-item loop as the message pump. + /// + private void StaThreadProc() + { + try + { + NativeLibraryLoader.EnsureLoaded(); + + if (!WinToastNative.WNT_IsCompatible()) + { + _isSupported = false; + _initialised.Set(); + return; + } + + bool ok = WinToastNative.WNT_Initialize(_appName, _appUserModelId); + if (!ok) + { + _initException = new NotificationException("WNT_Initialize returned false."); + _isSupported = false; + _initialised.Set(); + return; + } + + _isSupported = true; + _initialised.Set(); + + // Process work items until Dispose() calls CompleteAdding(). + foreach (Action work in _workQueue.GetConsumingEnumerable()) + { + // Pump pending Windows messages between work items so WinRT callbacks + // can be delivered to the STA message queue. + PumpMessages(); + work(); + PumpMessages(); + } + } + catch (Exception ex) + { + _initException = ex; + _isSupported = false; + _initialised.Set(); + } + finally + { + // Drain any remaining messages before uninitialising. + PumpMessages(); + if (_isSupported) + WinToastNative.WNT_Uninitialize(); + } + } + + private long ShowOnSta(NotificationRequest request) + { + // We need to pass wchar_t* pointers to the native layer. + // Pin managed strings as unmanaged UTF-16 memory for the duration of the call. + // button label pointers are pinned in the IntPtr[] and that array is pinned too. + + using var titlePin = new PinnedString(request.Title); + using var bodyPin = new PinnedString(request.Body); + using var imagePin = new PinnedString(ResolveImagePath(request.ImagePath)); + + // Build array of pinned button label pointers. + var buttonPins = new PinnedString[request.Buttons.Count]; + var buttonPtrs = new IntPtr[request.Buttons.Count]; + for (int i = 0; i < request.Buttons.Count; i++) + { + buttonPins[i] = new PinnedString(request.Buttons[i].Label); + buttonPtrs[i] = buttonPins[i].Pointer; + } + + try + { + // Pin the button pointer array itself. + GCHandle buttonArrayHandle = default; + IntPtr buttonArrayPtr = IntPtr.Zero; + + if (buttonPtrs.Length > 0) + { + buttonArrayHandle = GCHandle.Alloc(buttonPtrs, GCHandleType.Pinned); + buttonArrayPtr = buttonArrayHandle.AddrOfPinnedObject(); + } + + var descriptor = new WinToastNative.WNT_ToastDescriptor + { + title = titlePin.Pointer, + body = bodyPin.Pointer, + imagePath = imagePin.Pointer, + buttonLabels = buttonArrayPtr, + buttonCount = request.Buttons.Count, + expirationMs = request.Expiration.HasValue ? (long)request.Expiration.Value.TotalMilliseconds : 0L, + scenario = MapScenario(request.Urgency), + audioOption = MapAudio(request.Audio) + }; + + var handler = new WinToastNative.WNT_Handler + { + onActivated = WinToastHandlerBridge.PtrActivated, + onButtonActivated = WinToastHandlerBridge.PtrButtonActivated, + onDismissed = WinToastHandlerBridge.PtrDismissed, + onFailed = WinToastHandlerBridge.PtrFailed + }; + + long toastId = WinToastNative.WNT_ShowToast(ref descriptor, ref handler); + + if (buttonArrayHandle.IsAllocated) + buttonArrayHandle.Free(); + + if (toastId < 0) + throw new NotificationException($"WNT_ShowToast failed with error code {toastId}.", (int)toastId); + + // Register the per-notification bridge BEFORE any callback can fire. + WinToastHandlerBridge.Register(toastId, BuildCompositeHandler(request)); + + return toastId; + } + finally + { + foreach (var pin in buttonPins) + pin.Dispose(); + } + } + + /// + /// Builds an that combines the request-level handler + /// with per-button callbacks defined on each . + /// + private static INotificationHandler? BuildCompositeHandler(NotificationRequest request) + { + bool hasButtonCallbacks = false; + foreach (var btn in request.Buttons) + if (btn.Callback != null) { hasButtonCallbacks = true; break; } + + if (!hasButtonCallbacks) + return request.Handler; + + return new CompositeHandler(request.Handler, request.Buttons); + } + + private static int MapScenario(NotificationUrgency urgency) + { + switch (urgency) + { + case NotificationUrgency.Alarm: return WinToastNative.WNT_SCENARIO_ALARM; + case NotificationUrgency.Reminder: return WinToastNative.WNT_SCENARIO_REMINDER; + default: return WinToastNative.WNT_SCENARIO_DEFAULT; + } + } + + private static int MapAudio(NotificationAudio audio) + { + switch (audio) + { + case NotificationAudio.Silent: return WinToastNative.WNT_AUDIO_SILENT; + case NotificationAudio.Loop: return WinToastNative.WNT_AUDIO_LOOP; + default: return WinToastNative.WNT_AUDIO_DEFAULT; + } + } + + /// Pumps pending Win32/WinRT messages on the STA thread. + private static void PumpMessages() + { + NativeMessage msg; + while (PeekMessageW(out msg, IntPtr.Zero, 0, 0, 0x0001 /* PM_REMOVE */)) + { + TranslateMessage(ref msg); + DispatchMessageW(ref msg); + } + } + + [DllImport("user32.dll")] private static extern bool PeekMessageW(out NativeMessage msg, IntPtr hwnd, uint min, uint max, uint remove); + [DllImport("user32.dll")] private static extern bool TranslateMessage(ref NativeMessage msg); + [DllImport("user32.dll")] private static extern IntPtr DispatchMessageW(ref NativeMessage msg); + + [StructLayout(LayoutKind.Sequential)] + private struct NativeMessage + { + public IntPtr hwnd; + public uint msg; + public IntPtr wParam; + public IntPtr lParam; + public uint time; + public int ptX; + public int ptY; + } + + // ------------------------------------------------------------------ + // Inner helpers + // ------------------------------------------------------------------ + + /// + /// Resolves a (possibly relative) image path to an absolute path and verifies + /// the file exists. Returns null if the path is empty, unresolvable, or the + /// file is not found — causing the native layer to skip the image rather than crash. + /// WinToastLib embeds the path verbatim into toast XML; it cannot handle + /// relative paths or missing files gracefully. + /// + private static string? ResolveImagePath(string? path) + { + if (string.IsNullOrEmpty(path)) return null; + try + { + string absolute = Path.IsPathRooted(path) ? path : Path.GetFullPath(path); + return File.Exists(absolute) ? absolute : null; + } + catch (Exception) { return null; } + } + + /// + /// Copies a .NET string into unmanaged UTF-16 memory so it can be passed as + /// a wchar_t* to native code. The memory is freed on Dispose. + /// + private sealed class PinnedString : IDisposable + { + public IntPtr Pointer { get; } + + public PinnedString(string? value) + { + Pointer = value != null + ? Marshal.StringToHGlobalUni(value) + : IntPtr.Zero; + } + + public void Dispose() + { + if (Pointer != IntPtr.Zero) + Marshal.FreeHGlobal(Pointer); + } + } + + /// + /// Combines a top-level with per-button callbacks. + /// + private sealed class CompositeHandler : INotificationHandler + { + private readonly INotificationHandler? _inner; + private readonly System.Collections.Generic.IReadOnlyList _buttons; + + public CompositeHandler(INotificationHandler? inner, + System.Collections.Generic.IReadOnlyList buttons) + { + _inner = inner; + _buttons = buttons; + } + + public void OnActivated(long id) => _inner?.OnActivated(id); + + public void OnButtonActivated(long id, int index) + { + if (index >= 0 && index < _buttons.Count) + _buttons[index].Callback?.Invoke(id); + _inner?.OnButtonActivated(id, index); + } + + public void OnDismissed(long id, DismissReason reason) => _inner?.OnDismissed(id, reason); + public void OnFailed(long id) => _inner?.OnFailed(id); + } + } +}