From 9a2ebe61e4d3d681d03b3c6f6351a2a653f404dc Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:13:30 -0500 Subject: [PATCH] fix: dispatch toast action and reply events from WinRT activation path (#51330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: dispatch toast action and reply events from WinRT activation path ToastEventHandler::Invoke previously returned S_OK without dispatching whenever the activation arguments looked structured (type=action, type=reply, or contained &tag=), on the assumption that the COM INotificationActivationCallback::Activate path would deliver the event instead. That assumption only holds when Windows actually invokes the COM activator — which it does for MSIX-packaged apps launched cold, and for unpackaged apps with a properly-registered CLSID when the app is not already running. For non-MSIX apps with activationType="foreground" while the app is running (the common case), Windows raises only the in-process WinRT Activated event, so action and reply were silently dropped. Dispatch structured activations through the same HandleToastActivation the COM path uses. User input (reply text, selection values) is pulled from IToastActivatedEventArgs2::UserInput, which carries the data the COM callback would otherwise have received via NOTIFICATION_USER_INPUT_DATA. Also drop the &tag= term from the structured-args check. Plain clicks in Electron-generated XML don't carry tag=, and a custom toast_xml that puts tag= on a click argument should now dispatch as a click rather than being silently dropped. Co-authored-by: Shelley Vohr * fix: release HSTRING out-params from toast activation Co-authored-by: Shelley Vohr --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Shelley Vohr --- .../win/windows_toast_notification.cc | 132 +++++++++++++----- shell/browser/win/scoped_hstring.h | 7 + 2 files changed, 104 insertions(+), 35 deletions(-) diff --git a/shell/browser/notifications/win/windows_toast_notification.cc b/shell/browser/notifications/win/windows_toast_notification.cc index 90f8cc86af..0b102cc4b3 100644 --- a/shell/browser/notifications/win/windows_toast_notification.cc +++ b/shell/browser/notifications/win/windows_toast_notification.cc @@ -27,6 +27,7 @@ #include "content/public/browser/browser_thread.h" #include "shell/browser/notifications/notification_delegate.h" #include "shell/browser/notifications/win/notification_presenter_win.h" +#include "shell/browser/notifications/win/windows_toast_activator.h" #include "shell/browser/win/scoped_hstring.h" #include "shell/common/application_info.h" #include "third_party/libxml/chromium/xml_writer.h" @@ -35,6 +36,12 @@ using ABI::Windows::Data::Xml::Dom::IXmlDocument; using ABI::Windows::Data::Xml::Dom::IXmlDocumentIO; +using ABI::Windows::Foundation::IPropertyValue; +using ABI::Windows::Foundation::Collections::IIterable; +using ABI::Windows::Foundation::Collections::IIterator; +using ABI::Windows::Foundation::Collections::IKeyValuePair; +using ABI::Windows::Foundation::Collections::IMap; +using ABI::Windows::Foundation::Collections::IPropertySet; using Microsoft::WRL::Wrappers::HStringReference; namespace winui = ABI::Windows::UI; @@ -785,19 +792,86 @@ ToastEventHandler::ToastEventHandler(Notification* notification) ToastEventHandler::~ToastEventHandler() = default; +namespace { + +// Extracts string user-input values from an IToastActivatedEventArgs2. +// Windows only fires the WinRT Activated event (not the COM activator) for +// `activationType="foreground"` actions while the app is already running, so +// reply text and selection values must be pulled from UserInput here rather +// than from the COM callback's NOTIFICATION_USER_INPUT_DATA. +std::vector ExtractUserInputs(IInspectable* args) { + std::vector inputs; + if (!args) + return inputs; + + ComPtr args2; + if (FAILED(args->QueryInterface(IID_PPV_ARGS(&args2))) || !args2) + return inputs; + + ComPtr user_input; + if (FAILED(args2->get_UserInput(&user_input)) || !user_input) + return inputs; + + ComPtr> map; + if (FAILED(user_input.As(&map)) || !map) + return inputs; + + ComPtr*>> iterable; + if (FAILED(map.As(&iterable)) || !iterable) + return inputs; + + ComPtr*>> iter; + if (FAILED(iterable->First(&iter)) || !iter) + return inputs; + + boolean has_current = false; + if (FAILED(iter->get_HasCurrent(&has_current))) + return inputs; + + while (has_current) { + ComPtr> kvp; + if (FAILED(iter->get_Current(&kvp)) || !kvp) + break; + ScopedHString key_hs; + ComPtr value; + if (SUCCEEDED(kvp->get_Key(key_hs.Receive())) && + SUCCEEDED(kvp->get_Value(&value)) && key_hs.success() && value) { + ComPtr prop; + ScopedHString value_hs; + if (SUCCEEDED(value.As(&prop)) && prop && + SUCCEEDED(prop->GetString(value_hs.Receive())) && + value_hs.success()) { + UINT32 key_len = 0; + UINT32 val_len = 0; + const wchar_t* key_raw = WindowsGetStringRawBuffer(key_hs, &key_len); + const wchar_t* val_raw = WindowsGetStringRawBuffer(value_hs, &val_len); + ActivationUserInput ui; + if (key_raw && key_len) + ui.key.assign(key_raw, key_len); + if (val_raw && val_len) + ui.value.assign(val_raw, val_len); + inputs.push_back(std::move(ui)); + } + } + if (FAILED(iter->MoveNext(&has_current))) + break; + } + return inputs; +} + +} // namespace + IFACEMETHODIMP ToastEventHandler::Invoke( winui::Notifications::IToastNotification* sender, IInspectable* args) { std::wstring arguments_w; - std::wstring tag_w; - std::wstring group_w; if (args) { - Microsoft::WRL::ComPtr - activated_args; + ComPtr activated_args; if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&activated_args)))) { - HSTRING args_hs = nullptr; - if (SUCCEEDED(activated_args->get_Arguments(&args_hs)) && args_hs) { + ScopedHString args_hs; + if (SUCCEEDED(activated_args->get_Arguments(args_hs.Receive())) && + args_hs.success()) { UINT32 len = 0; const wchar_t* raw = WindowsGetStringRawBuffer(args_hs, &len); if (raw && len) @@ -806,36 +880,24 @@ IFACEMETHODIMP ToastEventHandler::Invoke( } } - if (sender) { - Microsoft::WRL::ComPtr toast2; - if (SUCCEEDED(sender->QueryInterface(IID_PPV_ARGS(&toast2)))) { - HSTRING tag_hs = nullptr; - if (SUCCEEDED(toast2->get_Tag(&tag_hs)) && tag_hs) { - UINT32 len = 0; - const wchar_t* raw = WindowsGetStringRawBuffer(tag_hs, &len); - if (raw && len) - tag_w.assign(raw, len); - } - HSTRING group_hs = nullptr; - if (SUCCEEDED(toast2->get_Group(&group_hs)) && group_hs) { - UINT32 len = 0; - const wchar_t* raw = WindowsGetStringRawBuffer(group_hs, &len); - if (raw && len) - group_w.assign(raw, len); - } - } - } - - std::string notif_id; - if (notification_) { - notif_id = notification_->notification_id(); - } - - bool structured = arguments_w.find(L"&tag=") != std::wstring::npos || - arguments_w.find(L"type=action") != std::wstring::npos || - arguments_w.find(L"type=reply") != std::wstring::npos; - if (structured) + // For structured action/reply args, dispatch through the same handler the + // COM activator uses. Previously this path early-returned, assuming the COM + // INotificationActivationCallback would fire — but for non-MSIX apps with + // activationType="foreground" (and for MSIX apps while already running) + // Windows only raises the in-process WinRT Activated event, so those + // action/reply events were being silently dropped. See electron/electron + // issue #51147. + const bool structured = + arguments_w.find(L"type=action") != std::wstring::npos || + arguments_w.find(L"type=reply") != std::wstring::npos; + if (structured) { + std::vector inputs = ExtractUserInputs(args); + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, + base::BindOnce(&HandleToastActivation, arguments_w, std::move(inputs))); + DebugLog("Notification activated (structured)"); return S_OK; + } content::GetUIThreadTaskRunner({})->PostTask( FROM_HERE, diff --git a/shell/browser/win/scoped_hstring.h b/shell/browser/win/scoped_hstring.h index d0edfe4ea6..a03fadee5f 100644 --- a/shell/browser/win/scoped_hstring.h +++ b/shell/browser/win/scoped_hstring.h @@ -33,6 +33,13 @@ class ScopedHString { // Returns string. operator HSTRING() const { return str_; } + // Resets and returns the address for use as an out-parameter. The returned + // HSTRING will be released via WindowsDeleteString on destruction. + HSTRING* Receive() { + Reset(); + return &str_; + } + // Whether there is a string created. bool success() const { return str_; }