feat: custom toast xml and failure reporting for notifications (#25401)

* allow custom toast xml and report failures

* docs

* tests

* don't use namespaces

* lint doesn't like trailing commas

* addressing feedback
This commit is contained in:
bitdisaster 2020-09-29 12:20:10 -07:00 committed by GitHub
parent d2282ac51a
commit b43859f098
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 282 additions and 219 deletions

View file

@ -29,9 +29,9 @@ Returns `Boolean` - Whether or not desktop notifications are supported on the cu
### `new Notification([options])` ### `new Notification([options])`
* `options` Object (optional) * `options` Object (optional)
* `title` String - A title for the notification, which will be shown at the top of the notification window when it is shown. * `title` String (optional) - A title for the notification, which will be shown at the top of the notification window when it is shown.
* `subtitle` String (optional) _macOS_ - A subtitle for the notification, which will be displayed below the title. * `subtitle` String (optional) _macOS_ - A subtitle for the notification, which will be displayed below the title.
* `body` String - The body text of the notification, which will be displayed below the title or subtitle. * `body` String (optional) - The body text of the notification, which will be displayed below the title or subtitle.
* `silent` Boolean (optional) - Whether or not to emit an OS notification noise when showing the notification. * `silent` Boolean (optional) - Whether or not to emit an OS notification noise when showing the notification.
* `icon` (String | [NativeImage](native-image.md)) (optional) - An icon to use in the notification. * `icon` (String | [NativeImage](native-image.md)) (optional) - An icon to use in the notification.
* `hasReply` Boolean (optional) _macOS_ - Whether or not to add an inline reply option to the notification. * `hasReply` Boolean (optional) _macOS_ - Whether or not to add an inline reply option to the notification.
@ -41,6 +41,7 @@ Returns `Boolean` - Whether or not desktop notifications are supported on the cu
* `urgency` String (optional) _Linux_ - The urgency level of the notification. Can be 'normal', 'critical', or 'low'. * `urgency` String (optional) _Linux_ - The urgency level of the notification. Can be 'normal', 'critical', or 'low'.
* `actions` [NotificationAction[]](structures/notification-action.md) (optional) _macOS_ - Actions to add to the notification. Please read the available actions and limitations in the `NotificationAction` documentation. * `actions` [NotificationAction[]](structures/notification-action.md) (optional) _macOS_ - Actions to add to the notification. Please read the available actions and limitations in the `NotificationAction` documentation.
* `closeButtonText` String (optional) _macOS_ - A custom title for the close button of an alert. An empty string will cause the default localized text to be used. * `closeButtonText` String (optional) _macOS_ - A custom title for the close button of an alert. An empty string will cause the default localized text to be used.
* `toastXml` String (optional) _Windows_ - A custom description of the Notification on Windows superseding all properties above. Provides full customization of design and behavior of the notification.
### Instance Events ### Instance Events
@ -94,6 +95,15 @@ Returns:
* `event` Event * `event` Event
* `index` Number - The index of the action that was activated. * `index` Number - The index of the action that was activated.
#### Event: 'failed' _Windows_
Returns:
* `event` Event
* `error` String - The error encountered during execution of the `show()` method.
Emitted when an error is encountered while creating and showing the native notification.
### Instance Methods ### Instance Methods
Objects created with `new Notification` have the following instance methods: Objects created with `new Notification` have the following instance methods:
@ -162,6 +172,10 @@ If `timeoutType` is set to 'never', the notification never expires. It stays ope
A [`NotificationAction[]`](structures/notification-action.md) property representing the actions of the notification. A [`NotificationAction[]`](structures/notification-action.md) property representing the actions of the notification.
#### `notification.toastXml` _Windows_
A `String` property representing the custom Toast XML of the notification.
### Playing Sounds ### Playing Sounds
On macOS, you can specify the name of the sound you'd like to play when the On macOS, you can specify the name of the sound you'd like to play when the

View file

@ -72,6 +72,7 @@ Notification::Notification(gin::Arguments* args) {
opts.Get("actions", &actions_); opts.Get("actions", &actions_);
opts.Get("sound", &sound_); opts.Get("sound", &sound_);
opts.Get("closeButtonText", &close_button_text_); opts.Get("closeButtonText", &close_button_text_);
opts.Get("toastXml", &toast_xml_);
} }
} }
@ -135,6 +136,10 @@ base::string16 Notification::GetCloseButtonText() const {
return close_button_text_; return close_button_text_;
} }
base::string16 Notification::GetToastXml() const {
return toast_xml_;
}
// Setters // Setters
void Notification::SetTitle(const base::string16& new_title) { void Notification::SetTitle(const base::string16& new_title) {
title_ = new_title; title_ = new_title;
@ -181,6 +186,10 @@ void Notification::SetCloseButtonText(const base::string16& text) {
close_button_text_ = text; close_button_text_ = text;
} }
void Notification::SetToastXml(const base::string16& new_toast_xml) {
toast_xml_ = new_toast_xml;
}
void Notification::NotificationAction(int index) { void Notification::NotificationAction(int index) {
Emit("action", index); Emit("action", index);
} }
@ -197,6 +206,10 @@ void Notification::NotificationDisplayed() {
Emit("show"); Emit("show");
} }
void Notification::NotificationFailed(const std::string& error) {
Emit("failed", error);
}
void Notification::NotificationDestroyed() {} void Notification::NotificationDestroyed() {}
void Notification::NotificationClosed() { void Notification::NotificationClosed() {
@ -231,6 +244,7 @@ void Notification::Show() {
options.sound = sound_; options.sound = sound_;
options.close_button_text = close_button_text_; options.close_button_text = close_button_text_;
options.urgency = urgency_; options.urgency = urgency_;
options.toast_xml = toast_xml_;
notification_->Show(options); notification_->Show(options);
} }
} }
@ -265,6 +279,8 @@ v8::Local<v8::ObjectTemplate> Notification::FillObjectTemplate(
&Notification::SetActions) &Notification::SetActions)
.SetProperty("closeButtonText", &Notification::GetCloseButtonText, .SetProperty("closeButtonText", &Notification::GetCloseButtonText,
&Notification::SetCloseButtonText) &Notification::SetCloseButtonText)
.SetProperty("toastXml", &Notification::GetToastXml,
&Notification::SetToastXml)
.Build(); .Build();
} }

