Skip to content
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 130 additions & 56 deletions flutter_local_notifications_windows/src/ffi_api.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <vector>

#include <windows.h> // <-- This must be the first Windows header
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Data.Xml.Dom.h>
Expand All @@ -19,20 +21,68 @@ NativePlugin* createPlugin() { return new NativePlugin(); }

void disposePlugin(NativePlugin* plugin) { delete plugin; }

/// (Re)creates the WinRT handles used to show and manage notifications.
static void createHandles(NativePlugin* plugin) {
plugin->notifier = plugin->hasIdentity
? ToastNotificationManager::CreateToastNotifier()
: ToastNotificationManager::CreateToastNotifier(plugin->aumid);
plugin->history = ToastNotificationManager::History();
}

/// Whether the error means the handles point to an object that no longer exists.
///
/// The notifier and the history are COM proxies created once in [init]. If the notification
/// platform behind them goes away while the app is running, every call on them fails with one of
/// these errors until they are created again.
///
/// Only codes that guarantee the call did not execute are listed, so the retry cannot run it twice.
static bool isDisconnected(const winrt::hresult_error& error) {
const HRESULT code = error.code();
return code == CO_E_OBJNOTCONNECTED || code == RPC_E_DISCONNECTED
|| code == RPC_E_SERVER_DIED_DNE || code == HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE)
|| code == HRESULT_FROM_WIN32(RPC_S_CALL_FAILED_DNE);
}

/// Runs [action], which uses the plugin's WinRT handles, without letting an exception escape.
///
/// An exception that crosses the FFI boundary cannot be caught on the Dart side: it terminates the
/// process. If the handles are disconnected, they are created again and [action] is retried once.
/// Returns false if [action] could not complete.
template <typename Action>
static bool withHandles(NativePlugin* plugin, Action action) {
try {
action();
return true;
} catch (const winrt::hresult_error& error) {
if (!isDisconnected(error)) return false;
} catch (...) {
return false;

@MaikuB MaikuB Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know the plugin wasn't doing this before but is there an opportunity for it to catch an exception that can be propagated on the Dart so it can be caught by apps and dealt with appropriately? This is a general question btw so applies to the next catch block below too

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's possible. An exception can't cross the FFI boundary, but the native side can keep the failure and the Dart side can throw it. Roughly:

  • when a call still fails after the reconnect-and-retry, withHandles stores the error (HRESULT and message) in the plugin instead of only returning false;
  • a small FFI getter exposes it, and the Dart wrapper throws a dedicated exception (for example WindowsNotificationException with code and message) whenever a native call returns false;
  • this also covers two points Copilot raised: showRawXml would no longer report a platform failure as invalid XML, and zonedSchedule / zonedScheduleRawXml would no longer complete silently when nothing was scheduled. A failed initialize() could become retryable at the same time.

It does change behaviour: calls such as cancelAll, which used to either succeed or terminate the app, would now throw, so apps may need a try/catch. Would you rather have it in this PR, or keep this one as the crash fix for the 22.x hotfix and do the error propagation in a follow-up PR on master? Either way I'll fix the std::stoi prefix issue Copilot found here.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the info, it can be done in a follow up PR

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll do it in a follow-up PR on master once this one is merged.

The std::stoi issue Copilot raised is fixed in 2ccc046: the tag is now parsed with std::from_chars and kept only when every character was consumed, so a tag such as 12abc is skipped instead of being listed as ID 12. Tags written by the plugin still parse, negative IDs included.

}
try {
createHandles(plugin);
action();
return true;
} catch (...) {
return false;
}
}

bool init(
NativePlugin* plugin, char* appName, char* aumId, char* guid, char* iconPath,
NativeNotificationCallback callback
) {
string icon;
if (iconPath != nullptr) icon = string(iconPath);
const auto didRegister = plugin->registerApp(aumId, appName, guid, icon, callback);
if (!didRegister) return false;
plugin->hasIdentity = hasPackageIdentity();
plugin->aumid = winrt::to_hstring(aumId);
plugin->notifier = plugin->hasIdentity
? ToastNotificationManager::CreateToastNotifier()
: ToastNotificationManager::CreateToastNotifier(plugin->aumid);
plugin->history = ToastNotificationManager::History();
try {
const auto didRegister = plugin->registerApp(aumId, appName, guid, icon, callback);
if (!didRegister) return false;
plugin->hasIdentity = hasPackageIdentity();
plugin->aumid = winrt::to_hstring(aumId);
createHandles(plugin);
} catch (...) {
// The notification platform can be unavailable at launch. Report it instead of terminating.
return false;
Comment on lines +84 to +86
}
plugin->isReady = true;
return true;
}
Expand All @@ -55,12 +105,13 @@ bool showNotification(NativePlugin* plugin, int id, char* xml, NativeStringMap b
} catch (winrt::hresult_error error) {
return false;
}
ToastNotification notification(doc);
const auto data = dataFromMap(bindings);
notification.Tag(winrt::to_hstring(id));
notification.Data(data);
plugin->notifier.value().Show(notification);
return true;
return withHandles(plugin, [&] {
ToastNotification notification(doc);
const auto data = dataFromMap(bindings);
notification.Tag(winrt::to_hstring(id));
notification.Data(data);
plugin->notifier.value().Show(notification);
});
Comment on lines +110 to +116
}

