| /* |
| Simple DirectMedia Layer |
| Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org> |
| |
| This software is provided 'as-is', without any express or implied |
| warranty. In no event will the authors be held liable for any damages |
| arising from the use of this software. |
| |
| Permission is granted to anyone to use this software for any purpose, |
| including commercial applications, and to alter it and redistribute it |
| freely, subject to the following restrictions: |
| |
| 1. The origin of this software must not be misrepresented; you must not |
| claim that you wrote the original software. If you use this software |
| in a product, an acknowledgment in the product documentation would be |
| appreciated but is not required. |
| 2. Altered source versions must be plainly marked as such, and must not be |
| misrepresented as being the original software. |
| 3. This notice may not be removed or altered from any source distribution. |
| */ |
| |
| #include "SDL_internal.h" |
| |
| #include "../../events/SDL_notificationevents_c.h" |
| #include "notification/SDL_notification_c.h" |
| #include "video/SDL_surface_c.h" |
| |
| #include "../../core/windows/SDL_windows.h" |
| #define COBJMACROS |
| #include <Windows.ui.notifications.h> |
| #include <bcrypt.h> |
| #include <initguid.h> |
| #include <roapi.h> |
| |
| typedef HRESULT(WINAPI *RoGetActivationFactory_t)(HSTRING activatableClassId, REFIID iid, void **factory); |
| typedef HRESULT(WINAPI *RoActivateInstance_t)(HSTRING activatableClassId, IInspectable **instance); |
| typedef HRESULT(WINAPI *WindowsCreateString_t)(PCWSTR sourceString, UINT32 length, HSTRING *string); |
| typedef HRESULT(WINAPI *WindowsCreateStringReference_t)(PCWSTR sourceString, UINT32 length, HSTRING_HEADER *hstringHeader, HSTRING *string); |
| typedef HRESULT(WINAPI *WindowsDeleteString_t)(HSTRING string); |
| typedef PCWSTR(WINAPI *WindowsGetStringRawBuffer_t)(HSTRING string, UINT32 *length); |
| typedef NTSTATUS(WINAPI *BCryptGenRandom_t)(BCRYPT_ALG_HANDLE hAlgorithm, PUCHAR pbBuffer, ULONG cbBuffer, ULONG dwFlags); |
| typedef LONG(WINAPI *GetCurrentPackageFullName_t)(UINT32 *packageFullNameLength, PWSTR packageFullName); |
| |
| static RoGetActivationFactory_t WIN_RoGetActivationFactory; |
| static RoActivateInstance_t WIN_RoActivateInstance; |
| static WindowsCreateString_t WIN_WindowsCreateString; |
| static WindowsCreateStringReference_t WIN_WindowsCreateStringReference; |
| static WindowsDeleteString_t WIN_WindowsDeleteString; |
| static WindowsGetStringRawBuffer_t WIN_WindowsGetStringRawBuffer; |
| static GetCurrentPackageFullName_t WIN_GetCurrentPackageFullName; |
| |
| // The registry key base needed to register the app instance so notifications can be sent. |
| #define REG_KEY_BASE L"SOFTWARE\\Classes\\AppUserModelId\\" |
| #define GROUP_ID_STR L"SDL_LocalNotification" |
| |
| // IIDs for the interfaces. |
| DEFINE_GUID(IID_IToastNotificationManagerStatics, |
| 0x50ac103f, 0xd235, 0x4598, 0xbb, 0xef, 0x98, 0xfe, 0x4d, 0x1a, 0x3a, 0xd4); |
| |
| DEFINE_GUID(IID_IToastNotificationManagerStatics2, |
| 0x7ab93c52, 0x0e48, 0x4750, 0xba, 0x9d, 0x1a, 0x41, 0x13, 0x98, 0x18, 0x47); |
| |
| DEFINE_GUID(IID_IToastNotificationFactory, |
| 0x04124b20, 0x82c6, 0x4229, 0xb1, 0x09, 0xfd, 0x9e, 0xd4, 0x66, 0x2b, 0x53); |
| |
| DEFINE_GUID(IID_IToastNotification2, |
| 0x9dfb9fd1, 0x143a, 0x490e, 0x90, 0xbf, 0xb9, 0xfb, 0xa7, 0x13, 0x2d, 0xe7); |
| |
| DEFINE_GUID(IID_IToastNotification4, |
| 0x15154935, 0x28ea, 0x4727, 0x88, 0xe9, 0xc5, 0x86, 0x80, 0xe2, 0xd1, 0x18); |
| |
| DEFINE_GUID(IID_INotificationActivationCallback, |
| 0x53e31837, 0x6600, 0x4a81, 0x93, 0x95, 0x75, 0xcf, 0xfe, 0x74, 0x6f, 0x94); |
| |
| DEFINE_GUID(IID_IXmlDocument, |
| 0xf7f3a506, 0x1e87, 0x42d6, 0xbc, 0xfb, 0xb8, 0xc8, 0x09, 0xfa, 0x54, 0x94); |
| |
| DEFINE_GUID(IID_IXmlDocumentIO, |
| 0x6cd0e74e, 0xee65, 0x4489, 0x9e, 0xbf, 0xca, 0x43, 0xe8, 0x7b, 0xa6, 0x37); |
| |
| DEFINE_GUID(IID_IToastActivatedEventArgs, |
| 0xe3bf92f3, 0xc197, 0x436f, 0x82, 0x65, 0x06, 0x25, 0x82, 0x4f, 0x8d, 0xac); |
| |
| DEFINE_GUID(IID_IToastActivatedEventHandler, |
| 0xab54de2d, 0x97d9, 0x5528, 0xb6, 0xad, 0x10, 0x5a, 0xfe, 0x15, 0x65, 0x30); |
| |
| DEFINE_GUID(IID_IToastDismissedEventHandler, |
| 0x61c2402f, 0x0ed0, 0x5a18, 0xab, 0x69, 0x59, 0xf4, 0xaa, 0x99, 0xa3, 0x68); |
| |
| static HSTRING hsGroupId = NULL; |
| static HSTRING hsAppId = NULL; |
| |
| static __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationManagerStatics *pToastNotificationManager = NULL; |
| static __x_ABI_CWindows_CUI_CNotifications_CIToastNotifier *pToastNotifier = NULL; |
| static __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationFactory *pNotificationFactory = NULL; |
| static __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationManagerStatics2 *pToastNotificationManagerStatics2 = NULL; |
| static __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationHistory *pToastNotificationHistory = NULL; |
| |
| static WCHAR *app_reg_key = NULL; |
| static WCHAR *app_icon_path = NULL; |
| |
| static enum |
| { |
| WIN32_NOTIFICATIONS_INITIALIZATION_FAILED = -1, |
| WIN32_NOTIFICATIONS_UNINITIALIZED = 0, |
| WIN32_NOTIFICATIONS_INITIALIZED = 1 |
| } initialization_status = WIN32_NOTIFICATIONS_UNINITIALIZED; |
| static bool ro_initialized = false; |
| static bool co_initialized = false; |
| |
| // IUnknown implementation |
| typedef struct Impl_IGeneric |
| { |
| IUnknownVtbl *lpVtbl; |
| SDL_AtomicInt refCount; |
| } Impl_IGeneric; |
| |
| static ULONG STDMETHODCALLTYPE Impl_IGeneric_AddRef(Impl_IGeneric *_this) |
| { |
| return (ULONG)SDL_AddAtomicInt(&_this->refCount, 1) + 1UL; |
| } |
| |
| static ULONG STDMETHODCALLTYPE Impl_IGeneric_Release(Impl_IGeneric *_this) |
| { |
| return (ULONG)SDL_AddAtomicInt(&_this->refCount, -1) - 1UL; |
| } |
| |
| // OnActivated interface |
| static HRESULT STDMETHODCALLTYPE Impl_OnActivated_QueryInterface(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable *_this, REFIID riid, void **ppvObject) |
| { |
| if (ppvObject == NULL) { |
| return E_POINTER; |
| } |
| if (WIN_IsEqualGUID(riid, &IID_IToastActivatedEventHandler) || |
| WIN_IsEqualGUID(riid, &IID_IAgileObject) || |
| WIN_IsEqualGUID(riid, &IID_IUnknown)) { |
| *ppvObject = _this; |
| _this->lpVtbl->AddRef(_this); |
| return S_OK; |
| } |
| return E_NOINTERFACE; |
| } |
| |
| static HRESULT STDMETHODCALLTYPE Impl_OnActivated_Invoke(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable *_this, __x_ABI_CWindows_CUI_CNotifications_CIToastNotification *Sender, IInspectable *Args) |
| { |
| __x_ABI_CWindows_CUI_CNotifications_CIToastActivatedEventArgs *pEventArgs; |
| HRESULT hr = Args->lpVtbl->QueryInterface(Args, &IID_IToastActivatedEventArgs, (LPVOID *)&pEventArgs); |
| if (SUCCEEDED(hr)) { |
| SDL_NotificationID id = 0; |
| |
| __x_ABI_CWindows_CUI_CNotifications_CIToastNotification2 *pToastNotification2; |
| hr = Sender->lpVtbl->QueryInterface(Sender, &IID_IToastNotification2, (LPVOID *)&pToastNotification2); |
| if (SUCCEEDED(hr)) { |
| HSTRING hsTag; |
| hr = pToastNotification2->lpVtbl->get_Tag(pToastNotification2, &hsTag); |
| if (SUCCEEDED(hr)) { |
| PCWSTR tag = WIN_WindowsGetStringRawBuffer(hsTag, NULL); |
| id = (SDL_NotificationID)SDL_wcstoul(tag, NULL, 10); |
| } |
| pToastNotification2->lpVtbl->Release(pToastNotification2); |
| } |
| |
| if (id) { |
| HSTRING hsEventString; |
| hr = pEventArgs->lpVtbl->get_Arguments(pEventArgs, &hsEventString); |
| if (SUCCEEDED(hr)) { |
| PCWCHAR wstr = WIN_WindowsGetStringRawBuffer(hsEventString, NULL); |
| if (wstr) { |
| char tmp[512]; |
| const int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, tmp, sizeof(tmp), NULL, NULL); |
| if (len > 0 && len <= sizeof(tmp)) { |
| SDL_SendNotificationAction(id, tmp); |
| } |
| } |
| } |
| } |
| } |
| |
| return S_OK; |
| } |
| |
| static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectableVtbl WindowsToast__OnActivatedVtbl = { |
| .QueryInterface = &Impl_OnActivated_QueryInterface, |
| .AddRef = (void *)Impl_IGeneric_AddRef, |
| .Release = (void *)Impl_IGeneric_Release, |
| .Invoke = &Impl_OnActivated_Invoke, |
| }; |
| |
| static Impl_IGeneric OnActivated = { |
| .lpVtbl = (IUnknownVtbl *)&WindowsToast__OnActivatedVtbl, |
| .refCount.value = 1 |
| }; |
| |
| // OnDismissed interface |
| static HRESULT STDMETHODCALLTYPE Impl_OnDismissed_QueryInterface(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs *_this, REFIID riid, void **ppvObject) |
| { |
| if (ppvObject == NULL) { |
| return E_POINTER; |
| } |
| if (WIN_IsEqualGUID(riid, &IID_IToastDismissedEventHandler) || |
| WIN_IsEqualGUID(riid, &IID_IAgileObject) || |
| WIN_IsEqualGUID(riid, &IID_IUnknown)) { |
| *ppvObject = _this; |
| _this->lpVtbl->AddRef(_this); |
| return S_OK; |
| } |
| return E_NOINTERFACE; |
| } |
| |
| static HRESULT STDMETHODCALLTYPE Impl_OnDismissed_Invoke(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs *_this, __x_ABI_CWindows_CUI_CNotifications_CIToastNotification *Sender, __x_ABI_CWindows_CUI_CNotifications_CIToastDismissedEventArgs *Args) |
| { |
| __x_ABI_CWindows_CUI_CNotifications_CToastDismissalReason Reason; |
| Args->lpVtbl->get_Reason(Args, &Reason); |
| |
| /* Remove transient notifications that were cancelled or timed out, |
| * so they won't persist in the notification center. |
| */ |
| switch (Reason) { |
| case ToastDismissalReason_TimedOut: |
| case ToastDismissalReason_UserCanceled: |
| pToastNotifier->lpVtbl->Hide(pToastNotifier, Sender); |
| break; |
| |
| default: |
| break; |
| } |
| |
| return S_OK; |
| } |
| |
| static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgsVtbl WindowsToast__OnDismissedVtbl = { |
| .QueryInterface = &Impl_OnDismissed_QueryInterface, |
| .AddRef = (void *)Impl_IGeneric_AddRef, |
| .Release = (void *)Impl_IGeneric_Release, |
| .Invoke = &Impl_OnDismissed_Invoke, |
| }; |
| |
| static Impl_IGeneric OnDismissed = { |
| .lpVtbl = (IUnknownVtbl *)&WindowsToast__OnDismissedVtbl, |
| .refCount.value = 1 |
| }; |
| |
| static bool IsInPackage() |
| { |
| if (!WIN_GetCurrentPackageFullName) { |
| HMODULE kernel32 = GetModuleHandle(TEXT("kernel32.dll")); |
| WIN_GetCurrentPackageFullName = (GetCurrentPackageFullName_t)GetProcAddress(kernel32, "GetCurrentPackageFullName"); |
| } |
| |
| if (WIN_GetCurrentPackageFullName) { |
| UINT32 length = 0; |
| LONG rc = WIN_GetCurrentPackageFullName(&length, NULL); |
| if (rc != ERROR_INSUFFICIENT_BUFFER) { |
| if (rc == APPMODEL_ERROR_NO_PACKAGE) { |
| return false; |
| } else { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| static WCHAR *GetExePath() |
| { |
| DWORD buflen = MAX_PATH; |
| WCHAR *path = NULL; |
| DWORD len = 0; |
| |
| for (;;) { |
| WCHAR *ptr = SDL_realloc(path, buflen * sizeof(WCHAR)); |
| if (!ptr) { |
| SDL_free(path); |
| return NULL; |
| } |
| |
| path = ptr; |
| |
| len = GetModuleFileNameW(NULL, path, buflen); |
| // If this was truncated, then len >= buflen - 1 |
| if (len < buflen - 1) { |
| break; |
| } |
| |
| // buffer too small? Try again. |
| buflen *= 2; |
| } |
| |
| if (len == 0) { |
| SDL_free(path); |
| return NULL; |
| } |
| |
| return path; |
| } |
| |
| /* There is no way to pass an image as a byte stream when creating Windows |
| * notifications, so surfaces are saved as PNG files to temporary storage |
| * while the notification is being shown, then cleaned up a few seconds later. |
| */ |
| typedef struct ToastIcon |
| { |
| struct ToastIcon *next; |
| WCHAR icon_file[1]; |
| } ToastIcon; |
| |
| static ToastIcon *toast_icons = NULL; |
| static UINT_PTR cleanup_timer_id = 0; |
| |
| static void CleanupIcons() |
| { |
| KillTimer(NULL, cleanup_timer_id); |
| cleanup_timer_id = 0; |
| |
| for (ToastIcon *i = toast_icons; i; i = toast_icons) { |
| DeleteFileW(i->icon_file); |
| toast_icons = i->next; |
| SDL_free(i); |
| } |
| } |
| |
| static void CALLBACK IconCleanupCallback(HWND hWnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) |
| { |
| CleanupIcons(); |
| } |
| |
| static WCHAR *GetFullPath(const char *path) |
| { |
| const size_t conv_len = SDL_strlen(path) + 1; |
| |
| WCHAR *conv_path = SDL_malloc(conv_len * sizeof(WCHAR)); |
| if (!conv_path) { |
| return NULL; |
| } |
| MultiByteToWideChar(CP_UTF8, 0, path, -1, conv_path, (int)conv_len); |
| |
| const DWORD full_len = GetFullPathNameW(conv_path, 0, NULL, NULL); |
| if (!full_len) { |
| SDL_free(conv_path); |
| return NULL; |
| } |
| |
| WCHAR *full_path = SDL_malloc(full_len * sizeof(WCHAR)); |
| if (!full_path) { |
| SDL_free(conv_path); |
| return NULL; |
| } |
| GetFullPathNameW(conv_path, full_len, full_path, NULL); |
| SDL_free(conv_path); |
| |
| // Make sure the file actually exists. |
| WIN32_FILE_ATTRIBUTE_DATA attr; |
| if (GetFileAttributesExW(full_path, GetFileExInfoStandard, &attr)) { |
| return full_path; |
| } |
| |
| SDL_free(full_path); |
| return NULL; |
| } |
| |
| static const WCHAR *GetToastIconPath() |
| { |
| if (app_icon_path) { |
| return app_icon_path; |
| } |
| |
| const char *icon = SDL_GetStringProperty(SDL_GetGlobalProperties(), SDL_PROP_GLOBAL_NOTIFICATION_HEADER_ICON_STRING, NULL); |
| if (icon) { |
| app_icon_path = GetFullPath(icon); |
| } |
| |
| return app_icon_path; |
| } |
| |
| static WCHAR *SaveToastImage(SDL_Surface *icon) |
| { |
| if (icon) { |
| // The documentation states that the buffers for these should be MAX_PATH. |
| WCHAR *temp_path = NULL; |
| size_t path_len = 0; |
| |
| // Stop any timers so they won't fire in the middle of this. |
| if (cleanup_timer_id) { |
| KillTimer(NULL, cleanup_timer_id); |
| } |
| |
| path_len = GetTempPathW(0, NULL); |
| if (!path_len) { |
| return NULL; |
| } |
| temp_path = SDL_realloc(temp_path, (path_len + 1) * sizeof(WCHAR)); |
| path_len = GetTempPathW((DWORD)path_len + 1, temp_path); |
| if (!path_len) { |
| SDL_free(temp_path); |
| return NULL; |
| } |
| |
| WCHAR file_name[MAX_PATH]; |
| const UINT name_ret = GetTempFileNameW(temp_path, L"SDL", 0, file_name); |
| SDL_free(temp_path); |
| if (!name_ret) { |
| return NULL; |
| } |
| |
| path_len += SDL_wcslen(file_name) + 5; |
| |
| WCHAR *path_buf = NULL; |
| ToastIcon *toast_icon = SDL_calloc(1, sizeof(ToastIcon) + (path_len * sizeof(WCHAR))); |
| toast_icon->next = toast_icons; |
| toast_icons = toast_icon; |
| path_buf = toast_icon->icon_file; |
| |
| SDL_wcslcat(path_buf, file_name, path_len); |
| SDL_wcslcat(path_buf, L".png", path_len); |
| |
| const int len = WideCharToMultiByte(CP_UTF8, 0, path_buf, -1, NULL, 0, NULL, NULL); |
| char *png_path = SDL_malloc(len * sizeof(char)); |
| WideCharToMultiByte(CP_UTF8, 0, path_buf, -1, png_path, len, NULL, NULL); |
| SDL_SavePNG(icon, png_path); |
| SDL_free(png_path); |
| |
| // Duplicate the path, since the source object will be destroyed by a timer. |
| path_buf = SDL_wcsdup(path_buf); |
| |
| // Schedule a cleanup of icons 5 seconds from now. |
| cleanup_timer_id = SetTimer(NULL, 0, 5000, IconCleanupCallback); |
| |
| return path_buf; |
| } |
| |
| return NULL; |
| } |
| |
| static WCHAR *GetAppMetadata(const char *metadata_name) |
| { |
| WCHAR *metadata = NULL; |
| int id_len = 0; |
| |
| const char *app_metadata = SDL_GetAppMetadataProperty(metadata_name); |
| if (app_metadata && *app_metadata != '\0') { |
| id_len = MultiByteToWideChar(CP_UTF8, 0, app_metadata, -1, NULL, 0); |
| if (id_len > 0) { |
| metadata = SDL_malloc(id_len * sizeof(WCHAR)); |
| if (!metadata) { |
| return NULL; |
| } |
| |
| MultiByteToWideChar(CP_UTF8, 0, app_metadata, -1, metadata, id_len); |
| return metadata; |
| } |
| } else { |
| WCHAR *wszExePath = GetExePath(); |
| |
| for (WCHAR *c = wszExePath + SDL_wcslen(wszExePath); c >= wszExePath; --c) { |
| if (*c == L'/' || *c == L'\\') { |
| metadata = c + 1; |
| break; |
| } |
| } |
| |
| if (!metadata) { |
| metadata = wszExePath; |
| } |
| |
| metadata = SDL_wcsdup(metadata); |
| SDL_free(wszExePath); |
| |
| return metadata; |
| } |
| |
| return NULL; |
| } |
| |
| static void QuitToastSystem() |
| { |
| if (pToastNotificationHistory) { |
| pToastNotificationHistory->lpVtbl->Release(pToastNotificationHistory); |
| pToastNotificationHistory = NULL; |
| } |
| if (pToastNotificationManagerStatics2) { |
| pToastNotificationManagerStatics2->lpVtbl->Release(pToastNotificationManagerStatics2); |
| pToastNotificationManagerStatics2 = NULL; |
| } |
| if (pNotificationFactory) { |
| pNotificationFactory->lpVtbl->Release(pNotificationFactory); |
| pNotificationFactory = NULL; |
| } |
| if (pToastNotifier) { |
| pToastNotifier->lpVtbl->Release(pToastNotifier); |
| pToastNotifier = NULL; |
| } |
| if (pToastNotificationManager) { |
| pToastNotificationManager->lpVtbl->Release(pToastNotificationManager); |
| pToastNotificationManager = NULL; |
| } |
| if (hsAppId) { |
| WIN_WindowsDeleteString(hsAppId); |
| hsAppId = NULL; |
| } |
| if (hsGroupId) { |
| WIN_WindowsDeleteString(hsGroupId); |
| hsGroupId = NULL; |
| } |
| |
| CleanupIcons(); |
| |
| if (ro_initialized) { |
| WIN_RoUninitialize(); |
| ro_initialized = false; |
| } |
| if (co_initialized) { |
| WIN_CoUninitialize(); |
| co_initialized = false; |
| } |
| |
| SDL_free(app_reg_key); |
| app_reg_key = NULL; |
| |
| SDL_free(app_icon_path); |
| app_icon_path = NULL; |
| } |
| |
| static bool InitToastSystem() |
| { |
| // Only try to initialize once. |
| if (initialization_status != WIN32_NOTIFICATIONS_UNINITIALIZED) { |
| return initialization_status == WIN32_NOTIFICATIONS_INITIALIZED; |
| } |
| |
| // Failure of anything results in an early-out, so the initial value is failure. |
| initialization_status = WIN32_NOTIFICATIONS_INITIALIZATION_FAILED; |
| |
| #define RESOLVE(x) \ |
| WIN_##x = (x##_t)WIN_LoadComBaseFunction(#x); \ |
| if (!WIN_##x) \ |
| return WIN_SetError("GetProcAddress failed for " #x) |
| RESOLVE(RoGetActivationFactory); |
| RESOLVE(RoActivateInstance); |
| RESOLVE(WindowsCreateString); |
| RESOLVE(WindowsCreateStringReference); |
| RESOLVE(WindowsGetStringRawBuffer); |
| RESOLVE(WindowsDeleteString); |
| #undef RESOLVE |
| |
| HSTRING_HEADER hshToastNotificationManager; |
| HSTRING hsToastNotificationManager = NULL; |
| HSTRING_HEADER hshToastNotification; |
| HSTRING hsToastNotification = NULL; |
| WCHAR *image_path = NULL; |
| |
| // Initialize COM and Windows runtime with the same threading model. |
| HRESULT hr = WIN_CoInitialize(); |
| if (FAILED(hr)) { |
| return false; |
| } |
| co_initialized = true; |
| |
| hr = WIN_RoInitialize(); |
| if (FAILED(hr)) { |
| return false; |
| } |
| ro_initialized = true; |
| |
| // Get the application ID and name. |
| WCHAR *app_id = GetAppMetadata(SDL_PROP_APP_METADATA_IDENTIFIER_STRING); |
| WCHAR *app_name = GetAppMetadata(SDL_PROP_APP_METADATA_NAME_STRING); |
| |
| // Create the persistent appID string. |
| hr = WIN_WindowsCreateString(app_id, (UINT32)SDL_wcslen(app_id), &hsAppId); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| // Create the persistent groupID string. |
| hr = WIN_WindowsCreateString(GROUP_ID_STR, (UINT32)SDL_wcslen(GROUP_ID_STR), &hsGroupId); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| // Build the registry key. |
| { |
| size_t reg_key_len = SDL_wcslen(REG_KEY_BASE) + SDL_wcslen(app_id) + 1; |
| app_reg_key = SDL_malloc(reg_key_len * sizeof(WCHAR)); |
| if (!app_reg_key) { |
| goto cleanup; |
| } |
| SDL_swprintf(app_reg_key, reg_key_len, L"%ls%ls", REG_KEY_BASE, app_id); |
| } |
| |
| // Set the app name and icon. |
| { |
| const WCHAR *icon_path = GetToastIconPath(); |
| if (icon_path) { |
| hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"IconUri", REG_SZ, icon_path, (DWORD)(SDL_wcslen(icon_path) * sizeof(WCHAR)))); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| } else { |
| // This will "fail" if the key already doesn't exist, which is fine. |
| RegDeleteKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"IconUri"); |
| } |
| } |
| |
| hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"DisplayName", REG_SZ, app_name, (DWORD)(SDL_wcslen(app_name) * sizeof(WCHAR)))); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = WIN_WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager, sizeof(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) / sizeof(WCHAR) - 1, &hshToastNotificationManager, &hsToastNotificationManager); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = WIN_RoGetActivationFactory(hsToastNotificationManager, &IID_IToastNotificationManagerStatics, (LPVOID *)&pToastNotificationManager); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = pToastNotificationManager->lpVtbl->CreateToastNotifierWithId(pToastNotificationManager, hsAppId, &pToastNotifier); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = WIN_WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification, (UINT32)(sizeof(RuntimeClass_Windows_UI_Notifications_ToastNotification) / sizeof(wchar_t) - 1), &hshToastNotification, &hsToastNotification); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = WIN_RoGetActivationFactory(hsToastNotification, &IID_IToastNotificationFactory, (LPVOID *)&pNotificationFactory); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| // This acts as a version check, as notification history is only available on Win10+. |
| hr = pToastNotificationManager->lpVtbl->QueryInterface(pToastNotificationManager, &IID_IToastNotificationManagerStatics2, (LPVOID *)&pToastNotificationManagerStatics2); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = pToastNotificationManagerStatics2->lpVtbl->get_History(pToastNotificationManagerStatics2, &pToastNotificationHistory); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| initialization_status = WIN32_NOTIFICATIONS_INITIALIZED; |
| |
| cleanup: |
| WIN_WindowsDeleteString(hsToastNotificationManager); |
| WIN_WindowsDeleteString(hsToastNotification); |
| SDL_free(image_path); |
| SDL_free(app_id); |
| SDL_free(app_name); |
| |
| if (initialization_status == WIN32_NOTIFICATIONS_INITIALIZATION_FAILED) { |
| QuitToastSystem(); |
| } |
| |
| return initialization_status == WIN32_NOTIFICATIONS_INITIALIZED; |
| } |
| |
| static bool AppendXmlAudio(SDL_IOStream *dst, const char *sound) |
| { |
| static const WCHAR *default_sound_path = L"ms-winsoundevent:Notification.Default"; |
| |
| WCHAR buf[512]; |
| WCHAR *buf_ptr = buf; |
| WCHAR *path_format = NULL; |
| const WCHAR *silent_str = L"false"; |
| const WCHAR *sound_path = default_sound_path; |
| int buf_len = SDL_arraysize(buf); |
| |
| if (SDL_strcmp(sound, "silent") == 0) { |
| silent_str = L"true"; |
| } else if (SDL_strcmp(sound, "default") != 0) { |
| /* Windows currently only loads custom notification sounds when the app is |
| * in an MSIX package. We'll prepend a 'file:///' prefix when not in a package |
| * in case this changes in the future, but for now, it just plays the default |
| * sound. |
| */ |
| if (IsInPackage()) { |
| const WCHAR *prefix = L"ms-appx:///"; |
| const size_t prefix_len = 11; |
| |
| const size_t path_len = SDL_strlen(sound) + prefix_len + 1; |
| if (path_len) { |
| path_format = SDL_malloc(sizeof(WCHAR *) * path_len); |
| if (!path_format) { |
| return false; |
| } |
| SDL_wcslcpy(path_format, prefix, path_len); |
| |
| if (*sound == '/') { |
| ++sound; |
| } |
| MultiByteToWideChar(CP_UTF8, 0, sound, -1, path_format + prefix_len, (int)(path_len - prefix_len)); |
| sound_path = path_format; |
| } |
| } else { |
| SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Windows does not support custom notification sounds outside an MSIX package"); |
| |
| WCHAR *path = GetFullPath(sound); |
| if (path) { |
| const WCHAR *prefix = L"file:///"; |
| const DWORD prefix_len = 8; |
| |
| const size_t path_len = SDL_wcslen(path) + prefix_len + 1; |
| sound_path = path_format = SDL_malloc(sizeof(WCHAR *) * path_len); |
| if (!path_format) { |
| SDL_free(path); |
| return false; |
| } |
| |
| SDL_wcslcpy(path_format, prefix, path_len); |
| SDL_wcslcat(path_format, path, path_len); |
| SDL_free(path); |
| } |
| } |
| } |
| |
| for (;;) { |
| const int len = SDL_swprintf(buf_ptr, buf_len, L"<audio src=\"%ls\" loop=\"false\" silent=\"%ls\"/>", sound_path, silent_str); |
| if (len < buf_len) { |
| buf_len = len; |
| break; |
| } |
| |
| buf_ptr = SDL_malloc((len + 1) * sizeof(WCHAR)); |
| buf_len = len; |
| } |
| |
| SDL_free(path_format); |
| |
| const size_t write_size = buf_len * sizeof(WCHAR); |
| const bool res = SDL_WriteIO(dst, buf_ptr, write_size) == write_size; |
| |
| if (buf_ptr != buf) { |
| SDL_free(buf_ptr); |
| } |
| |
| return res; |
| } |
| |
| static bool AppendXmlAction(SDL_IOStream *dst, const char *label, const char *action) |
| { |
| char static_buf[512]; |
| char *buf = static_buf; |
| int buf_len = SDL_arraysize(static_buf); |
| |
| for (;;) { |
| int ret = SDL_snprintf(buf, buf_len, "<action content=\"%s\" activationType=\"foreground\" arguments=\"%s\"/>", label, action); |
| if (ret < buf_len) { |
| buf_len = ret + 1; |
| break; |
| } |
| |
| buf_len = ret + 1; |
| buf = SDL_realloc(buf, ret); |
| if (!buf) { |
| return false; |
| } |
| } |
| |
| // We know that, at most, an equal number of wide chars are needed for conversion. |
| WCHAR *wcbuf = SDL_malloc(buf_len * sizeof(WCHAR)); |
| bool ret = true; |
| |
| if (!wcbuf) { |
| ret = false; |
| goto done; |
| } |
| int wclen = MultiByteToWideChar(CP_UTF8, 0, buf, -1, wcbuf, buf_len); |
| if (wclen <= 0) { |
| ret = false; |
| goto done; |
| } |
| |
| const size_t size = (wclen - 1) * sizeof(WCHAR); |
| if (SDL_WriteIO(dst, wcbuf, size) < size) { |
| ret = false; |
| } |
| |
| done: |
| SDL_free(wcbuf); |
| if (buf != static_buf) { |
| SDL_free(buf); |
| } |
| |
| return ret; |
| } |
| |
| static bool AppendXmlImage(SDL_IOStream *dst, const WCHAR *image_path) |
| { |
| WCHAR static_buf[512]; |
| WCHAR *buf = static_buf; |
| int buf_len = SDL_arraysize(static_buf); |
| bool ret = true; |
| |
| for (;;) { |
| const int len = SDL_swprintf(buf, buf_len, L"<image id=\"1\" placement=\"appLogoOverride\" src=\"file:///%ls\"></image>", image_path); |
| if (len < buf_len) { |
| const size_t size = len * sizeof(WCHAR); |
| if (SDL_WriteIO(dst, buf, size) < size) { |
| ret = false; |
| } |
| break; |
| } |
| |
| buf_len = len + 1; |
| buf = SDL_realloc(buf, len * sizeof(WCHAR)); |
| if (!buf) { |
| return false; |
| } |
| } |
| |
| if (buf != static_buf) { |
| SDL_free(buf); |
| } |
| |
| return ret; |
| } |
| |
| #define XML_TOAST_OPENING_STR L"<toast scenario=\"default\" activationType=\"foreground\" launch=\"default\">" \ |
| L"<visual>" \ |
| L"<binding template=\"ToastGeneric\">" |
| #define XML_TOAST_CLOSING_STR L"</toast>" |
| #define XML_TEXT_OPENING_STR L"<text><![CDATA[" |
| #define XML_TEXT_CLOSING_STR L"]]></text>" |
| #define XML_ACTIONS_OPENING_STR L"<actions>" |
| #define XML_ACTIONS_CLOSING_STR L"</actions>" |
| #define XML_VISUAL_CLOSING_STR L"</binding></visual>" |
| |
| static bool AppendXmlText(SDL_IOStream *dst, const char *text) |
| { |
| const int wclen = MultiByteToWideChar(CP_UTF8, 0, text, -1, NULL, 0); |
| if (wclen <= 0) { |
| return false; |
| } |
| WCHAR *wcbuf = SDL_malloc(wclen * sizeof(WCHAR)); |
| if (!wcbuf) { |
| return false; |
| } |
| MultiByteToWideChar(CP_UTF8, 0, text, -1, wcbuf, wclen); |
| |
| size_t size = sizeof(XML_TEXT_OPENING_STR) - sizeof(WCHAR); |
| if (SDL_WriteIO(dst, XML_TEXT_OPENING_STR, size) < size) { |
| SDL_free(wcbuf); |
| return false; |
| } |
| |
| size = (wclen - 1) * sizeof(WCHAR); |
| if (SDL_WriteIO(dst, wcbuf, size) < size) { |
| SDL_free(wcbuf); |
| return false; |
| } |
| SDL_free(wcbuf); |
| |
| size = sizeof(XML_TEXT_CLOSING_STR) - sizeof(WCHAR); |
| if (SDL_WriteIO(dst, XML_TEXT_CLOSING_STR, size) < size) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static WCHAR *BuildNotificationXml(SDL_PropertiesID props, const WCHAR *icon_path, Uint32 *wchar_count) |
| { |
| WCHAR *xml = NULL; |
| SDL_IOStream *dst = SDL_IOFromDynamicMem(); |
| if (!dst) { |
| return NULL; |
| } |
| |
| const char *title = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_TITLE_STRING, NULL); |
| const char *message = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_MESSAGE_STRING, NULL); |
| const char *sound = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_SOUND_STRING, "default"); |
| const SDL_NotificationAction *actions = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_ACTIONS_POINTER, NULL); |
| const int num_actions = (int)SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_ACTION_COUNT_NUMBER, 0); |
| |
| size_t size = sizeof(XML_TOAST_OPENING_STR) - sizeof(WCHAR); |
| if (SDL_WriteIO(dst, XML_TOAST_OPENING_STR, size) < size) { |
| goto done; |
| } |
| if (!AppendXmlImage(dst, icon_path)) { |
| goto done; |
| } |
| if (!AppendXmlText(dst, title)) { |
| goto done; |
| } |
| if (!AppendXmlText(dst, message)) { |
| goto done; |
| } |
| |
| size = sizeof(XML_VISUAL_CLOSING_STR) - sizeof(WCHAR); |
| if (SDL_WriteIO(dst, XML_VISUAL_CLOSING_STR, size) < size) { |
| goto done; |
| } |
| |
| if (!AppendXmlAudio(dst, sound)) { |
| goto done; |
| } |
| |
| if (actions && num_actions) { |
| size = sizeof(XML_ACTIONS_OPENING_STR) - sizeof(WCHAR); |
| if (SDL_WriteIO(dst, XML_ACTIONS_OPENING_STR, size) < size) { |
| goto done; |
| } |
| |
| for (int i = 0; i < num_actions; ++i) { |
| if (actions[i].type == SDL_NOTIFICATION_ACTION_TYPE_BUTTON) { |
| if (!AppendXmlAction(dst, actions[i].button.action_label, actions[i].button.action_id)) { |
| goto done; |
| } |
| } |
| } |
| |
| size = sizeof(XML_ACTIONS_CLOSING_STR) - sizeof(WCHAR); |
| if (SDL_WriteIO(dst, XML_ACTIONS_CLOSING_STR, size) < size) { |
| goto done; |
| } |
| } |
| |
| // The XML string *must* be null-terminated for WindowsCreateStringReference(). |
| size = sizeof(XML_TOAST_CLOSING_STR); |
| if (SDL_WriteIO(dst, XML_TOAST_CLOSING_STR, size) < size) { |
| goto done; |
| } |
| *wchar_count = (Uint32)((SDL_TellIO(dst) / sizeof(WCHAR)) - 1); |
| |
| // Get the pointer to the XML string. |
| SDL_PropertiesID io_props = SDL_GetIOProperties(dst); |
| xml = SDL_GetPointerProperty(io_props, SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER, NULL); |
| SDL_SetPointerProperty(io_props, SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER, NULL); |
| |
| done: |
| SDL_CloseIO(dst); |
| return xml; |
| } |
| |
| static void ClearNotificationWithID(SDL_NotificationID id) |
| { |
| HSTRING_HEADER hshTag; |
| HSTRING hsTag = NULL; |
| WCHAR tag[32]; |
| |
| SDL_swprintf(tag, SDL_arraysize(tag), L"%" SDL_PRIu32, id); |
| HRESULT hr = WIN_WindowsCreateStringReference(tag, (UINT32)SDL_wcslen(tag), &hshTag, &hsTag); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| pToastNotificationHistory->lpVtbl->RemoveGroupedTagWithId(pToastNotificationHistory, hsTag, hsGroupId, hsAppId); |
| |
| cleanup: |
| WIN_WindowsDeleteString(hsTag); |
| } |
| |
| static NTSTATUS WIN_BCryptGenRandom(BCRYPT_ALG_HANDLE hAlgorithm, PUCHAR pbBuffer, ULONG cbBuffer, ULONG dwFlags) |
| { |
| static bool s_bLoaded; |
| static HMODULE s_hBCrypt; |
| static BCryptGenRandom_t pBCryptGenRandom; |
| |
| if (!s_bLoaded) { |
| s_hBCrypt = LoadLibraryEx(TEXT("bcrypt.dll"), NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); |
| s_bLoaded = true; |
| } |
| if (!pBCryptGenRandom) { |
| pBCryptGenRandom = (BCryptGenRandom_t)GetProcAddress(s_hBCrypt, "BCryptGenRandom"); |
| } |
| |
| if (pBCryptGenRandom) { |
| return pBCryptGenRandom(hAlgorithm, pbBuffer, cbBuffer, dwFlags); |
| } |
| |
| return -1; |
| } |
| |
| SDL_NotificationID SDL_SYS_ShowNotification(SDL_PropertiesID props) |
| { |
| HRESULT hr = S_OK; |
| SDL_NotificationID ret = 0; |
| |
| // Need Win10 or higher for notifications. |
| if (!InitToastSystem()) { |
| SDL_SetError("Failed to initialize the notification system (requires Windows 10 or higher)"); |
| return 0; |
| } |
| |
| SDL_Surface *image = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_IMAGE_POINTER, NULL); |
| const SDL_NotificationID replaces = SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_REPLACES_NUMBER, 0); |
| const SDL_NotificationPriority priority = SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_PRIORITY_NUMBER, SDL_NOTIFICATION_PRIORITY_NORMAL); |
| const bool transient = SDL_GetBooleanProperty(props, SDL_PROP_NOTIFICATION_TRANSIENT_BOOLEAN, false); |
| |
| if (replaces) { |
| ClearNotificationWithID(replaces); |
| } |
| |
| SDL_NotificationID new_id = 0; |
| // Generate a unique notification ID. |
| if (WIN_BCryptGenRandom(NULL, (PUCHAR)&new_id, sizeof(new_id), BCRYPT_USE_SYSTEM_PREFERRED_RNG) != 0) { // STATUS_SUCCESS == 0 |
| // No RNG? Use the low 32 bits of the current time. |
| new_id = (SDL_NotificationID)SDL_GetTicksNS(); |
| } |
| |
| // Windows notifications load images from disk, so save it to temporary storage. |
| WCHAR *image_path = SaveToastImage(image); |
| |
| // Build the XML description for the notification. |
| Uint32 xml_len = 0; |
| WCHAR *xml = BuildNotificationXml(props, image_path, &xml_len); |
| SDL_free(image_path); |
| if (!xml) { |
| return 0; |
| } |
| |
| HSTRING_HEADER hshXmlDocument; |
| HSTRING hsXmlDocument = NULL; |
| HSTRING_HEADER hshBanner; |
| HSTRING hsBanner = NULL; |
| IInspectable *pInspectable = NULL; |
| __x_ABI_CWindows_CData_CXml_CDom_CIXmlDocument *pXmlDocument = NULL; |
| __x_ABI_CWindows_CData_CXml_CDom_CIXmlDocumentIO *pXmlDocumentIO = NULL; |
| __x_ABI_CWindows_CUI_CNotifications_CIToastNotification *pToastNotification = NULL; |
| |
| hr = WIN_WindowsCreateStringReference(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument, (UINT32)(sizeof(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument) / sizeof(WCHAR) - 1), &hshXmlDocument, &hsXmlDocument); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = WIN_WindowsCreateStringReference(xml, xml_len, &hshBanner, &hsBanner); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = WIN_RoActivateInstance(hsXmlDocument, &pInspectable); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = pInspectable->lpVtbl->QueryInterface(pInspectable, &IID_IXmlDocument, (void **)(&pXmlDocument)); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = pXmlDocument->lpVtbl->QueryInterface(pXmlDocument, &IID_IXmlDocumentIO, (void **)(&pXmlDocumentIO)); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = pXmlDocumentIO->lpVtbl->LoadXml(pXmlDocumentIO, hsBanner); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = pNotificationFactory->lpVtbl->CreateToastNotification(pNotificationFactory, pXmlDocument, &pToastNotification); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| // Register the OnDismissed notifier to clear transient notifications when cancelled or timed out. |
| if (transient) { |
| EventRegistrationToken dismissedToken; |
| hr = pToastNotification->lpVtbl->add_Dismissed(pToastNotification, (__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs *)&OnDismissed, &dismissedToken); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| } |
| { |
| EventRegistrationToken activatedToken; |
| hr = pToastNotification->lpVtbl->add_Activated(pToastNotification, (__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable *)&OnActivated, &activatedToken); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| } |
| |
| // Tag with the ID for future removal or replacement. |
| { |
| __x_ABI_CWindows_CUI_CNotifications_CIToastNotification2 *pToastNotification2; |
| HSTRING_HEADER hshTag; |
| HSTRING hsTag = NULL; |
| WCHAR tag[32]; |
| |
| hr = pToastNotification->lpVtbl->QueryInterface(pToastNotification, &IID_IToastNotification2, (LPVOID *)&pToastNotification2); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| SDL_swprintf(tag, SDL_arraysize(tag), L"%" SDL_PRIu32, new_id); |
| hr = WIN_WindowsCreateStringReference(tag, (UINT32)SDL_wcslen(tag), &hshTag, &hsTag); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = pToastNotification2->lpVtbl->put_Group(pToastNotification2, hsGroupId); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = pToastNotification2->lpVtbl->put_Tag(pToastNotification2, hsTag); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| pToastNotification2->lpVtbl->Release(pToastNotification2); |
| } |
| |
| /* Set the priority. |
| * All levels except for SDL_NOTIFICATION_PRIORITY_CRITICAL map to normal on Windows, as selecting |
| * high priority can wake the screen, and should only be done when absolutely necessary. |
| */ |
| { |
| const __x_ABI_CWindows_CUI_CNotifications_CToastNotificationPriority toast_priority = priority != SDL_NOTIFICATION_PRIORITY_CRITICAL ? ToastNotificationPriority_Default : ToastNotificationPriority_High; |
| __x_ABI_CWindows_CUI_CNotifications_CIToastNotification4 *pToastNotification4; |
| hr = pToastNotification->lpVtbl->QueryInterface(pToastNotification, &IID_IToastNotification4, (LPVOID *)&pToastNotification4); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| hr = pToastNotification4->lpVtbl->put_Priority(pToastNotification4, toast_priority); |
| if (FAILED(hr)) { |
| goto cleanup; |
| } |
| |
| pToastNotification4->lpVtbl->Release(pToastNotification4); |
| } |
| |
| // Finally, show the notification. |
| hr = pToastNotifier->lpVtbl->Show(pToastNotifier, pToastNotification); |
| if (SUCCEEDED(hr)) { |
| ret = new_id; |
| } |
| |
| cleanup: |
| if (pToastNotification) { |
| pToastNotification->lpVtbl->Release(pToastNotification); |
| } |
| if (pXmlDocumentIO) { |
| pXmlDocumentIO->lpVtbl->Release(pXmlDocumentIO); |
| } |
| if (pXmlDocument) { |
| pXmlDocument->lpVtbl->Release(pXmlDocument); |
| } |
| if (pInspectable) { |
| pInspectable->lpVtbl->Release(pInspectable); |
| } |
| if (hsBanner) { |
| WIN_WindowsDeleteString(hsBanner); |
| } |
| if (hsXmlDocument) { |
| WIN_WindowsDeleteString(hsXmlDocument); |
| } |
| |
| SDL_free(xml); |
| |
| return ret; |
| } |
| |
| bool SDL_RemoveNotification(SDL_NotificationID notification) |
| { |
| if (!InitToastSystem()) { |
| return SDL_SetError("Failed to initialize the notification system (requires Windows 10 or higher)"); |
| } |
| |
| ClearNotificationWithID(notification); |
| return true; |
| } |
| |
| void SDL_CleanupNotifications(void) |
| { |
| QuitToastSystem(); |
| initialization_status = WIN32_NOTIFICATIONS_UNINITIALIZED; |
| } |
| |
| bool SDL_RequestNotificationPermission(void) |
| { |
| // Notifications are supported on Win10 or higher. |
| return InitToastSystem(); |
| } |