View file

@ -52,6 +52,7 @@ class Notification : public gin::Wrappable<Notification>,
void NotificationDisplayed() override; void NotificationDisplayed() override;
void NotificationDestroyed() override; void NotificationDestroyed() override;
void NotificationClosed() override; void NotificationClosed() override;
void NotificationFailed(const std::string& error) override;
// gin::Wrappable // gin::Wrappable
static gin::WrapperInfo kWrapperInfo; static gin::WrapperInfo kWrapperInfo;
@ -75,6 +76,7 @@ class Notification : public gin::Wrappable<Notification>,
base::string16 GetSound() const; base::string16 GetSound() const;
std::vector<electron::NotificationAction> GetActions() const; std::vector<electron::NotificationAction> GetActions() const;
base::string16 GetCloseButtonText() const; base::string16 GetCloseButtonText() const;
base::string16 GetToastXml() const;
// Prop Setters // Prop Setters
void SetTitle(const base::string16& new_title); void SetTitle(const base::string16& new_title);
@ -88,6 +90,7 @@ class Notification : public gin::Wrappable<Notification>,
void SetSound(const base::string16& sound); void SetSound(const base::string16& sound);
void SetActions(const std::vector<electron::NotificationAction>& actions); void SetActions(const std::vector<electron::NotificationAction>& actions);
void SetCloseButtonText(const base::string16& text); void SetCloseButtonText(const base::string16& text);
void SetToastXml(const base::string16& text);
private: private:
base::string16 title_; base::string16 title_;
@ -104,6 +107,7 @@ class Notification : public gin::Wrappable<Notification>,
base::string16 urgency_; base::string16 urgency_;
std::vector<electron::NotificationAction> actions_; std::vector<electron::NotificationAction> actions_;
base::string16 close_button_text_; base::string16 close_button_text_;
base::string16 toast_xml_;
electron::NotificationPresenter* presenter_; electron::NotificationPresenter* presenter_;

View file

@ -33,9 +33,9 @@ void Notification::NotificationDismissed() {
Destroy(); Destroy();
} }
void Notification::NotificationFailed() { void Notification::NotificationFailed(const std::string& error) {
if (delegate()) if (delegate())
delegate()->NotificationFailed(); delegate()->NotificationFailed(error);
Destroy(); Destroy();
} }

View file