bool scheduleNotification(NativePlugin* plugin, int id, char* xml, int time) {
Expand All @@ -71,61 +122,86 @@ bool scheduleNotification(NativePlugin* plugin, int id, char* xml, int time) {
} catch (winrt::hresult_error error) {
return false;
}
ScheduledToastNotification notification(doc, winrt::clock::from_time_t(time));
notification.Tag(winrt::to_hstring(id));
plugin->notifier.value().AddToSchedule(notification);
return true;
return withHandles(plugin, [&] {
ScheduledToastNotification notification(doc, winrt::clock::from_time_t(time));
notification.Tag(winrt::to_hstring(id));
plugin->notifier.value().AddToSchedule(notification);
});
Comment on lines +127 to +131
}

NativeUpdateResult updateNotification(NativePlugin* plugin, int id, NativeStringMap bindings) {
if (!plugin->isReady) return NativeUpdateResult::failed;
const auto tag = winrt::to_hstring(id);
const auto data = dataFromMap(bindings);
const auto result = plugin->notifier.value().Update(data, tag);
return (NativeUpdateResult) result;
NativeUpdateResult result = NativeUpdateResult::failed;
const auto didUpdate = withHandles(plugin, [&] {
const auto data = dataFromMap(bindings);
result = (NativeUpdateResult) plugin->notifier.value().Update(data, tag);
});
return didUpdate ? result : NativeUpdateResult::failed;
}

void cancelAll(NativePlugin* plugin) {
if (!plugin->isReady) return;
if (plugin->hasIdentity) {
plugin->history.value().Clear();
} else {
plugin->history.value().Clear(plugin->aumid);
}
for (const auto notification : plugin->notifier.value().GetScheduledToastNotifications()) {
plugin->notifier.value().RemoveFromSchedule(notification);
}
withHandles(plugin, [&] {
if (plugin->hasIdentity) {
plugin->history.value().Clear();
} else {
plugin->history.value().Clear(plugin->aumid);
}
for (const auto notification : plugin->notifier.value().GetScheduledToastNotifications()) {
plugin->notifier.value().RemoveFromSchedule(notification);
}
});
}

void cancelNotification(NativePlugin* plugin, int id) {
if (!plugin->isReady) return;
const auto tag = winrt::to_hstring(id);
if (plugin->hasIdentity) plugin->history.value().Remove(tag);
for (const auto notification : plugin->notifier.value().GetScheduledToastNotifications()) {
if (notification.Tag() == tag) {
plugin->notifier.value().RemoveFromSchedule(notification);
return;
withHandles(plugin, [&] {
if (plugin->hasIdentity) plugin->history.value().Remove(tag);
for (const auto notification : plugin->notifier.value().GetScheduledToastNotifications()) {
if (notification.Tag() == tag) {
plugin->notifier.value().RemoveFromSchedule(notification);
return;
}
}
});
}

/// Adds the notification ID held in [tag], skipping a tag that is not a number.
///
/// The history holds every notification shown under the app's ID, including ones this plugin did
/// not create, so one unexpected tag must not hide the others.
static void addId(vector<int>& ids, const winrt::hstring& tag) {
try {
ids.push_back(std::stoi(winrt::to_string(tag)));
} catch (const std::exception&) {
}
}
Comment on lines +179 to 185

/// Copies the notification IDs into an array that must be released with [freeDetailsArray].
static NativeNotificationDetails* toDetailsArray(const vector<int>& ids, int* size) {
*size = static_cast<int>(ids.size());
const auto result = new NativeNotificationDetails[ids.size()];
for (size_t index = 0; index < ids.size(); index++) result[index].id = ids[index];
return result;
}

NativeNotificationDetails* getActiveNotifications(NativePlugin* plugin, int* size) {
// TODO: Get more details here
if (!plugin->isReady || !plugin->hasIdentity) {
*size = 0;
return nullptr;
}
const auto active = plugin->history.value().GetHistory();
*size = active.Size();
const auto result = new NativeNotificationDetails[*size];
int index = 0;
for (const auto notification : active) {
const auto tag = notification.Tag();
const auto tagStr = winrt::to_string(tag);
const auto tagInt = std::stoi(tagStr);
result[index++].id = tagInt;
}
return result;
vector<int> ids;
const auto didRead = withHandles(plugin, [&] {
ids.clear();
for (const auto notification : plugin->history.value().GetHistory()) {
addId(ids, notification.Tag());
}
});
if (!didRead) ids.clear();
return toDetailsArray(ids, size);
}

NativeNotificationDetails* getPendingNotifications(NativePlugin* plugin, int* size) {
Expand All @@ -134,17 +210,15 @@ NativeNotificationDetails* getPendingNotifications(NativePlugin* plugin, int* si
*size = 0;
return nullptr;
}
const auto pending = plugin->notifier.value().GetScheduledToastNotifications();
*size = pending.Size();
const auto result = new NativeNotificationDetails[*size];
int index = 0;
for (const auto notification : pending) {
const auto tag = notification.Tag();
const auto tagStr = winrt::to_string(tag);
const auto tagInt = std::stoi(tagStr);
result[index++].id = tagInt;
}
return result;
vector<int> ids;
const auto didRead = withHandles(plugin, [&] {
ids.clear();
for (const auto notification : plugin->notifier.value().GetScheduledToastNotifications()) {
addId(ids, notification.Tag());
}
});
if (!didRead) ids.clear();
return toDetailsArray(ids, size);
}

void freeDetailsArray(NativeNotificationDetails* ptr) { delete[] ptr; }
Expand Down
Loading