mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
* feat: support notification priority on Windows Add Windows notifications support urgency/priority levels. This maps the existing `urgency` option (previously Linux-only) to Windows toast notification priorities: - 'critical' maps to ToastNotificationPriority_High, which sorts the notification above default-priority items in Action Center. - 'normal' and 'low' both map to ToastNotificationPriority_Default. Note that on Windows, 'critical' priority does not prevent the toast from being auto-dismissed. Users should additionally set `timeoutType` to 'never' for that behavior. Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com> * chore: make linter happy Co-authored-by: Charles Kerr <charles@charleskerr.com> --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com> Co-authored-by: Charles Kerr <charles@charleskerr.com>
863 lines
32 KiB
C++
863 lines
32 KiB
C++
// Copyright (c) 2015 Felix Rieseberg <feriese@microsoft.com> and Jason Poon
|
|
// <jason.poon@microsoft.com>. All rights reserved.
|
|
// Copyright (c) 2015 Ryan McShane <rmcshane@bandwidth.com> and Brandon Smith
|
|
// <bsmith@bandwidth.com>
|
|
// Thanks to both of those folks mentioned above who first thought up a bunch of
|
|
// this code
|
|
// and released it as MIT to the world.
|
|
|
|
#include "shell/browser/notifications/win/windows_toast_notification.h"
|
|
|
|
#include <string_view>
|
|
|
|
#include <shlobj.h>
|
|
#include <wrl\wrappers\corewrappers.h>
|
|
|
|
#include "base/containers/fixed_flat_map.h"
|
|
#include "base/hash/hash.h"
|
|
#include "base/logging.h"
|
|
#include "base/no_destructor.h"
|
|
#include "base/strings/strcat.h"
|
|
#include "base/strings/string_number_conversions.h"
|
|
#include "base/strings/string_util.h"
|
|
#include "base/strings/string_util_win.h"
|
|
#include "base/strings/utf_string_conversions.h"
|
|
#include "base/task/thread_pool.h"
|
|
#include "content/public/browser/browser_task_traits.h"
|
|
#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/win/scoped_hstring.h"
|
|
#include "shell/common/application_info.h"
|
|
#include "third_party/libxml/chromium/xml_writer.h"
|
|
#include "ui/base/l10n/l10n_util_win.h"
|
|
#include "ui/strings/grit/ui_strings.h"
|
|
|
|
using ABI::Windows::Data::Xml::Dom::IXmlDocument;
|
|
using ABI::Windows::Data::Xml::Dom::IXmlDocumentIO;
|
|
using Microsoft::WRL::Wrappers::HStringReference;
|
|
|
|
namespace winui = ABI::Windows::UI;
|
|
|
|
#define RETURN_IF_FAILED(hr) \
|
|
do { \
|
|
if (const HRESULT _hrTemp = hr; FAILED(_hrTemp)) { \
|
|
return _hrTemp; \
|
|
} \
|
|
} while (false)
|
|
|
|
#define REPORT_AND_RETURN_IF_FAILED(hr, msg) \
|
|
do { \
|
|
if (const HRESULT _hrTemp = hr; FAILED(_hrTemp)) { \
|
|
std::string _err = \
|
|
base::StrCat({msg, ", ERROR ", FailureResultToString(_hrTemp)}); \
|
|
DebugLog(_err); \
|
|
Notification::NotificationFailed(_err); \
|
|
return _hrTemp; \
|
|
} \
|
|
} while (false)
|
|
|
|
namespace electron {
|
|
|
|
namespace {
|
|
|
|
// This string needs to be max 16 characters to work on Windows 10 prior to
|
|
// applying Creators Update (build 15063).
|
|
constexpr wchar_t kGroup[] = L"Notifications";
|
|
|
|
void DebugLog(std::string_view log_msg) {
|
|
if (electron::debug_notifications)
|
|
LOG(INFO) << log_msg;
|
|
}
|
|
|
|
std::wstring GetTag(const std::string_view notification_id) {
|
|
return base::NumberToWString(base::FastHash(notification_id));
|
|
}
|
|
|
|
// See https://www.hresult.info for HRESULT error codes.
|
|
const std::string FailureResultToString(HRESULT failure_reason) {
|
|
static constexpr auto kFailureMessages = base::MakeFixedFlatMap<
|
|
HRESULT, std::string_view>(
|
|
{{-2143420143, "Settings prevent the notification from being delivered."},
|
|
{-2143420142,
|
|
"Application capabilities prevent the notification from being "
|
|
"delivered."},
|
|
{-2143420140,
|
|
"Settings prevent the notification type from being delivered."},
|
|
{-2143420139, "The size of the notification content is too large."},
|
|
{-2143420138, "The size of the notification tag is too large."},
|
|
{-2143420155, "The notification platform is unavailable."},
|
|
{-2143420154, "The notification has already been posted."},
|
|
{-2143420153, "The notification has already been hidden."},
|
|
{-2143420128,
|
|
"The size of the developer id for scheduled notification is too "
|
|
"large."},
|
|
{-2143420118, "The notification tag is not alphanumeric."},
|
|
{-2143419897,
|
|
"Toast Notification was dropped without being displayed to the user."},
|
|
{-2143419896,
|
|
"The notification platform does not have the proper privileges to "
|
|
"complete the request."}});
|
|
|
|
std::string hresult_str = base::StrCat(
|
|
{" (HRESULT: ", base::NumberToString(static_cast<long>(failure_reason)),
|
|
")"});
|
|
|
|
if (const auto it = kFailureMessages.find(failure_reason);
|
|
it != kFailureMessages.end()) {
|
|
return base::StrCat({"Notification failed - ", it->second, hresult_str});
|
|
}
|
|
|
|
return hresult_str;
|
|
}
|
|
|
|
constexpr char kPlaceholderContent[] = "placeHolderContent";
|
|
constexpr char kContent[] = "content";
|
|
constexpr char kToast[] = "toast";
|
|
constexpr char kVisual[] = "visual";
|
|
constexpr char kBinding[] = "binding";
|
|
constexpr char kTemplate[] = "template";
|
|
constexpr char kToastTemplate[] = "ToastGeneric";
|
|
constexpr char kText[] = "text";
|
|
constexpr char kImage[] = "image";
|
|
constexpr char kPlacement[] = "placement";
|
|
constexpr char kAppLogoOverride[] = "appLogoOverride";
|
|
constexpr char kHintCrop[] = "hint-crop";
|
|
constexpr char kHintInputId[] = "hint-inputId";
|
|
constexpr char kHintCropNone[] = "none";
|
|
constexpr char kSrc[] = "src";
|
|
constexpr char kAudio[] = "audio";
|
|
constexpr char kSilent[] = "silent";
|
|
constexpr char kReply[] = "reply";
|
|
constexpr char kTrue[] = "true";
|
|
constexpr char kID[] = "id";
|
|
constexpr char kInput[] = "input";
|
|
constexpr char kType[] = "type";
|
|
constexpr char kSelection[] = "selection";
|
|
constexpr char kScenario[] = "scenario";
|
|
constexpr char kReminder[] = "reminder";
|
|
constexpr char kActions[] = "actions";
|
|
constexpr char kAction[] = "action";
|
|
constexpr char kActivationType[] = "activationType";
|
|
constexpr char kActivationTypeForeground[] = "foreground";
|
|
constexpr char kActivationTypeSystem[] = "system";
|
|
constexpr char kArguments[] = "arguments";
|
|
constexpr char kDismiss[] = "dismiss";
|
|
// The XML version header that has to be stripped from the output.
|
|
constexpr char kXmlVersionHeader[] = "<?xml version=\"1.0\"?>\n";
|
|
|
|
} // namespace
|
|
|
|
// static
|
|
ComPtr<winui::Notifications::IToastNotificationManagerStatics>*
|
|
WindowsToastNotification::toast_manager_ = nullptr;
|
|
|
|
// static
|
|
ComPtr<winui::Notifications::IToastNotifier>*
|
|
WindowsToastNotification::toast_notifier_ = nullptr;
|
|
|
|
// static
|
|
scoped_refptr<base::SequencedTaskRunner>
|
|
WindowsToastNotification::GetToastTaskRunner() {
|
|
// Use function-local static to avoid exit-time destructor warning
|
|
static base::NoDestructor<scoped_refptr<base::SequencedTaskRunner>>
|
|
task_runner(base::ThreadPool::CreateSequencedTaskRunner(
|
|
{base::TaskPriority::USER_BLOCKING,
|
|
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN,
|
|
base::MayBlock()}));
|
|
return *task_runner;
|
|
}
|
|
|
|
bool WindowsToastNotification::Initialize() {
|
|
HRESULT hr = Windows::Foundation::Initialize(RO_INIT_SINGLETHREADED);
|
|
if (FAILED(hr))
|
|
Windows::Foundation::Initialize(RO_INIT_MULTITHREADED);
|
|
|
|
ScopedHString toast_manager_str(
|
|
RuntimeClass_Windows_UI_Notifications_ToastNotificationManager);
|
|
if (!toast_manager_str.success())
|
|
return false;
|
|
|
|
if (!toast_manager_) {
|
|
toast_manager_ = new ComPtr<
|
|
ABI::Windows::UI::Notifications::IToastNotificationManagerStatics>();
|
|
}
|
|
|
|
if (FAILED(Windows::Foundation::GetActivationFactory(
|
|
toast_manager_str, toast_manager_->GetAddressOf())))
|
|
return false;
|
|
|
|
if (!toast_notifier_) {
|
|
toast_notifier_ =
|
|
new ComPtr<ABI::Windows::UI::Notifications::IToastNotifier>();
|
|
}
|
|
|
|
if (IsRunningInDesktopBridge()) {
|
|
// Ironically, the Desktop Bridge / UWP environment
|
|
// requires us to not give Windows an appUserModelId.
|
|
return SUCCEEDED(
|
|
(*toast_manager_)
|
|
->CreateToastNotifier(toast_notifier_->GetAddressOf()));
|
|
} else {
|
|
ScopedHString app_id;
|
|
if (!GetAppUserModelID(&app_id))
|
|
return false;
|
|
|
|
return SUCCEEDED((*toast_manager_)
|
|
->CreateToastNotifierWithId(
|
|
app_id, toast_notifier_->GetAddressOf()));
|
|
}
|
|
}
|
|
|
|
WindowsToastNotification::WindowsToastNotification(
|
|
NotificationDelegate* delegate,
|
|
NotificationPresenter* presenter)
|
|
: Notification(delegate, presenter) {}
|
|
|
|
WindowsToastNotification::~WindowsToastNotification() {
|
|
// Remove the notification on exit.
|
|
if (toast_notification_) {
|
|
RemoveCallbacks(toast_notification_.Get());
|
|
}
|
|
}
|
|
|
|
// This method posts a request onto the toast background thread, which
|
|
// creates the toast xml then posts notification creation to the UI thread. This
|
|
// avoids blocking the UI for expensive XML parsing and COM initialization or
|
|
// the COM server becoming unavailable. All needed fields are captured before
|
|
// posting the task.
|
|
// The method will eventually result in a display or failure signal being posted
|
|
// back to the UI thread, where further callbacks (clicked, dismissed, failed)
|
|
// are handled by ToastEventHandler.
|
|
void WindowsToastNotification::Show(const NotificationOptions& options) {
|
|
DebugLog("WindowsToastNotification::Show called");
|
|
DebugLog(base::StrCat(
|
|
{"toast_xml empty: ", options.toast_xml.empty() ? "yes" : "no"}));
|
|
if (!options.toast_xml.empty()) {
|
|
DebugLog(base::StrCat({"toast_xml length: ",
|
|
base::NumberToString(options.toast_xml.length())}));
|
|
}
|
|
|
|
// Capture all needed data on UI thread before posting to background thread
|
|
std::string notif_id = notification_id();
|
|
NotificationPresenter* presenter_ptr = presenter();
|
|
base::WeakPtr<Notification> weak_this = GetWeakPtr();
|
|
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner =
|
|
content::GetUIThreadTaskRunner({});
|
|
|
|
DebugLog("Posting task to background thread");
|
|
auto task_runner = GetToastTaskRunner();
|
|
DebugLog(base::StrCat({"Task runner valid: ", task_runner ? "yes" : "no"}));
|
|
|
|
// Post Show operation to background thread to prevent blocking
|
|
// This is the main entry point for the notification creation process
|
|
bool posted = task_runner->PostTask(
|
|
FROM_HERE,
|
|
base::BindOnce(
|
|
&WindowsToastNotification::CreateToastNotificationOnBackgroundThread,
|
|
options, presenter_ptr, notif_id, weak_this, ui_task_runner));
|
|
DebugLog(base::StrCat(
|
|
{"Task posted to background thread: ", posted ? "yes" : "no"}));
|
|
}
|
|
|
|
// Creates the toast XML on the background thread. If the XML is invalid, posts
|
|
// a failure event back to the UI thread. Otherwise, continues to create the
|
|
// toast notification on the background thread.
|
|
void WindowsToastNotification::CreateToastNotificationOnBackgroundThread(
|
|
const NotificationOptions& options,
|
|
NotificationPresenter* presenter,
|
|
const std::string& notification_id,
|
|
base::WeakPtr<Notification> weak_notification,
|
|
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner) {
|
|
DebugLog("CreateToastXmlOnBackgroundThread called");
|
|
ComPtr<IXmlDocument> toast_xml;
|
|
|
|
if (!CreateToastXmlDocument(options, presenter, notification_id,
|
|
weak_notification, ui_task_runner, &toast_xml)) {
|
|
return; // Error already posted to UI thread
|
|
}
|
|
|
|
// Continue to create the toast notification
|
|
ComPtr<ABI::Windows::UI::Notifications::IToastNotification>
|
|
toast_notification;
|
|
if (!CreateToastNotification(toast_xml, options, notification_id,
|
|
weak_notification, ui_task_runner,
|
|
&toast_notification)) {
|
|
return; // Error already posted to UI thread
|
|
}
|
|
|
|
// Setup callbacks and show on UI thread (Show must be called on UI thread)
|
|
scoped_refptr<base::SingleThreadTaskRunner> ui_runner =
|
|
content::GetUIThreadTaskRunner({});
|
|
ui_runner->PostTask(
|
|
FROM_HERE,
|
|
base::BindOnce(&WindowsToastNotification::SetupAndShowOnUIThread,
|
|
weak_notification, toast_notification));
|
|
}
|
|
|
|
// Creates the toast XML document on the background thread. Returns true on
|
|
// success, false on failure. On failure, posts error to UI thread. static
|
|
bool WindowsToastNotification::CreateToastXmlDocument(
|
|
const NotificationOptions& options,
|
|
NotificationPresenter* presenter,
|
|
const std::string& notification_id,
|
|
base::WeakPtr<Notification> weak_notification,
|
|
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
|
|
ComPtr<IXmlDocument>* toast_xml) {
|
|
// The custom xml takes priority over the preset template.
|
|
if (!options.toast_xml.empty()) {
|
|
DebugLog("Toast XML (custom) id=" + notification_id + ": " +
|
|
base::UTF16ToUTF8(options.toast_xml));
|
|
HRESULT hr = XmlDocumentFromString(base::as_wcstr(options.toast_xml),
|
|
toast_xml->GetAddressOf());
|
|
DebugLog(base::StrCat({"XmlDocumentFromString returned HRESULT: ",
|
|
base::NumberToString(hr)}));
|
|
if (FAILED(hr)) {
|
|
std::string err =
|
|
base::StrCat({"XML: Invalid XML, ERROR ", FailureResultToString(hr)});
|
|
DebugLog(base::StrCat({"XML parsing failed, posting error: ", err}));
|
|
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
|
|
return false;
|
|
}
|
|
DebugLog("XML parsing succeeded");
|
|
} else {
|
|
auto* presenter_win = static_cast<NotificationPresenterWin*>(presenter);
|
|
std::wstring icon_path =
|
|
presenter_win->SaveIconToFilesystem(options.icon, options.icon_url);
|
|
std::u16string toast_xml_str =
|
|
GetToastXml(notification_id, options.title, options.msg, icon_path,
|
|
options.timeout_type, options.silent, options.actions,
|
|
options.has_reply, options.reply_placeholder);
|
|
|
|
DebugLog("Toast XML (generated) id=" + notification_id + ": " +
|
|
base::UTF16ToUTF8(toast_xml_str));
|
|
|
|
HRESULT hr = XmlDocumentFromString(base::as_wcstr(toast_xml_str),
|
|
toast_xml->GetAddressOf());
|
|
if (FAILED(hr)) {
|
|
std::string err =
|
|
base::StrCat({"XML: Invalid XML, ERROR ", FailureResultToString(hr)});
|
|
DebugLog(err);
|
|
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Creates the toast notification on the background thread. Returns true on
|
|
// success, false on failure. On failure, posts error to UI thread. On success,
|
|
// returns the created notification via out parameter.
|
|
bool WindowsToastNotification::CreateToastNotification(
|
|
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument> toast_xml,
|
|
const NotificationOptions& options,
|
|
const std::string& notification_id,
|
|
base::WeakPtr<Notification> weak_notification,
|
|
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
|
|
ComPtr<ABI::Windows::UI::Notifications::IToastNotification>*
|
|
toast_notification) {
|
|
ScopedHString toast_str(
|
|
RuntimeClass_Windows_UI_Notifications_ToastNotification);
|
|
if (!toast_str.success()) {
|
|
PostNotificationFailedToUIThread(
|
|
weak_notification, "Creating ScopedHString failed", ui_task_runner);
|
|
return false;
|
|
}
|
|
|
|
ComPtr<winui::Notifications::IToastNotificationFactory> toast_factory;
|
|
HRESULT hr =
|
|
Windows::Foundation::GetActivationFactory(toast_str, &toast_factory);
|
|
if (FAILED(hr)) {
|
|
std::string err =
|
|
base::StrCat({"WinAPI: GetActivationFactory failed, ERROR ",
|
|
FailureResultToString(hr)});
|
|
DebugLog(err);
|
|
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
|
|
return false;
|
|
}
|
|
|
|
hr = toast_factory->CreateToastNotification(
|
|
toast_xml.Get(), toast_notification->GetAddressOf());
|
|
if (FAILED(hr)) {
|
|
std::string err =
|
|
base::StrCat({"WinAPI: CreateToastNotification failed, ERROR ",
|
|
FailureResultToString(hr)});
|
|
DebugLog(err);
|
|
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
|
|
return false;
|
|
}
|
|
|
|
ComPtr<winui::Notifications::IToastNotification2> toast2;
|
|
hr = (*toast_notification)->QueryInterface(IID_PPV_ARGS(&toast2));
|
|
if (FAILED(hr)) {
|
|
std::string err =
|
|
base::StrCat({"WinAPI: Getting Notification interface failed, ERROR ",
|
|
FailureResultToString(hr)});
|
|
DebugLog(err);
|
|
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
|
|
return false;
|
|
}
|
|
|
|
ScopedHString group(kGroup);
|
|
hr = toast2->put_Group(group);
|
|
if (FAILED(hr)) {
|
|
std::string err = base::StrCat(
|
|
{"WinAPI: Setting group failed, ERROR ", FailureResultToString(hr)});
|
|
DebugLog(err);
|
|
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
|
|
return false;
|
|
}
|
|
|
|
ScopedHString tag(GetTag(notification_id));
|
|
hr = toast2->put_Tag(tag);
|
|
if (FAILED(hr)) {
|
|
std::string err = base::StrCat(
|
|
{"WinAPI: Setting tag failed, ERROR ", FailureResultToString(hr)});
|
|
DebugLog(err);
|
|
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
|
|
return false;
|
|
}
|
|
|
|
ComPtr<winui::Notifications::IToastNotification4> toast4;
|
|
hr = (*toast_notification)->QueryInterface(IID_PPV_ARGS(&toast4));
|
|
if (SUCCEEDED(hr)) {
|
|
winui::Notifications::ToastNotificationPriority priority =
|
|
winui::Notifications::ToastNotificationPriority::
|
|
ToastNotificationPriority_Default;
|
|
if (options.urgency == u"critical") {
|
|
priority = winui::Notifications::ToastNotificationPriority::
|
|
ToastNotificationPriority_High;
|
|
}
|
|
|
|
hr = toast4->put_Priority(priority);
|
|
if (FAILED(hr)) {
|
|
std::string err = base::StrCat({"WinAPI: Setting priority failed, ERROR ",
|
|
FailureResultToString(hr)});
|
|
DebugLog(err);
|
|
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Sets up callbacks and shows the notification on the UI thread.
|
|
// This part has to be called on the UI thread. This WinRT API
|
|
// does not allow calls from background threads.
|
|
void WindowsToastNotification::SetupAndShowOnUIThread(
|
|
base::WeakPtr<Notification> weak_notification,
|
|
ComPtr<ABI::Windows::UI::Notifications::IToastNotification> notification) {
|
|
auto* notif = static_cast<WindowsToastNotification*>(weak_notification.get());
|
|
if (!notif)
|
|
return;
|
|
|
|
// Setup callbacks and store notification on UI thread
|
|
HRESULT hr = notif->SetupCallbacks(notification.Get());
|
|
if (FAILED(hr)) {
|
|
std::string err = base::StrCat(
|
|
{"WinAPI: SetupCallbacks failed, ERROR ", FailureResultToString(hr)});
|
|
DebugLog(err);
|
|
notif->NotificationFailed(err);
|
|
return;
|
|
}
|
|
|
|
notif->toast_notification_ = notification;
|
|
|
|
// Show notification on UI thread (must be called on UI thread)
|
|
hr = (*toast_notifier_)->Show(notification.Get());
|
|
if (FAILED(hr)) {
|
|
std::string err = base::StrCat(
|
|
{"WinAPI: Show failed, ERROR ", FailureResultToString(hr)});
|
|
DebugLog(err);
|
|
notif->NotificationFailed(err);
|
|
return;
|
|
}
|
|
|
|
DebugLog("Notification created");
|
|
if (notif->delegate())
|
|
notif->delegate()->NotificationDisplayed();
|
|
}
|
|
|
|
// Posts a notification failure event back to the UI thread. If the UI thread's
|
|
// task runner is not provided, it fetches it. On the UI thread, calls
|
|
// NotificationFailed on the Notification instance (if it is still valid), which
|
|
// will invoke the delegate (if set) and clean up.
|
|
void WindowsToastNotification::PostNotificationFailedToUIThread(
|
|
base::WeakPtr<Notification> weak_notification,
|
|
const std::string& error,
|
|
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner) {
|
|
DebugLog(base::StrCat(
|
|
{"PostNotificationFailedToUIThread called with error: ", error}));
|
|
if (!ui_task_runner) {
|
|
ui_task_runner = content::GetUIThreadTaskRunner({});
|
|
}
|
|
ui_task_runner->PostTask(
|
|
FROM_HERE,
|
|
base::BindOnce(
|
|
[](base::WeakPtr<Notification> weak_notification,
|
|
const std::string& error) {
|
|
DebugLog(
|
|
"PostNotificationFailedToUIThread lambda executing on UI "
|
|
"thread");
|
|
if (!weak_notification) {
|
|
DebugLog(base::StrCat(
|
|
{"Notification failed but object destroyed: ", error}));
|
|
return;
|
|
}
|
|
|
|
// Call NotificationFailed - it will check for delegate internally
|
|
// and emit the event if delegate is set
|
|
DebugLog(base::StrCat(
|
|
{"Calling NotificationFailed with error: ", error}));
|
|
auto* delegate = weak_notification->delegate();
|
|
DebugLog(
|
|
base::StrCat({"Delegate is set: ", delegate ? "yes" : "no"}));
|
|
|
|
// Call NotificationFailed which will call delegate if set, then
|
|
// cleanup
|
|
weak_notification->NotificationFailed(error);
|
|
DebugLog("NotificationFailed call completed");
|
|
},
|
|
weak_notification, error));
|
|
}
|
|
|
|
void WindowsToastNotification::Remove() {
|
|
DebugLog("Removing notification from action center");
|
|
|
|
ComPtr<winui::Notifications::IToastNotificationManagerStatics2>
|
|
toast_manager2;
|
|
if (FAILED(toast_manager_->As(&toast_manager2)))
|
|
return;
|
|
|
|
ComPtr<winui::Notifications::IToastNotificationHistory> notification_history;
|
|
if (FAILED(toast_manager2->get_History(¬ification_history)))
|
|
return;
|
|
|
|
ScopedHString app_id;
|
|
if (!GetAppUserModelID(&app_id))
|
|
return;
|
|
|
|
ScopedHString group(kGroup);
|
|
ScopedHString tag(GetTag(notification_id()));
|
|
notification_history->RemoveGroupedTagWithId(tag, group, app_id);
|
|
}
|
|
|
|
void WindowsToastNotification::Dismiss() {
|
|
DebugLog("Hiding notification");
|
|
|
|
(*toast_notifier_)->Hide(toast_notification_.Get());
|
|
}
|
|
|
|
std::u16string WindowsToastNotification::GetToastXml(
|
|
const std::string& notification_id,
|
|
const std::u16string& title,
|
|
const std::u16string& msg,
|
|
const std::wstring& icon_path,
|
|
const std::u16string& timeout_type,
|
|
bool silent,
|
|
const std::vector<NotificationAction>& actions,
|
|
bool has_reply,
|
|
const std::u16string& reply_placeholder) {
|
|
XmlWriter xml_writer;
|
|
xml_writer.StartWriting();
|
|
|
|
// <toast ...>
|
|
xml_writer.StartElement(kToast);
|
|
|
|
const bool is_reminder = (timeout_type == u"never");
|
|
if (is_reminder) {
|
|
xml_writer.AddAttribute(kScenario, kReminder);
|
|
}
|
|
|
|
// <visual>
|
|
xml_writer.StartElement(kVisual);
|
|
// <binding template="<template>">
|
|
xml_writer.StartElement(kBinding);
|
|
xml_writer.AddAttribute(kTemplate, kToastTemplate);
|
|
|
|
std::u16string line1;
|
|
std::u16string line2;
|
|
if (title.empty() || msg.empty()) {
|
|
line1 = title.empty() ? msg : title;
|
|
if (line1.empty())
|
|
line1 = u"[no message]";
|
|
// <text>
|
|
xml_writer.StartElement(kText);
|
|
xml_writer.AppendElementContent(base::UTF16ToUTF8(line1));
|
|
xml_writer.EndElement(); // </text>
|
|
} else {
|
|
line1 = title;
|
|
line2 = msg;
|
|
// <text>
|
|
xml_writer.StartElement(kText);
|
|
xml_writer.AppendElementContent(base::UTF16ToUTF8(line1));
|
|
xml_writer.EndElement(); // </text>
|
|
// <text>
|
|
xml_writer.StartElement(kText);
|
|
xml_writer.AppendElementContent(base::UTF16ToUTF8(line2));
|
|
xml_writer.EndElement(); // </text>
|
|
}
|
|
|
|
if (!icon_path.empty()) {
|
|
// <image>
|
|
xml_writer.StartElement(kImage);
|
|
xml_writer.AddAttribute(kID, "1");
|
|
xml_writer.AddAttribute(kPlacement, kAppLogoOverride);
|
|
xml_writer.AddAttribute(kHintCrop, kHintCropNone);
|
|
xml_writer.AddAttribute(kSrc, base::WideToUTF8(icon_path));
|
|
xml_writer.EndElement(); // </image>
|
|
}
|
|
|
|
xml_writer.EndElement(); // </binding>
|
|
xml_writer.EndElement(); // </visual>
|
|
|
|
if (is_reminder || has_reply || !actions.empty()) {
|
|
// <actions>
|
|
xml_writer.StartElement(kActions);
|
|
if (is_reminder) {
|
|
xml_writer.StartElement(kAction);
|
|
xml_writer.AddAttribute(kActivationType, kActivationTypeSystem);
|
|
xml_writer.AddAttribute(kArguments, kDismiss);
|
|
xml_writer.AddAttribute(
|
|
kContent, base::WideToUTF8(l10n_util::GetWideString(IDS_APP_CLOSE)));
|
|
xml_writer.EndElement(); // </action>
|
|
}
|
|
|
|
if (has_reply) {
|
|
// <input>
|
|
xml_writer.StartElement(kInput);
|
|
xml_writer.AddAttribute(kID, kReply);
|
|
xml_writer.AddAttribute(kType, kText);
|
|
if (!reply_placeholder.empty()) {
|
|
xml_writer.AddAttribute(kPlaceholderContent,
|
|
base::UTF16ToUTF8(reply_placeholder));
|
|
}
|
|
xml_writer.EndElement(); // </input>
|
|
}
|
|
|
|
for (size_t i = 0; i < actions.size(); ++i) {
|
|
const auto& act = actions[i];
|
|
if (act.type == u"button" || act.type.empty()) {
|
|
// <action>
|
|
xml_writer.StartElement(kAction);
|
|
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
|
|
std::string args = base::StrCat(
|
|
{"type=action&action=", base::NumberToString(i),
|
|
"&tag=", base::NumberToString(base::FastHash(notification_id))});
|
|
xml_writer.AddAttribute(kArguments, args.c_str());
|
|
xml_writer.AddAttribute(kContent, base::UTF16ToUTF8(act.text));
|
|
xml_writer.EndElement(); // <action>
|
|
} else if (act.type == u"selection") {
|
|
std::string input_id =
|
|
base::StrCat({kSelection, base::NumberToString(i)});
|
|
xml_writer.StartElement(kInput); // <input>
|
|
xml_writer.AddAttribute(kID, input_id.c_str());
|
|
xml_writer.AddAttribute(kType, kSelection);
|
|
for (size_t opt_i = 0; opt_i < act.items.size(); ++opt_i) {
|
|
xml_writer.StartElement(kSelection); // <selection>
|
|
xml_writer.AddAttribute(kID, base::NumberToString(opt_i).c_str());
|
|
xml_writer.AddAttribute(kContent,
|
|
base::UTF16ToUTF8(act.items[opt_i]));
|
|
xml_writer.EndElement(); // </selection>
|
|
}
|
|
xml_writer.EndElement(); // </input>
|
|
|
|
// The button that submits the selection.
|
|
xml_writer.StartElement(kAction);
|
|
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
|
|
std::string args = base::StrCat(
|
|
{"type=action&action=", base::NumberToString(i),
|
|
"&tag=", base::NumberToString(base::FastHash(notification_id))});
|
|
xml_writer.AddAttribute(kArguments, args.c_str());
|
|
xml_writer.AddAttribute(
|
|
kContent,
|
|
base::UTF16ToUTF8(act.text.empty() ? u"Select" : act.text));
|
|
xml_writer.AddAttribute(kHintInputId, input_id.c_str());
|
|
xml_writer.EndElement(); // </action>
|
|
}
|
|
}
|
|
|
|
if (has_reply) {
|
|
// <action>
|
|
xml_writer.StartElement(kAction);
|
|
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
|
|
std::string args =
|
|
base::StrCat({"type=reply&tag=",
|
|
base::NumberToString(base::FastHash(notification_id))});
|
|
xml_writer.AddAttribute(kArguments, args.c_str());
|
|
// TODO(codebytere): we should localize this.
|
|
xml_writer.AddAttribute(kContent, "Reply");
|
|
xml_writer.AddAttribute(kHintInputId, kReply);
|
|
xml_writer.EndElement(); // <action>
|
|
}
|
|
xml_writer.EndElement(); // </actions>
|
|
}
|
|
|
|
// Silent audio if requested.
|
|
if (silent) {
|
|
xml_writer.StartElement(kAudio);
|
|
xml_writer.AddAttribute(kSilent, kTrue);
|
|
xml_writer.EndElement(); // </audio>
|
|
}
|
|
|
|
xml_writer.EndElement(); // </toast>
|
|
|
|
xml_writer.StopWriting();
|
|
std::string xml = xml_writer.GetWrittenString();
|
|
if (base::StartsWith(xml, kXmlVersionHeader, base::CompareCase::SENSITIVE)) {
|
|
xml.erase(0, sizeof(kXmlVersionHeader) - 1);
|
|
}
|
|
|
|
return base::UTF8ToUTF16(xml);
|
|
}
|
|
|
|
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(
|
|
winui::Notifications::IToastNotification* toast) {
|
|
event_handler_ = Make<ToastEventHandler>(this);
|
|
RETURN_IF_FAILED(
|
|
toast->add_Activated(event_handler_.Get(), &activated_token_));
|
|
RETURN_IF_FAILED(
|
|
toast->add_Dismissed(event_handler_.Get(), &dismissed_token_));
|
|
RETURN_IF_FAILED(toast->add_Failed(event_handler_.Get(), &failed_token_));
|
|
return S_OK;
|
|
}
|
|
|
|
bool WindowsToastNotification::RemoveCallbacks(
|
|
winui::Notifications::IToastNotification* toast) {
|
|
if (FAILED(toast->remove_Activated(activated_token_)))
|
|
return false;
|
|
|
|
if (FAILED(toast->remove_Dismissed(dismissed_token_)))
|
|
return false;
|
|
|
|
return SUCCEEDED(toast->remove_Failed(failed_token_));
|
|
}
|
|
|
|
/*
|
|
/ Toast Event Handler
|
|
*/
|
|
ToastEventHandler::ToastEventHandler(Notification* notification)
|
|
: notification_(notification->GetWeakPtr()) {}
|
|
|
|
ToastEventHandler::~ToastEventHandler() = default;
|
|
|
|
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<winui::Notifications::IToastActivatedEventArgs>
|
|
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) {
|
|
UINT32 len = 0;
|
|
const wchar_t* raw = WindowsGetStringRawBuffer(args_hs, &len);
|
|
if (raw && len)
|
|
arguments_w.assign(raw, len);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sender) {
|
|
Microsoft::WRL::ComPtr<winui::Notifications::IToastNotification2> 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;
|
|
std::string notif_hash;
|
|
if (notification_) {
|
|
notif_id = notification_->notification_id();
|
|
notif_hash = base::NumberToString(base::FastHash(notif_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)
|
|
return S_OK;
|
|
|
|
content::GetUIThreadTaskRunner({})->PostTask(
|
|
FROM_HERE,
|
|
base::BindOnce(&Notification::NotificationClicked, notification_));
|
|
DebugLog("Notification clicked");
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
IFACEMETHODIMP ToastEventHandler::Invoke(
|
|
winui::Notifications::IToastNotification* sender,
|
|
winui::Notifications::IToastDismissedEventArgs* e) {
|
|
winui::Notifications::ToastDismissalReason reason;
|
|
std::string reason_string;
|
|
if (SUCCEEDED(e->get_Reason(&reason))) {
|
|
switch (reason) {
|
|
case winui::Notifications::ToastDismissalReason_UserCanceled:
|
|
reason_string = "userCanceled";
|
|
break;
|
|
case winui::Notifications::ToastDismissalReason_ApplicationHidden:
|
|
reason_string = "applicationHidden";
|
|
break;
|
|
case winui::Notifications::ToastDismissalReason_TimedOut:
|
|
reason_string = "timedOut";
|
|
break;
|
|
}
|
|
}
|
|
content::GetUIThreadTaskRunner({})->PostTask(
|
|
FROM_HERE, base::BindOnce(&Notification::NotificationDismissed,
|
|
notification_, false, reason_string));
|
|
DebugLog("Notification dismissed");
|
|
return S_OK;
|
|
}
|
|
|
|
IFACEMETHODIMP ToastEventHandler::Invoke(
|
|
winui::Notifications::IToastNotification* sender,
|
|
winui::Notifications::IToastFailedEventArgs* e) {
|
|
HRESULT error;
|
|
e->get_ErrorCode(&error);
|
|
std::string errorMessage = FailureResultToString(error);
|
|
content::GetUIThreadTaskRunner({})->PostTask(
|
|
FROM_HERE, base::BindOnce(&Notification::NotificationFailed,
|
|
notification_, errorMessage));
|
|
DebugLog(errorMessage);
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
} // namespace electron
|