@ -38,6 +38,7 @@ struct NotificationOptions {
base::string16 urgency; // Linux base::string16 urgency; // Linux
std::vector<NotificationAction> actions; std::vector<NotificationAction> actions;
base::string16 close_button_text; base::string16 close_button_text;
base::string16 toast_xml;
NotificationOptions(); NotificationOptions();
~NotificationOptions(); ~NotificationOptions();
@ -56,7 +57,7 @@ class Notification {
// Should be called by derived classes. // Should be called by derived classes.
void NotificationClicked(); void NotificationClicked();
void NotificationDismissed(); void NotificationDismissed();
void NotificationFailed(); void NotificationFailed(const std::string& error = "");
// delete this. // delete this.
void Destroy(); void Destroy();

View file

@ -15,7 +15,7 @@ class NotificationDelegate {
virtual void NotificationDestroyed() {} virtual void NotificationDestroyed() {}
// Failed to send the notification. // Failed to send the notification.
virtual void NotificationFailed() {} virtual void NotificationFailed(const std::string& error) {}
// Notification was replied to // Notification was replied to
virtual void NotificationReplied(const std::string& reply) {} virtual void NotificationReplied(const std::string& reply) {}

View file

@ -9,6 +9,7 @@
#include "shell/browser/notifications/win/windows_toast_notification.h" #include "shell/browser/notifications/win/windows_toast_notification.h"
#include <shlobj.h> #include <shlobj.h>
#include <wrl\wrappers\corewrappers.h>
#include <vector> #include <vector>
#include "base/environment.h" #include "base/environment.h"
@ -23,11 +24,33 @@
using ABI::Windows::Data::Xml::Dom::IXmlAttribute; using ABI::Windows::Data::Xml::Dom::IXmlAttribute;
using ABI::Windows::Data::Xml::Dom::IXmlDocument; using ABI::Windows::Data::Xml::Dom::IXmlDocument;
using ABI::Windows::Data::Xml::Dom::IXmlDocumentIO;
using ABI::Windows::Data::Xml::Dom::IXmlElement; using ABI::Windows::Data::Xml::Dom::IXmlElement;
using ABI::Windows::Data::Xml::Dom::IXmlNamedNodeMap; using ABI::Windows::Data::Xml::Dom::IXmlNamedNodeMap;
using ABI::Windows::Data::Xml::Dom::IXmlNode; using ABI::Windows::Data::Xml::Dom::IXmlNode;
using ABI::Windows::Data::Xml::Dom::IXmlNodeList; using ABI::Windows::Data::Xml::Dom::IXmlNodeList;
using ABI::Windows::Data::Xml::Dom::IXmlText; using ABI::Windows::Data::Xml::Dom::IXmlText;
using Microsoft::WRL::Wrappers::HStringReference;
#define RETURN_IF_FAILED(hr) \
do { \
HRESULT _hrTemp = hr; \
if (FAILED(_hrTemp)) { \
return _hrTemp; \
} \
} while (false)
#define REPORT_AND_RETURN_IF_FAILED(hr, msg) \
do { \
HRESULT _hrTemp = hr; \
std::string _msgTemp = msg; \
if (FAILED(_hrTemp)) { \
std::string _err = _msgTemp + ",ERROR " + std::to_string(_hrTemp); \
if (IsDebuggingNotifications()) \
LOG(INFO) << _err; \
Notification::NotificationFailed(_err); \
return _hrTemp; \
} \
} while (false)
namespace electron { namespace electron {
@ -36,7 +59,6 @@ namespace {
bool IsDebuggingNotifications() { bool IsDebuggingNotifications() {
return base::Environment::Create()->HasVar("ELECTRON_DEBUG_NOTIFICATIONS"); return base::Environment::Create()->HasVar("ELECTRON_DEBUG_NOTIFICATIONS");
} }
} // namespace } // namespace
// static // static
@ -83,59 +105,17 @@ WindowsToastNotification::~WindowsToastNotification() {
// Remove the notification on exit. // Remove the notification on exit.
if (toast_notification_) { if (toast_notification_) {
RemoveCallbacks(toast_notification_.Get()); RemoveCallbacks(toast_notification_.Get());
Dismiss();
} }
} }
void WindowsToastNotification::Show(const NotificationOptions& options) { void WindowsToastNotification::Show(const NotificationOptions& options) {
auto* presenter_win = static_cast<NotificationPresenterWin*>(presenter()); if (SUCCEEDED(ShowInternal(options))) {
std::wstring icon_path = if (IsDebuggingNotifications())
presenter_win->SaveIconToFilesystem(options.icon, options.icon_url); LOG(INFO) << "Notification created";
ComPtr<IXmlDocument> toast_xml; if (delegate())
if (FAILED(GetToastXml(toast_manager_.Get(), options.title, options.msg, delegate()->NotificationDisplayed();
icon_path, options.timeout_type, options.silent,
&toast_xml))) {
NotificationFailed();
return;
} }
ScopedHString toast_str(
RuntimeClass_Windows_UI_Notifications_ToastNotification);
if (!toast_str.success()) {
NotificationFailed();
return;
}
ComPtr<ABI::Windows::UI::Notifications::IToastNotificationFactory>
toast_factory;
if (FAILED(Windows::Foundation::GetActivationFactory(toast_str,
&toast_factory))) {
NotificationFailed();
return;
}
if (FAILED(toast_factory->CreateToastNotification(toast_xml.Get(),
&toast_notification_))) {
NotificationFailed();
return;
}
if (!SetupCallbacks(toast_notification_.Get())) {
NotificationFailed();
return;
}
if (FAILED(toast_notifier_->Show(toast_notification_.Get()))) {
NotificationFailed();
return;
}
if (IsDebuggingNotifications())
LOG(INFO) << "Notification created";
if (delegate())
delegate()->NotificationDisplayed();
} }
void WindowsToastNotification::Dismiss() { void WindowsToastNotification::Dismiss() {
@ -144,7 +124,50 @@ void WindowsToastNotification::Dismiss() {
toast_notifier_->Hide(toast_notification_.Get()); toast_notifier_->Hide(toast_notification_.Get());
} }
bool WindowsToastNotification::GetToastXml( HRESULT WindowsToastNotification::ShowInternal(
const NotificationOptions& options) {
ComPtr<IXmlDocument> toast_xml;
// The custom xml takes priority over the preset template.
if (!options.toast_xml.empty()) {
REPORT_AND_RETURN_IF_FAILED(
XmlDocumentFromString(options.toast_xml.c_str(), &toast_xml),
"XML: Invalid XML");
} else {
auto* presenter_win = static_cast<NotificationPresenterWin*>(presenter());
std::wstring icon_path =
presenter_win->SaveIconToFilesystem(options.icon, options.icon_url);
REPORT_AND_RETURN_IF_FAILED(
GetToastXml(toast_manager_.Get(), options.title, options.msg, icon_path,
options.timeout_type, options.silent, &toast_xml),
"XML: Failed to create XML document");
}
ScopedHString toast_str(
RuntimeClass_Windows_UI_Notifications_ToastNotification);
if (!toast_str.success()) {
NotificationFailed("Creating ScopedHString failed");
return E_FAIL;
}
ComPtr<ABI::Windows::UI::Notifications::IToastNotificationFactory>
toast_factory;
REPORT_AND_RETURN_IF_FAILED(
Windows::Foundation::GetActivationFactory(toast_str, &toast_factory),
"WinAPI: GetActivationFactory failed");
REPORT_AND_RETURN_IF_FAILED(toast_factory->CreateToastNotification(
toast_xml.Get(), &toast_notification_),
"WinAPI: CreateToastNotification failed");
REPORT_AND_RETURN_IF_FAILED(SetupCallbacks(toast_notification_.Get()),
"WinAPI: SetupCallbacks failed");
REPORT_AND_RETURN_IF_FAILED(toast_notifier_->Show(toast_notification_.Get()),
"WinAPI: Show failed");
return S_OK;
}
HRESULT WindowsToastNotification::GetToastXml(
ABI::Windows::UI::Notifications::IToastNotificationManagerStatics* ABI::Windows::UI::Notifications::IToastNotificationManagerStatics*
toastManager, toastManager,
const std::wstring& title, const std::wstring& title,
@ -161,10 +184,15 @@ bool WindowsToastNotification::GetToastXml(
? ABI::Windows::UI::Notifications::ToastTemplateType_ToastText01 ? ABI::Windows::UI::Notifications::ToastTemplateType_ToastText01
: ABI::Windows::UI::Notifications:: : ABI::Windows::UI::Notifications::
ToastTemplateType_ToastImageAndText01; ToastTemplateType_ToastImageAndText01;
if (FAILED(toast_manager_->GetTemplateContent(template_type, toast_xml))) REPORT_AND_RETURN_IF_FAILED(
return false; toast_manager_->GetTemplateContent(template_type, toast_xml),
if (!SetXmlText(*toast_xml, title.empty() ? msg : title)) "XML: Fetching XML ToastImageAndText01 template failed");
return false; std::wstring toastMsg = title.empty() ? msg : title;
// we can't create an empty notification
toastMsg = toastMsg.empty() ? L"[no message]" : toastMsg;
REPORT_AND_RETURN_IF_FAILED(
SetXmlText(*toast_xml, toastMsg),
"XML: Filling XML ToastImageAndText01 template failed");
} else { } else {
// Title and body toast. // Title and body toast.
template_type = template_type =
@ -172,284 +200,256 @@ bool WindowsToastNotification::GetToastXml(
? ABI::Windows::UI::Notifications::ToastTemplateType_ToastText02 ? ABI::Windows::UI::Notifications::ToastTemplateType_ToastText02
: ABI::Windows::UI::Notifications:: : ABI::Windows::UI::Notifications::
ToastTemplateType_ToastImageAndText02; ToastTemplateType_ToastImageAndText02;
if (FAILED(toastManager->GetTemplateContent(template_type, toast_xml))) { REPORT_AND_RETURN_IF_FAILED(
if (IsDebuggingNotifications()) toastManager->GetTemplateContent(template_type, toast_xml),
LOG(INFO) << "Fetching XML template failed"; "XML: Fetching XML ToastImageAndText02 template failed");
return false; REPORT_AND_RETURN_IF_FAILED(
} SetXmlText(*toast_xml, title, msg),
"XML: Filling XML ToastImageAndText02 template failed");
if (!SetXmlText(*toast_xml, title, msg)) {
if (IsDebuggingNotifications())
LOG(INFO) << "Setting text fields on template failed";
return false;
}
} }
// Configure the toast's timeout settings // Configure the toast's timeout settings
if (timeout_type == base::ASCIIToUTF16("never")) { if (timeout_type == base::ASCIIToUTF16("never")) {
if (FAILED(SetXmlScenarioReminder(*toast_xml))) { REPORT_AND_RETURN_IF_FAILED(
if (IsDebuggingNotifications()) (SetXmlScenarioReminder(*toast_xml)),
LOG(INFO) << "Setting \"scenario\" option on notification failed"; "XML: Setting \"scenario\" option on notification failed");
return false;
}
} }
// Configure the toast's notification sound // Configure the toast's notification sound
if (silent) { if (silent) {
if (FAILED(SetXmlAudioSilent(*toast_xml))) { REPORT_AND_RETURN_IF_FAILED(
if (IsDebuggingNotifications()) { SetXmlAudioSilent(*toast_xml),
LOG(INFO) << "Setting \"silent\" option on notification failed"; "XML: Setting \"silent\" option on notification failed");
}
return false;
}
} }
// Configure the toast's image // Configure the toast's image
if (!icon_path.empty()) if (!icon_path.empty()) {
return SetXmlImage(*toast_xml, icon_path); REPORT_AND_RETURN_IF_FAILED(
SetXmlImage(*toast_xml, icon_path),
"XML: Setting \"icon\" option on notification failed");
}
return true; return S_OK;
} }
bool WindowsToastNotification::SetXmlScenarioReminder(IXmlDocument* doc) { HRESULT WindowsToastNotification::SetXmlScenarioReminder(IXmlDocument* doc) {
ScopedHString tag(L"toast"); ScopedHString tag(L"toast");
if (!tag.success()) if (!tag.success())
return false; return false;
ComPtr<IXmlNodeList> node_list; ComPtr<IXmlNodeList> node_list;
if (FAILED(doc->GetElementsByTagName(tag, &node_list))) RETURN_IF_FAILED(doc->GetElementsByTagName(tag, &node_list));
return false;
// Check that root "toast" node exists // Check that root "toast" node exists
ComPtr<IXmlNode> root; ComPtr<IXmlNode> root;
if (FAILED(node_list->Item(0, &root))) RETURN_IF_FAILED(node_list->Item(0, &root));
return false;
// get attributes of root "toast" node // get attributes of root "toast" node
ComPtr<IXmlNamedNodeMap> attributes; ComPtr<IXmlNamedNodeMap> attributes;
if (FAILED(root->get_Attributes(&attributes))) RETURN_IF_FAILED(root->get_Attributes(&attributes));
return false;
ComPtr<IXmlAttribute> scenario_attribute; ComPtr<IXmlAttribute> scenario_attribute;
ScopedHString scenario_str(L"scenario"); ScopedHString scenario_str(L"scenario");
if (FAILED(doc->CreateAttribute(scenario_str, &scenario_attribute))) RETURN_IF_FAILED(doc->CreateAttribute(scenario_str, &scenario_attribute));
return false;
ComPtr<IXmlNode> scenario_attribute_node; ComPtr<IXmlNode> scenario_attribute_node;
if (FAILED(scenario_attribute.As(&scenario_attribute_node))) RETURN_IF_FAILED(scenario_attribute.As(&scenario_attribute_node));
return false;
ScopedHString scenario_value(L"reminder"); ScopedHString scenario_value(L"reminder");
if (!scenario_value.success()) if (!scenario_value.success())
return false; return E_FAIL;
ComPtr<IXmlText> scenario_text; ComPtr<IXmlText> scenario_text;
if (FAILED(doc->CreateTextNode(scenario_value, &scenario_text))) RETURN_IF_FAILED(doc->CreateTextNode(scenario_value, &scenario_text));
return false;
ComPtr<IXmlNode> scenario_node; ComPtr<IXmlNode> scenario_node;
if (FAILED(scenario_text.As(&scenario_node))) RETURN_IF_FAILED(scenario_text.As(&scenario_node));
return false;
ComPtr<IXmlNode> child_node; ComPtr<IXmlNode> child_node;
if (FAILED(scenario_attribute_node->AppendChild(scenario_node.Get(), RETURN_IF_FAILED(
&child_node))) scenario_attribute_node->AppendChild(scenario_node.Get(), &child_node));
return false;
ComPtr<IXmlNode> scenario_attribute_pnode; ComPtr<IXmlNode> scenario_attribute_pnode;
return SUCCEEDED(attributes.Get()->SetNamedItem(scenario_attribute_node.Get(), return attributes.Get()->SetNamedItem(scenario_attribute_node.Get(),
&scenario_attribute_pnode)); &scenario_attribute_pnode);
} }
bool WindowsToastNotification::SetXmlAudioSilent(IXmlDocument* doc) { HRESULT WindowsToastNotification::SetXmlAudioSilent(IXmlDocument* doc) {
ScopedHString tag(L"toast"); ScopedHString tag(L"toast");
if (!tag.success()) if (!tag.success())
return false; return E_FAIL;
ComPtr<IXmlNodeList> node_list; ComPtr<IXmlNodeList> node_list;
if (FAILED(doc->GetElementsByTagName(tag, &node_list))) RETURN_IF_FAILED(doc->GetElementsByTagName(tag, &node_list));
return false;
ComPtr<IXmlNode> root; ComPtr<IXmlNode> root;
if (FAILED(node_list->Item(0, &root))) RETURN_IF_FAILED(node_list->Item(0, &root));
return false;
ComPtr<IXmlElement> audio_element; ComPtr<IXmlElement> audio_element;
ScopedHString audio_str(L"audio"); ScopedHString audio_str(L"audio");
if (FAILED(doc->CreateElement(audio_str, &audio_element))) RETURN_IF_FAILED(doc->CreateElement(audio_str, &audio_element));
return false;
ComPtr<IXmlNode> audio_node_tmp; ComPtr<IXmlNode> audio_node_tmp;
if (FAILED(audio_element.As(&audio_node_tmp))) RETURN_IF_FAILED(audio_element.As(&audio_node_tmp));
return false;
// Append audio node to toast xml // Append audio node to toast xml
ComPtr<IXmlNode> audio_node; ComPtr<IXmlNode> audio_node;
if (FAILED(root->AppendChild(audio_node_tmp.Get(), &audio_node))) RETURN_IF_FAILED(root->AppendChild(audio_node_tmp.Get(), &audio_node));
return false;
// Create silent attribute // Create silent attribute
ComPtr<IXmlNamedNodeMap> attributes; ComPtr<IXmlNamedNodeMap> attributes;
if (FAILED(audio_node->get_Attributes(&attributes))) RETURN_IF_FAILED(audio_node->get_Attributes(&attributes));
return false;
ComPtr<IXmlAttribute> silent_attribute; ComPtr<IXmlAttribute> silent_attribute;
ScopedHString silent_str(L"silent"); ScopedHString silent_str(L"silent");
if (FAILED(doc->CreateAttribute(silent_str, &silent_attribute))) RETURN_IF_FAILED(doc->CreateAttribute(silent_str, &silent_attribute));
return false;
ComPtr<IXmlNode> silent_attribute_node; ComPtr<IXmlNode> silent_attribute_node;
if (FAILED(silent_attribute.As(&silent_attribute_node))) RETURN_IF_FAILED(silent_attribute.As(&silent_attribute_node));
return false;
// Set silent attribute to true // Set silent attribute to true
ScopedHString silent_value(L"true"); ScopedHString silent_value(L"true");
if (!silent_value.success()) if (!silent_value.success())
return false; return E_FAIL;
ComPtr<IXmlText> silent_text; ComPtr<IXmlText> silent_text;
if (FAILED(doc->CreateTextNode(silent_value, &silent_text))) RETURN_IF_FAILED(doc->CreateTextNode(silent_value, &silent_text));
return false;
ComPtr<IXmlNode> silent_node; ComPtr<IXmlNode> silent_node;
if (FAILED(silent_text.As(&silent_node))) RETURN_IF_FAILED(silent_text.As(&silent_node));
return false;
ComPtr<IXmlNode> child_node; ComPtr<IXmlNode> child_node;
if (FAILED( RETURN_IF_FAILED(
silent_attribute_node->AppendChild(silent_node.Get(), &child_node))) silent_attribute_node->AppendChild(silent_node.Get(), &child_node));
return false;
ComPtr<IXmlNode> silent_attribute_pnode; ComPtr<IXmlNode> silent_attribute_pnode;
return SUCCEEDED(attributes.Get()->SetNamedItem(silent_attribute_node.Get(), return attributes.Get()->SetNamedItem(silent_attribute_node.Get(),
&silent_attribute_pnode)); &silent_attribute_pnode);
} }
bool WindowsToastNotification::SetXmlText(IXmlDocument* doc, HRESULT WindowsToastNotification::SetXmlText(IXmlDocument* doc,
const std::wstring& text) { const std::wstring& text) {
ScopedHString tag; ScopedHString tag;
ComPtr<IXmlNodeList> node_list; ComPtr<IXmlNodeList> node_list;
if (!GetTextNodeList(&tag, doc, &node_list, 1)) RETURN_IF_FAILED(GetTextNodeList(&tag, doc, &node_list, 1));
return false;
ComPtr<IXmlNode> node; ComPtr<IXmlNode> node;
if (FAILED(node_list->Item(0, &node))) RETURN_IF_FAILED(node_list->Item(0, &node));
return false;
return AppendTextToXml(doc, node.Get(), text); return AppendTextToXml(doc, node.Get(), text);
} }
bool WindowsToastNotification::SetXmlText(IXmlDocument* doc, HRESULT WindowsToastNotification::SetXmlText(IXmlDocument* doc,
const std::wstring& title, const std::wstring& title,
const std::wstring& body) { const std::wstring& body) {
ScopedHString tag; ScopedHString tag;
ComPtr<IXmlNodeList> node_list; ComPtr<IXmlNodeList> node_list;
if (!GetTextNodeList(&tag, doc, &node_list, 2)) RETURN_IF_FAILED(GetTextNodeList(&tag, doc, &node_list, 2));
return false;
ComPtr<IXmlNode> node; ComPtr<IXmlNode> node;
if (FAILED(node_list->Item(0, &node))) RETURN_IF_FAILED(node_list->Item(0, &node));
return false; RETURN_IF_FAILED(AppendTextToXml(doc, node.Get(), title));
RETURN_IF_FAILED(node_list->Item(1, &node));
if (!AppendTextToXml(doc, node.Get(), title))
return false;
if (FAILED(node_list->Item(1, &node)))
return false;
return AppendTextToXml(doc, node.Get(), body); return AppendTextToXml(doc, node.Get(), body);
} }
bool WindowsToastNotification::SetXmlImage(IXmlDocument* doc, HRESULT WindowsToastNotification::SetXmlImage(IXmlDocument* doc,
const std::wstring& icon_path) { const std::wstring& icon_path) {
ScopedHString tag(L"image"); ScopedHString tag(L"image");
if (!tag.success()) if (!tag.success())
return false; return E_FAIL;
ComPtr<IXmlNodeList> node_list; ComPtr<IXmlNodeList> node_list;
if (FAILED(doc->GetElementsByTagName(tag, &node_list))) RETURN_IF_FAILED(doc->GetElementsByTagName(tag, &node_list));
return false;
ComPtr<IXmlNode> image_node; ComPtr<IXmlNode> image_node;
if (FAILED(node_list->Item(0, &image_node))) RETURN_IF_FAILED(node_list->Item(0, &image_node));
return false;
ComPtr<IXmlNamedNodeMap> attrs; ComPtr<IXmlNamedNodeMap> attrs;
if (FAILED(image_node->get_Attributes(&attrs))) RETURN_IF_FAILED(image_node->get_Attributes(&attrs));
return false;
ScopedHString src(L"src"); ScopedHString src(L"src");
if (!src.success()) if (!src.success())
return false; return E_FAIL;
ComPtr<IXmlNode> src_attr; ComPtr<IXmlNode> src_attr;
if (FAILED(attrs->GetNamedItem(src, &src_attr))) RETURN_IF_FAILED(attrs->GetNamedItem(src, &src_attr));
return false;
ScopedHString img_path(icon_path.c_str()); ScopedHString img_path(icon_path.c_str());
if (!img_path.success()) if (!img_path.success())
return false; return E_FAIL;
ComPtr<IXmlText> src_text; ComPtr<IXmlText> src_text;
if (FAILED(doc->CreateTextNode(img_path, &src_text))) RETURN_IF_FAILED(doc->CreateTextNode(img_path, &src_text));
return false;
ComPtr<IXmlNode> src_node; ComPtr<IXmlNode> src_node;
if (FAILED(src_text.As(&src_node))) RETURN_IF_FAILED(src_text.As(&src_node));
return false;
ComPtr<IXmlNode> child_node; ComPtr<IXmlNode> child_node;
return SUCCEEDED(src_attr->AppendChild(src_node.Get(), &child_node)); return src_attr->AppendChild(src_node.Get(), &child_node);
} }
bool WindowsToastNotification::GetTextNodeList(ScopedHString* tag, HRESULT WindowsToastNotification::GetTextNodeList(ScopedHString* tag,
IXmlDocument* doc, IXmlDocument* doc,
IXmlNodeList** node_list, IXmlNodeList** node_list,
uint32_t req_length) { uint32_t req_length) {
tag->Reset(L"text"); tag->Reset(L"text");
if (!tag->success()) if (!tag->success())
return false; return E_FAIL;
if (FAILED(doc->GetElementsByTagName(*tag, node_list))) RETURN_IF_FAILED(doc->GetElementsByTagName(*tag, node_list));
return false;
uint32_t node_length; uint32_t node_length;
if (FAILED((*node_list)->get_Length(&node_length))) RETURN_IF_FAILED((*node_list)->get_Length(&node_length));
return false;
return node_length >= req_length; return node_length >= req_length;
} }
bool WindowsToastNotification::AppendTextToXml(IXmlDocument* doc, HRESULT WindowsToastNotification::AppendTextToXml(IXmlDocument* doc,
IXmlNode* node, IXmlNode* node,
const std::wstring& text) { const std::wstring& text) {
ScopedHString str(text); ScopedHString str(text);
if (!str.success()) if (!str.success())
return false; return E_FAIL;
ComPtr<IXmlText> xml_text; ComPtr<IXmlText> xml_text;
if (FAILED(doc->CreateTextNode(str, &xml_text))) RETURN_IF_FAILED(doc->CreateTextNode(str, &xml_text));
return false;
ComPtr<IXmlNode> text_node; ComPtr<IXmlNode> text_node;
if (FAILED(xml_text.As(&text_node))) RETURN_IF_FAILED(xml_text.As(&text_node));
return false;
ComPtr<IXmlNode> append_node; ComPtr<IXmlNode> append_node;
return SUCCEEDED(node->AppendChild(text_node.Get(), &append_node)); RETURN_IF_FAILED(node->AppendChild(text_node.Get(), &append_node));
return S_OK;
} }
bool WindowsToastNotification::SetupCallbacks( HRESULT WindowsToastNotification::XmlDocumentFromString(
const wchar_t* xmlString,
IXmlDocument** doc) {
ComPtr<IXmlDocument> xmlDoc;
RETURN_IF_FAILED(Windows::Foundation::ActivateInstance(
HStringReference(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument).Get(),
&xmlDoc));
ComPtr<IXmlDocumentIO> docIO;
RETURN_IF_FAILED(xmlDoc.As(&docIO));
RETURN_IF_FAILED(docIO->LoadXml(HStringReference(xmlString).Get()));
return xmlDoc.CopyTo(doc);
}
HRESULT WindowsToastNotification::SetupCallbacks(
ABI::Windows::UI::Notifications::IToastNotification* toast) { ABI::Windows::UI::Notifications::IToastNotification* toast) {
event_handler_ = Make<ToastEventHandler>(this); event_handler_ = Make<ToastEventHandler>(this);
if (FAILED(toast->add_Activated(event_handler_.Get(), &activated_token_))) RETURN_IF_FAILED(
return false; toast->add_Activated(event_handler_.Get(), &activated_token_));
RETURN_IF_FAILED(
if (FAILED(toast->add_Dismissed(event_handler_.Get(), &dismissed_token_))) toast->add_Dismissed(event_handler_.Get(), &dismissed_token_));
return false; RETURN_IF_FAILED(toast->add_Failed(event_handler_.Get(), &failed_token_));
return S_OK;
return SUCCEEDED(toast->add_Failed(event_handler_.Get(), &failed_token_));
} }
bool WindowsToastNotification::RemoveCallbacks( bool WindowsToastNotification::RemoveCallbacks(
@ -498,11 +498,15 @@ IFACEMETHODIMP ToastEventHandler::Invoke(
IFACEMETHODIMP ToastEventHandler::Invoke( IFACEMETHODIMP ToastEventHandler::Invoke(
ABI::Windows::UI::Notifications::IToastNotification* sender, ABI::Windows::UI::Notifications::IToastNotification* sender,
ABI::Windows::UI::Notifications::IToastFailedEventArgs* e) { ABI::Windows::UI::Notifications::IToastFailedEventArgs* e) {
base::PostTask( HRESULT error;
FROM_HERE, {content::BrowserThread::UI}, e->get_ErrorCode(&error);
base::BindOnce(&Notification::NotificationFailed, notification_)); std::string errorMessage =
"Notification failed. HRESULT:" + std::to_string(error);
base::PostTask(FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&Notification::NotificationFailed,
notification_, errorMessage));
if (IsDebuggingNotifications()) if (IsDebuggingNotifications())
LOG(INFO) << "Notification failed"; LOG(INFO) << errorMessage;
return S_OK; return S_OK;
} }

View file

@ -57,7 +57,8 @@ class WindowsToastNotification : public Notification {
private: private:
friend class ToastEventHandler; friend class ToastEventHandler;
bool GetToastXml( HRESULT ShowInternal(const NotificationOptions& options);
HRESULT GetToastXml(
ABI::Windows::UI::Notifications::IToastNotificationManagerStatics* ABI::Windows::UI::Notifications::IToastNotificationManagerStatics*
toastManager, toastManager,
const std::wstring& title, const std::wstring& title,
@ -66,23 +67,27 @@ class WindowsToastNotification : public Notification {
const std::wstring& timeout_type, const std::wstring& timeout_type,
const bool silent, const bool silent,
ABI::Windows::Data::Xml::Dom::IXmlDocument** toastXml); ABI::Windows::Data::Xml::Dom::IXmlDocument** toastXml);
bool SetXmlAudioSilent(ABI::Windows::Data::Xml::Dom::IXmlDocument* doc); HRESULT SetXmlAudioSilent(ABI::Windows::Data::Xml::Dom::IXmlDocument* doc);
bool SetXmlScenarioReminder(ABI::Windows::Data::Xml::Dom::IXmlDocument* doc); HRESULT SetXmlScenarioReminder(
bool SetXmlText(ABI::Windows::Data::Xml::Dom::IXmlDocument* doc, ABI::Windows::Data::Xml::Dom::IXmlDocument* doc);
const std::wstring& text); HRESULT SetXmlText(ABI::Windows::Data::Xml::Dom::IXmlDocument* doc,
bool SetXmlText(ABI::Windows::Data::Xml::Dom::IXmlDocument* doc, const std::wstring& text);
const std::wstring& title, HRESULT SetXmlText(ABI::Windows::Data::Xml::Dom::IXmlDocument* doc,
const std::wstring& body); const std::wstring& title,
bool SetXmlImage(ABI::Windows::Data::Xml::Dom::IXmlDocument* doc, const std::wstring& body);
const std::wstring& icon_path); HRESULT SetXmlImage(ABI::Windows::Data::Xml::Dom::IXmlDocument* doc,
bool GetTextNodeList(ScopedHString* tag, const std::wstring& icon_path);
ABI::Windows::Data::Xml::Dom::IXmlDocument* doc, HRESULT GetTextNodeList(ScopedHString* tag,
ABI::Windows::Data::Xml::Dom::IXmlNodeList** nodeList, ABI::Windows::Data::Xml::Dom::IXmlDocument* doc,
uint32_t reqLength); ABI::Windows::Data::Xml::Dom::IXmlNodeList** nodeList,
bool AppendTextToXml(ABI::Windows::Data::Xml::Dom::IXmlDocument* doc, uint32_t reqLength);
ABI::Windows::Data::Xml::Dom::IXmlNode* node, HRESULT AppendTextToXml(ABI::Windows::Data::Xml::Dom::IXmlDocument* doc,
const std::wstring& text); ABI::Windows::Data::Xml::Dom::IXmlNode* node,
bool SetupCallbacks( const std::wstring& text);
HRESULT XmlDocumentFromString(
const wchar_t* xmlString,
ABI::Windows::Data::Xml::Dom::IXmlDocument** doc);
HRESULT SetupCallbacks(
ABI::Windows::UI::Notifications::IToastNotification* toast); ABI::Windows::UI::Notifications::IToastNotification* toast);
bool RemoveCallbacks( bool RemoveCallbacks(
ABI::Windows::UI::Notifications::IToastNotification* toast); ABI::Windows::UI::Notifications::IToastNotification* toast);

View file

@ -108,6 +108,14 @@ describe('Notification module', () => {
n.close(); n.close();
}); });
ifit(process.platform === 'win32')('inits, gets and sets custom xml', () => {
const n = new Notification({
toastXml: '<xml/>'
});
expect(n.toastXml).to.equal('<xml/>');
});
ifit(process.platform === 'darwin')('emits show and close events', async () => { ifit(process.platform === 'darwin')('emits show and close events', async () => {
const n = new Notification({ const n = new Notification({
title: 'test notification', title: 'test notification',
@ -126,5 +134,16 @@ describe('Notification module', () => {
} }
}); });
ifit(process.platform === 'win32')('emits failed event', async () => {
const n = new Notification({
toastXml: 'not xml'
});
{
const e = emittedOnce(n, 'failed');
n.show();
await e;
}
});
// TODO(sethlu): Find way to test init with notification icon? // TODO(sethlu): Find way to test init with notification icon?
}); });