1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sts=2 sw=2 et cin: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "ToastNotificationHandler.h"
9 #include <windows.foundation.h>
12 #include "gfxPlatform.h"
13 #include "imgIContainer.h"
14 #include "imgIRequest.h"
15 #include "json/json.h"
16 #include "mozilla/gfx/2D.h"
17 #ifdef MOZ_BACKGROUNDTASKS
18 # include "mozilla/BackgroundTasks.h"
20 #include "mozilla/HashFunctions.h"
21 #include "mozilla/JSONStringWriteFuncs.h"
22 #include "mozilla/Result.h"
23 #include "mozilla/Logging.h"
24 #include "mozilla/Tokenizer.h"
25 #include "mozilla/Unused.h"
26 #include "mozilla/WindowsVersion.h"
27 #include "mozilla/intl/Localization.h"
28 #include "nsAppDirectoryServiceDefs.h"
29 #include "nsAppRunner.h"
30 #include "nsDirectoryServiceDefs.h"
31 #include "nsDirectoryServiceUtils.h"
32 #include "nsIDUtils.h"
33 #include "nsIStringBundle.h"
34 #include "nsIToolkitProfile.h"
35 #include "nsIToolkitProfileService.h"
37 #include "nsIWidget.h"
38 #include "nsIWindowMediator.h"
39 #include "nsNetUtil.h"
40 #include "nsPIDOMWindow.h"
41 #include "nsProxyRelease.h"
42 #include "nsXREDirProvider.h"
43 #include "ToastNotificationHeaderOnlyUtils.h"
44 #include "WidgetUtils.h"
47 #include "ToastNotification.h"
52 extern LazyLogModule sWASLog
;
54 using namespace ABI::Windows::Data::Xml::Dom
;
55 using namespace ABI::Windows::Foundation
;
56 using namespace ABI::Windows::UI::Notifications
;
57 using namespace Microsoft::WRL
;
58 using namespace Microsoft::WRL::Wrappers
;
59 using namespace toastnotification
;
61 // Needed to disambiguate internal and Windows `ToastNotification` classes.
62 using WinToastNotification
= ABI::Windows::UI::Notifications::ToastNotification
;
63 using ToastActivationHandler
=
64 ITypedEventHandler
<WinToastNotification
*, IInspectable
*>;
65 using ToastDismissedHandler
=
66 ITypedEventHandler
<WinToastNotification
*, ToastDismissedEventArgs
*>;
67 using ToastFailedHandler
=
68 ITypedEventHandler
<WinToastNotification
*, ToastFailedEventArgs
*>;
69 using IVectorView_ToastNotification
=
70 Collections::IVectorView
<WinToastNotification
*>;
72 NS_IMPL_ISUPPORTS(ToastNotificationHandler
, nsIAlertNotificationImageListener
)
74 static bool SetNodeValueString(const nsString
& aString
, IXmlNode
* node
,
76 ComPtr
<IXmlText
> inputText
;
78 hr
= xml
->CreateTextNode(HStringReference(aString
.get()).Get(), &inputText
);
79 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
81 ComPtr
<IXmlNode
> inputTextNode
;
82 hr
= inputText
.As(&inputTextNode
);
83 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
85 ComPtr
<IXmlNode
> appendedChild
;
86 hr
= node
->AppendChild(inputTextNode
.Get(), &appendedChild
);
87 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
92 static bool SetAttribute(ComPtr
<IXmlElement
>& element
,
93 const HStringReference
& name
, const nsAString
& value
) {
95 valueStr
.Set(PromiseFlatString(value
).get());
97 HRESULT hr
= element
->SetAttribute(name
.Get(), valueStr
.Get());
98 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
103 static bool AddActionNode(ComPtr
<IXmlDocument
>& toastXml
,
104 ComPtr
<IXmlNode
>& actionsNode
,
105 const nsAString
& actionTitle
,
106 const nsAString
& launchArg
,
107 const nsAString
& actionArgs
,
108 const nsAString
& actionPlacement
= u
""_ns
,
109 const nsAString
& activationType
= u
""_ns
) {
110 ComPtr
<IXmlElement
> action
;
112 toastXml
->CreateElement(HStringReference(L
"action").Get(), &action
);
113 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
116 SetAttribute(action
, HStringReference(L
"content"), actionTitle
);
117 NS_ENSURE_TRUE(success
, false);
119 // Action arguments overwrite the toast's launch arguments, so we need to
120 // prepend the launch arguments necessary for the Notification Server to
121 // reconstruct the toast's origin.
123 // Web Notification actions are arbitrary strings; to prevent breaking launch
124 // argument parsing the action argument must be last. All delimiters after
125 // `action` are part of the action arugment.
126 nsAutoString args
= launchArg
+ u
"\n"_ns
+
127 nsDependentString(kLaunchArgAction
) + u
"\n"_ns
+
129 success
= SetAttribute(action
, HStringReference(L
"arguments"), args
);
130 NS_ENSURE_TRUE(success
, false);
132 if (!actionPlacement
.IsEmpty()) {
134 SetAttribute(action
, HStringReference(L
"placement"), actionPlacement
);
135 NS_ENSURE_TRUE(success
, false);
138 if (!activationType
.IsEmpty()) {
139 success
= SetAttribute(action
, HStringReference(L
"activationType"),
141 NS_ENSURE_TRUE(success
, false);
143 // No special argument handling: when `activationType="system"`, `arguments`
144 // should be a Windows-specific keyword, namely "dismiss" or "snooze", which
145 // are supposed to make a system handled dismiss/snooze buttons.
146 // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml#snoozedismiss
148 // Note that while using it prevents calling our notification COM server,
149 // it somehow still calls OnActivate instead of OnDismiss. Thus, we still
150 // need to handle such callbacks manually by checking `arguments`.
151 success
= SetAttribute(action
, HStringReference(L
"arguments"), actionArgs
);
152 NS_ENSURE_TRUE(success
, false);
155 // Add <action> to <actions>
156 ComPtr
<IXmlNode
> actionNode
;
157 hr
= action
.As(&actionNode
);
158 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
160 ComPtr
<IXmlNode
> appendedChild
;
161 hr
= actionsNode
->AppendChild(actionNode
.Get(), &appendedChild
);
162 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
167 nsresult
ToastNotificationHandler::GetWindowsTag(nsAString
& aWindowsTag
) {
168 aWindowsTag
.Assign(mWindowsTag
);
172 nsresult
ToastNotificationHandler::SetWindowsTag(const nsAString
& aWindowsTag
) {
173 mWindowsTag
.Assign(aWindowsTag
);
177 // clang - format off
178 /* Populate the launch argument so the COM server can reconstruct the toast
187 Result
<nsString
, nsresult
> ToastNotificationHandler::GetLaunchArgument() {
190 // When the preference is false, the COM notification server will be invoked,
191 // discover that there is no `program`, and exit (successfully), after which
192 // Windows will invoke the in-product Windows 8-style callbacks. When true,
193 // the COM notification server will launch Firefox with sufficient arguments
194 // for Firefox to handle the notification.
195 if (!Preferences::GetBool(
196 "alerts.useSystemBackend.windows.notificationserver.enabled",
198 // Include dummy key/value so that newline appended arguments aren't off by
200 launchArg
+= u
"invalid key\ninvalid value"_ns
;
204 // `program` argument.
205 launchArg
+= nsDependentString(kLaunchArgProgram
) + u
"\n"_ns MOZ_APP_NAME
;
207 // `profile` argument.
208 nsCOMPtr
<nsIFile
> profDir
;
209 bool wantCurrentProfile
= true;
210 #ifdef MOZ_BACKGROUNDTASKS
211 if (BackgroundTasks::IsBackgroundTaskMode()) {
212 // Notifications popped from a background task want to invoke Firefox with a
213 // different profile -- the default browsing profile. We'd prefer to not
214 // specify a profile, so that the Firefox invoked by the notification server
215 // chooses its default profile, but this might pop the profile chooser in
216 // some configurations.
217 wantCurrentProfile
= false;
219 nsCOMPtr
<nsIToolkitProfileService
> profileSvc
=
220 do_GetService(NS_PROFILESERVICE_CONTRACTID
);
222 nsCOMPtr
<nsIToolkitProfile
> defaultProfile
;
224 profileSvc
->GetDefaultProfile(getter_AddRefs(defaultProfile
));
225 if (NS_SUCCEEDED(rv
) && defaultProfile
) {
226 // Not all installations have a default profile. But if one is set,
227 // then it should have a profile directory.
228 MOZ_TRY(defaultProfile
->GetRootDir(getter_AddRefs(profDir
)));
233 if (wantCurrentProfile
) {
234 MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR
,
235 getter_AddRefs(profDir
)));
239 nsAutoString profilePath
;
240 MOZ_TRY(profDir
->GetPath(profilePath
));
241 launchArg
+= u
"\n"_ns
+ nsDependentString(kLaunchArgProfile
) + u
"\n"_ns
+
245 // `windowsTag` argument.
247 u
"\n"_ns
+ nsDependentString(kLaunchArgTag
) + u
"\n"_ns
+ mWindowsTag
;
249 // `logging` argument.
250 if (Preferences::GetBool(
251 "alerts.useSystemBackend.windows.notificationserver.verbose",
253 // Signal notification to log verbose messages.
255 u
"\n"_ns
+ nsDependentString(kLaunchArgLogging
) + u
"\nverbose"_ns
;
261 static ComPtr
<IToastNotificationManagerStatics
>
262 GetToastNotificationManagerStatics() {
263 ComPtr
<IToastNotificationManagerStatics
> toastNotificationManagerStatics
;
264 HRESULT hr
= GetActivationFactory(
266 RuntimeClass_Windows_UI_Notifications_ToastNotificationManager
)
268 &toastNotificationManagerStatics
);
269 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
271 return toastNotificationManagerStatics
;
274 ToastNotificationHandler::~ToastNotificationHandler() {
276 mImageRequest
->Cancel(NS_BINDING_ABORTED
);
277 mImageRequest
= nullptr;
280 if (mHasImage
&& mImageFile
) {
281 DebugOnly
<nsresult
> rv
= mImageFile
->Remove(false);
282 NS_ASSERTION(NS_SUCCEEDED(rv
), "Cannot remove temporary image file");
288 void ToastNotificationHandler::UnregisterHandler() {
290 mNotification
->remove_Dismissed(mDismissedToken
);
291 mNotification
->remove_Activated(mActivatedToken
);
292 mNotification
->remove_Failed(mFailedToken
);
295 mNotification
= nullptr;
301 nsresult
ToastNotificationHandler::InitAlertAsync(
302 nsIAlertNotification
* aAlert
) {
303 MOZ_TRY(InitWindowsTag());
305 #ifdef MOZ_BACKGROUNDTASKS
306 nsAutoString imageUrl
;
307 if (BackgroundTasks::IsBackgroundTaskMode() &&
308 NS_SUCCEEDED(aAlert
->GetImageURL(imageUrl
)) && !imageUrl
.IsEmpty()) {
309 // Bug 1870750: Image decoding relies on gfx and runs on a thread pool,
310 // which expects to have been initialized early and on the main thread.
311 // Since background tasks run headless this never occurs. In this case we
312 // force gfx initialization.
313 Unused
<< NS_WARN_IF(!gfxPlatform::GetPlatform());
317 return aAlert
->LoadImage(/* aTimeout = */ 0, this, /* aUserData = */ nullptr,
318 getter_AddRefs(mImageRequest
));
321 // Uniquely identify this toast to Windows. Existing names and cookies are not
322 // suitable: we want something generated and unique. This is needed to check if
323 // toast is still present in the Windows Action Center when we receive a dismiss
326 // Local testing reveals that the space of tags is not global but instead is per
327 // AUMID. Since an installation uses a unique AUMID incorporating the install
328 // directory hash, it should not witness another installation's tag.
329 nsresult
ToastNotificationHandler::InitWindowsTag() {
330 mWindowsTag
.Truncate();
334 // Multiple profiles might overwrite each other's toast messages when a
335 // common name is used for a given host port. We prevent this by including
336 // the profile directory as part of the toast hash.
337 nsCOMPtr
<nsIFile
> profDir
;
338 MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR
,
339 getter_AddRefs(profDir
)));
340 MOZ_TRY(profDir
->GetPath(tag
));
342 if (!mHostPort
.IsEmpty()) {
343 // Notification originated from a web notification.
344 // `mName` will be in the form `{mHostPort}#tag:{tag}` if the notification
345 // was created with a tag and `{mHostPort}#notag:{uuid}` otherwise.
348 // Notification originated from the browser chrome.
349 if (!mName
.IsEmpty()) {
350 tag
+= u
"chrome#tag:"_ns
;
351 // Browser chrome notifications don't follow any convention for naming.
354 // No associated name, append a UUID to prevent reuse of the same tag.
355 nsIDToCString
uuidString(nsID::GenerateUUID());
356 size_t len
= strlen(uuidString
.get());
357 MOZ_ASSERT(len
== NSID_LENGTH
- 1);
359 CopyASCIItoUTF16(nsDependentCSubstring(uuidString
.get(), len
), uuid
);
361 tag
+= u
"chrome#notag:"_ns
;
366 // Windows notification tags are limited to 16 characters, or 64 characters
367 // after the Creators Update; therefore we hash the tag to fit the minimum
369 HashNumber hash
= HashString(tag
);
370 mWindowsTag
.AppendPrintf("%010u", hash
);
375 nsString
ToastNotificationHandler::ActionArgsJSONString(
376 const nsString
& aAction
, const nsString
& aOpaqueRelaunchData
= u
""_ns
) {
377 nsAutoCString actionArgsData
;
379 JSONStringRefWriteFunc
js(actionArgsData
);
380 JSONWriter
w(js
, JSONWriter::SingleLineStyle
);
383 w
.StringProperty("action", NS_ConvertUTF16toUTF8(aAction
));
385 if (mIsSystemPrincipal
) {
386 // Privileged/chrome alerts (not activated by Windows) can have custom
388 if (!aOpaqueRelaunchData
.IsEmpty()) {
389 w
.StringProperty("opaqueRelaunchData",
390 NS_ConvertUTF16toUTF8(aOpaqueRelaunchData
));
393 // Privileged alerts include any provided name for metrics.
394 if (!mName
.IsEmpty()) {
395 w
.StringProperty("privilegedName", NS_ConvertUTF16toUTF8(mName
));
398 if (!mHostPort
.IsEmpty()) {
399 w
.StringProperty("launchUrl", NS_ConvertUTF16toUTF8(mHostPort
));
405 return NS_ConvertUTF8toUTF16(actionArgsData
);
408 ComPtr
<IXmlDocument
> ToastNotificationHandler::CreateToastXmlDocument() {
409 ComPtr
<IToastNotificationManagerStatics
> toastNotificationManagerStatics
=
410 GetToastNotificationManagerStatics();
411 NS_ENSURE_TRUE(toastNotificationManagerStatics
, nullptr);
413 ToastTemplateType toastTemplate
;
414 if (mHostPort
.IsEmpty()) {
416 mHasImage
? ToastTemplateType::ToastTemplateType_ToastImageAndText03
417 : ToastTemplateType::ToastTemplateType_ToastText03
;
420 mHasImage
? ToastTemplateType::ToastTemplateType_ToastImageAndText04
421 : ToastTemplateType::ToastTemplateType_ToastText04
;
424 ComPtr
<IXmlDocument
> toastXml
;
425 toastNotificationManagerStatics
->GetTemplateContent(toastTemplate
, &toastXml
);
436 ComPtr
<IXmlNodeList
> toastImageElements
;
437 hr
= toastXml
->GetElementsByTagName(HStringReference(L
"image").Get(),
438 &toastImageElements
);
439 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
441 ComPtr
<IXmlNode
> imageNode
;
442 hr
= toastImageElements
->Item(0, &imageNode
);
443 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
445 ComPtr
<IXmlElement
> image
;
446 hr
= imageNode
.As(&image
);
447 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
449 success
= SetAttribute(image
, HStringReference(L
"src"), mImageUri
);
450 NS_ENSURE_TRUE(success
, nullptr);
452 switch (mImagePlacement
) {
453 case ImagePlacement::eHero
:
455 SetAttribute(image
, HStringReference(L
"placement"), u
"hero"_ns
);
456 NS_ENSURE_TRUE(success
, nullptr);
458 case ImagePlacement::eIcon
:
459 success
= SetAttribute(image
, HStringReference(L
"placement"),
460 u
"appLogoOverride"_ns
);
461 NS_ENSURE_TRUE(success
, nullptr);
463 case ImagePlacement::eInline
:
464 // No attribute placement attribute for inline images.
469 ComPtr
<IXmlNodeList
> toastTextElements
;
470 hr
= toastXml
->GetElementsByTagName(HStringReference(L
"text").Get(),
472 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
474 ComPtr
<IXmlNode
> titleTextNodeRoot
;
475 hr
= toastTextElements
->Item(0, &titleTextNodeRoot
);
476 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
478 ComPtr
<IXmlNode
> msgTextNodeRoot
;
479 hr
= toastTextElements
->Item(1, &msgTextNodeRoot
);
480 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
482 success
= SetNodeValueString(mTitle
, titleTextNodeRoot
.Get(), toastXml
.Get());
483 NS_ENSURE_TRUE(success
, nullptr);
485 success
= SetNodeValueString(mMsg
, msgTextNodeRoot
.Get(), toastXml
.Get());
486 NS_ENSURE_TRUE(success
, nullptr);
488 ComPtr
<IXmlNodeList
> toastElements
;
489 hr
= toastXml
->GetElementsByTagName(HStringReference(L
"toast").Get(),
491 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
493 ComPtr
<IXmlNode
> toastNodeRoot
;
494 hr
= toastElements
->Item(0, &toastNodeRoot
);
495 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
497 ComPtr
<IXmlElement
> toastElement
;
498 hr
= toastNodeRoot
.As(&toastElement
);
499 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
501 if (mRequireInteraction
) {
502 success
= SetAttribute(toastElement
, HStringReference(L
"scenario"),
504 NS_ENSURE_TRUE(success
, nullptr);
507 auto maybeLaunchArg
= GetLaunchArgument();
508 NS_ENSURE_TRUE(maybeLaunchArg
.isOk(), nullptr);
509 nsString launchArg
= maybeLaunchArg
.unwrap();
511 nsString launchArgWithoutAction
= launchArg
;
513 if (!mIsSystemPrincipal
) {
514 // Unprivileged/content alerts can't have custom relaunch data.
515 NS_WARNING_ASSERTION(mOpaqueRelaunchData
.IsEmpty(),
516 "unprivileged/content alert "
517 "should have trivial `mOpaqueRelaunchData`");
520 launchArg
+= u
"\n"_ns
+ nsDependentString(kLaunchArgAction
) + u
"\n"_ns
+
521 ActionArgsJSONString(u
""_ns
, mOpaqueRelaunchData
);
523 success
= SetAttribute(toastElement
, HStringReference(L
"launch"), launchArg
);
524 NS_ENSURE_TRUE(success
, nullptr);
526 MOZ_LOG(sWASLog
, LogLevel::Debug
,
527 ("launchArg: '%s'", NS_ConvertUTF16toUTF8(launchArg
).get()));
529 // Use newer toast layout for system (chrome-privileged) toasts. This gains us
530 // UI elements such as new image placement options (default image placement is
531 // larger and inline) and buttons.
532 if (mIsSystemPrincipal
) {
533 ComPtr
<IXmlNodeList
> bindingElements
;
534 hr
= toastXml
->GetElementsByTagName(HStringReference(L
"binding").Get(),
536 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
538 ComPtr
<IXmlNode
> bindingNodeRoot
;
539 hr
= bindingElements
->Item(0, &bindingNodeRoot
);
540 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
542 ComPtr
<IXmlElement
> bindingElement
;
543 hr
= bindingNodeRoot
.As(&bindingElement
);
544 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
546 success
= SetAttribute(bindingElement
, HStringReference(L
"template"),
548 NS_ENSURE_TRUE(success
, nullptr);
551 ComPtr
<IXmlElement
> actions
;
552 hr
= toastXml
->CreateElement(HStringReference(L
"actions").Get(), &actions
);
553 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
555 ComPtr
<IXmlNode
> actionsNode
;
556 hr
= actions
.As(&actionsNode
);
557 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
559 nsCOMPtr
<nsIStringBundleService
> sbs
=
560 do_GetService(NS_STRINGBUNDLE_CONTRACTID
);
561 NS_ENSURE_TRUE(sbs
, nullptr);
563 nsCOMPtr
<nsIStringBundle
> bundle
;
564 sbs
->CreateBundle("chrome://alerts/locale/alert.properties",
565 getter_AddRefs(bundle
));
566 NS_ENSURE_TRUE(bundle
, nullptr);
568 if (!mHostPort
.IsEmpty()) {
569 AutoTArray
<nsString
, 1> formatStrings
= {mHostPort
};
571 ComPtr
<IXmlNode
> urlTextNodeRoot
;
572 hr
= toastTextElements
->Item(2, &urlTextNodeRoot
);
573 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
575 nsAutoString urlReference
;
576 bundle
->FormatStringFromName("source.label", formatStrings
, urlReference
);
579 SetNodeValueString(urlReference
, urlTextNodeRoot
.Get(), toastXml
.Get());
580 NS_ENSURE_TRUE(success
, nullptr);
582 if (IsWin10AnniversaryUpdateOrLater()) {
583 ComPtr
<IXmlElement
> placementText
;
584 hr
= urlTextNodeRoot
.As(&placementText
);
586 // placement is supported on Windows 10 Anniversary Update or later
587 SetAttribute(placementText
, HStringReference(L
"placement"),
592 nsAutoString disableButtonTitle
;
593 ns
= bundle
->FormatStringFromName("webActions.disableForOrigin.label",
594 formatStrings
, disableButtonTitle
);
595 NS_ENSURE_SUCCESS(ns
, nullptr);
597 AddActionNode(toastXml
, actionsNode
, disableButtonTitle
,
598 // TODO: launch into `about:preferences`?
599 launchArgWithoutAction
, ActionArgsJSONString(u
"snooze"_ns
),
603 bool wantSettings
= true;
604 #ifdef MOZ_BACKGROUNDTASKS
605 if (BackgroundTasks::IsBackgroundTaskMode()) {
606 // Notifications popped from a background task want to invoke Firefox with a
607 // different profile -- the default browsing profile. Don't link to Firefox
608 // settings in some different profile: the relevant Firefox settings won't
610 wantSettings
= false;
613 if (MOZ_LIKELY(wantSettings
)) {
614 nsAutoString settingsButtonTitle
;
615 bundle
->GetStringFromName("webActions.settings.label", settingsButtonTitle
);
616 success
= AddActionNode(
617 toastXml
, actionsNode
, settingsButtonTitle
, launchArgWithoutAction
,
618 // TODO: launch into `about:preferences`?
619 ActionArgsJSONString(u
"settings"_ns
), u
"contextmenu"_ns
);
620 NS_ENSURE_TRUE(success
, nullptr);
623 for (const auto& action
: mActions
) {
624 // Bug 1778596: include per-action icon from image URL.
626 ns
= action
->GetTitle(title
);
627 NS_ENSURE_SUCCESS(ns
, nullptr);
628 if (!EnsureUTF16Validity(title
)) {
629 MOZ_LOG(sWASLog
, LogLevel::Warning
,
630 ("Notification text was invalid UTF16, unpaired surrogates have "
634 nsString actionString
;
635 ns
= action
->GetAction(actionString
);
636 NS_ENSURE_SUCCESS(ns
, nullptr);
637 if (!EnsureUTF16Validity(actionString
)) {
638 MOZ_LOG(sWASLog
, LogLevel::Warning
,
639 ("Notification text was invalid UTF16, unpaired surrogates have "
643 nsString opaqueRelaunchData
;
644 ns
= action
->GetOpaqueRelaunchData(opaqueRelaunchData
);
645 NS_ENSURE_SUCCESS(ns
, nullptr);
647 MOZ_LOG(sWASLog
, LogLevel::Debug
,
648 ("launchArgWithoutAction for '%s': '%s'",
649 NS_ConvertUTF16toUTF8(actionString
).get(),
650 NS_ConvertUTF16toUTF8(launchArgWithoutAction
).get()));
652 // Privileged/chrome alerts can have actions that are activated by Windows.
653 // Recognize these actions and enable these activations.
654 bool activationType(false);
655 ns
= action
->GetWindowsSystemActivationType(&activationType
);
656 NS_ENSURE_SUCCESS(ns
, nullptr);
658 nsString
activationTypeString(
659 (mIsSystemPrincipal
&& activationType
) ? u
"system"_ns
: u
""_ns
);
662 if (mIsSystemPrincipal
&& activationType
) {
663 // Privileged/chrome alerts that are activated by Windows can't have
664 // custom relaunch data.
665 actionArgs
= actionString
;
667 NS_WARNING_ASSERTION(opaqueRelaunchData
.IsEmpty(),
668 "action with `windowsSystemActivationType=true` "
669 "should have trivial `opaqueRelaunchData`");
671 actionArgs
= ActionArgsJSONString(actionString
, opaqueRelaunchData
);
674 success
= AddActionNode(toastXml
, actionsNode
, title
,
675 /* launchArg */ launchArgWithoutAction
,
676 /* actionArgs */ actionArgs
,
677 /* actionPlacement */ u
""_ns
,
678 /* activationType */ activationTypeString
);
679 NS_ENSURE_TRUE(success
, nullptr);
682 // Windows ignores scenario=reminder added by mRequiredInteraction if
683 // there's no non-contextmenu action.
684 if (mRequireInteraction
&& !mActions
.Length()) {
685 // `activationType="system" arguments="dismiss" content=""` provides
686 // localized text from Windows, but we support more locales than Windows
687 // does, so let's have our own.
688 nsTArray
<nsCString
> resIds
= {
689 "toolkit/global/alert.ftl"_ns
,
691 RefPtr
<intl::Localization
> l10n
= intl::Localization::Create(resIds
, true);
692 IgnoredErrorResult rv
;
693 nsAutoCString closeTitle
;
694 l10n
->FormatValueSync("notification-default-dismiss"_ns
, {}, closeTitle
,
696 NS_ENSURE_TRUE(!rv
.Failed(), nullptr);
699 AddActionNode(toastXml
, actionsNode
, NS_ConvertUTF8toUTF16(closeTitle
),
700 u
""_ns
, u
"dismiss"_ns
, u
""_ns
, u
"system"_ns
),
704 ComPtr
<IXmlNode
> appendedChild
;
705 hr
= toastNodeRoot
->AppendChild(actionsNode
.Get(), &appendedChild
);
706 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
709 ComPtr
<IXmlNode
> audioNode
;
710 // Create <audio silent="true"/> for silent notifications.
711 ComPtr
<IXmlElement
> audio
;
712 hr
= toastXml
->CreateElement(HStringReference(L
"audio").Get(), &audio
);
713 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
715 SetAttribute(audio
, HStringReference(L
"silent"), u
"true"_ns
);
717 hr
= audio
.As(&audioNode
);
718 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
719 hr
= toastNodeRoot
->AppendChild(audioNode
.Get(), &appendedChild
);
720 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
726 nsresult
ToastNotificationHandler::CreateToastXmlString(
727 const nsAString
& aImageURL
, nsAString
& aString
) {
730 if (!aImageURL
.IsEmpty()) {
731 // For testing: don't fetch and write image to disk, just include the URL.
733 mImageUri
.Assign(aImageURL
);
736 ComPtr
<IXmlDocument
> toastXml
= CreateToastXmlDocument();
738 return NS_ERROR_FAILURE
;
741 ComPtr
<IXmlNodeSerializer
> ser
;
742 hr
= toastXml
.As(&ser
);
743 NS_ENSURE_TRUE(SUCCEEDED(hr
), NS_ERROR_FAILURE
);
746 hr
= ser
->GetXml(data
.GetAddressOf());
747 NS_ENSURE_TRUE(SUCCEEDED(hr
), NS_ERROR_FAILURE
);
750 const wchar_t* rawData
= data
.GetRawBuffer(&len
);
751 NS_ENSURE_TRUE(rawData
, NS_ERROR_FAILURE
);
752 aString
.Assign(rawData
, len
);
757 bool ToastNotificationHandler::ShowAlert() {
758 if (!mBackend
->IsActiveHandler(mName
, this)) {
762 ComPtr
<IXmlDocument
> toastXml
= CreateToastXmlDocument();
768 return CreateWindowsNotificationFromXml(toastXml
);
771 bool ToastNotificationHandler::IsPrivate() { return mInPrivateBrowsing
; }
773 void ToastNotificationHandler::HideAlert() {
774 if (mNotifier
&& mNotification
) {
775 mNotifier
->Hide(mNotification
.Get());
779 bool ToastNotificationHandler::CreateWindowsNotificationFromXml(
780 ComPtr
<IXmlDocument
>& aXml
) {
781 ComPtr
<IToastNotificationFactory
> factory
;
784 hr
= GetActivationFactory(
785 HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification
)
788 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
790 hr
= factory
->CreateToastNotification(aXml
.Get(), &mNotification
);
791 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
793 RefPtr
<ToastNotificationHandler
> self
= this;
795 hr
= mNotification
->add_Activated(
796 Callback
<ToastActivationHandler
>([self
](IToastNotification
* aNotification
,
797 IInspectable
* aInspectable
) {
798 return self
->OnActivate(ComPtr
<IToastNotification
>(aNotification
),
799 ComPtr
<IInspectable
>(aInspectable
));
802 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
804 hr
= mNotification
->add_Dismissed(
805 Callback
<ToastDismissedHandler
>([self
](IToastNotification
* aNotification
,
806 IToastDismissedEventArgs
* aArgs
) {
807 return self
->OnDismiss(ComPtr
<IToastNotification
>(aNotification
),
808 ComPtr
<IToastDismissedEventArgs
>(aArgs
));
811 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
813 hr
= mNotification
->add_Failed(
814 Callback
<ToastFailedHandler
>([self
](IToastNotification
* aNotification
,
815 IToastFailedEventArgs
* aArgs
) {
816 return self
->OnFail(ComPtr
<IToastNotification
>(aNotification
),
817 ComPtr
<IToastFailedEventArgs
>(aArgs
));
820 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
822 ComPtr
<IToastNotification2
> notification2
;
823 hr
= mNotification
.As(¬ification2
);
824 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
827 hr
= hTag
.Set(mWindowsTag
.get());
828 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
830 hr
= notification2
->put_Tag(hTag
.Get());
831 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
833 ComPtr
<IToastNotificationManagerStatics
> toastNotificationManagerStatics
=
834 GetToastNotificationManagerStatics();
835 NS_ENSURE_TRUE(toastNotificationManagerStatics
, false);
838 hr
= aumid
.Set(mAumid
.get());
839 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
840 hr
= toastNotificationManagerStatics
->CreateToastNotifierWithId(aumid
.Get(),
842 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
844 hr
= mNotifier
->Show(mNotification
.Get());
845 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
847 if (mAlertListener
) {
848 mAlertListener
->Observe(nullptr, "alertshow", mCookie
.get());
854 void ToastNotificationHandler::SendFinished() {
855 if (!mSentFinished
&& mAlertListener
) {
856 mAlertListener
->Observe(nullptr, "alertfinished", mCookie
.get());
859 mSentFinished
= true;
863 ToastNotificationHandler::OnActivate(
864 const ComPtr
<IToastNotification
>& notification
,
865 const ComPtr
<IInspectable
>& inspectable
) {
866 MOZ_LOG(sWASLog
, LogLevel::Info
, ("OnActivate"));
868 if (mAlertListener
) {
869 // Extract the `action` value from the argument string.
870 nsAutoString argumentsString
;
871 nsAutoString actionString
;
873 ComPtr
<IToastActivatedEventArgs
> eventArgs
;
874 HRESULT hr
= inspectable
.As(&eventArgs
);
877 hr
= eventArgs
->get_Arguments(arguments
.GetAddressOf());
880 const char16_t
* buffer
= (char16_t
*)arguments
.GetRawBuffer(&len
);
882 MOZ_LOG(sWASLog
, LogLevel::Info
,
883 ("OnActivate: arguments: %s",
884 NS_ConvertUTF16toUTF8(buffer
).get()));
885 argumentsString
.Assign(buffer
);
887 // Toast arguments are a newline separated key/value combination of
888 // launch arguments and an optional action argument provided as an
889 // argument to the toast's constructor. After the `action` key is
890 // found, the remainder of toast argument (including newlines) is
891 // the `action` value.
892 Tokenizer16
parse(buffer
);
893 nsDependentSubstring token
;
895 while (parse
.ReadUntil(Tokenizer16::Token::NewLine(), token
)) {
896 if (token
== nsDependentString(kLaunchArgAction
)) {
897 Unused
<< parse
.ReadUntil(Tokenizer16::Token::EndOfFile(),
900 // Next line is a value in a key/value pair, skip.
901 parse
.SkipUntil(Tokenizer16::Token::NewLine());
904 Tokenizer16::Token unused
;
905 Unused
<< parse
.Next(unused
);
912 if (argumentsString
.EqualsLiteral("dismiss")) {
913 // XXX: Somehow Windows still fires OnActivate instead of OnDismiss for
914 // supposedly system managed dismiss button (with activationType=system
915 // and arguments=dismiss). We have to manually treat such callback as a
916 // dismiss action. For this case `arguments` only includes a keyword so we
917 // don't need to compare with a parsed result.
919 } else if (actionString
.EqualsLiteral("settings")) {
920 mAlertListener
->Observe(nullptr, "alertsettingscallback", mCookie
.get());
921 } else if (actionString
.EqualsLiteral("snooze")) {
922 mAlertListener
->Observe(nullptr, "alertdisablecallback", mCookie
.get());
923 } else if (mClickable
) {
924 // When clicking toast, focus moves to another process, but we want to set
925 // focus on Firefox process.
926 nsCOMPtr
<nsIWindowMediator
> winMediator(
927 do_GetService(NS_WINDOWMEDIATOR_CONTRACTID
));
929 nsCOMPtr
<mozIDOMWindowProxy
> navWin
;
930 winMediator
->GetMostRecentBrowserWindow(getter_AddRefs(navWin
));
932 nsCOMPtr
<nsIWidget
> widget
=
933 WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin
));
936 static_cast<HWND
>(widget
->GetNativeData(NS_NATIVE_WINDOW
)));
941 if (mHandleActions
) {
942 Json::Value jsonData
;
943 Json::Reader jsonReader
;
945 if (jsonReader
.parse(NS_ConvertUTF16toUTF8(actionString
).get(),
947 char actionKey
[] = "action";
948 if (jsonData
.isMember(actionKey
) && jsonData
[actionKey
].isString()) {
949 mAlertListener
->Observe(
950 nullptr, "alertactioncallback",
951 NS_ConvertUTF8toUTF16(jsonData
[actionKey
].asCString()).get());
956 mAlertListener
->Observe(nullptr, "alertclickcallback", mCookie
.get());
959 mBackend
->RemoveHandler(mName
, this);
963 // Returns `nullptr` if no such toast exists.
964 /* static */ ComPtr
<IToastNotification
>
965 ToastNotificationHandler::FindNotificationByTag(const nsAString
& aWindowsTag
,
966 const nsAString
& aAumid
) {
970 current_id
.Set(PromiseFlatString(aWindowsTag
).get());
972 ComPtr
<IToastNotificationManagerStatics
> manager
=
973 GetToastNotificationManagerStatics();
974 NS_ENSURE_TRUE(manager
, nullptr);
976 ComPtr
<IToastNotificationManagerStatics2
> manager2
;
977 hr
= manager
.As(&manager2
);
978 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
980 ComPtr
<IToastNotificationHistory
> history
;
981 hr
= manager2
->get_History(&history
);
982 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
983 ComPtr
<IToastNotificationHistory2
> history2
;
984 hr
= history
.As(&history2
);
985 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
987 ComPtr
<IVectorView_ToastNotification
> toasts
;
988 hr
= history2
->GetHistoryWithId(
989 HStringReference(PromiseFlatString(aAumid
).get()).Get(), &toasts
);
990 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
992 unsigned int hist_size
;
993 hr
= toasts
->get_Size(&hist_size
);
994 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
995 for (unsigned int i
= 0; i
< hist_size
; i
++) {
996 ComPtr
<IToastNotification
> hist_toast
;
997 hr
= toasts
->GetAt(i
, &hist_toast
);
998 if (NS_WARN_IF(FAILED(hr
))) {
1002 ComPtr
<IToastNotification2
> hist_toast2
;
1003 hr
= hist_toast
.As(&hist_toast2
);
1004 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
1007 hr
= hist_toast2
->get_Tag(history_id
.GetAddressOf());
1008 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
1010 // We can not directly compare IToastNotification objects; their IUnknown
1011 // pointers should be equivalent but under inspection were not. Therefore we
1012 // use the notification's tag instead.
1013 if (current_id
== history_id
) {
1021 // A single toast message can receive multiple dismiss events, at most one for
1022 // the popup and at most one for the action center. We can't simply count
1023 // dismiss events as the user may have disabled either popups or action center
1024 // notifications, therefore we have to check if the toast remains in the history
1025 // (action center) to determine if the toast is fully dismissed.
1027 ToastNotificationHandler::OnDismiss(
1028 const ComPtr
<IToastNotification
>& notification
,
1029 const ComPtr
<IToastDismissedEventArgs
>& aArgs
) {
1030 ComPtr
<IToastNotification2
> notification2
;
1031 HRESULT hr
= notification
.As(¬ification2
);
1032 NS_ENSURE_TRUE(SUCCEEDED(hr
), E_FAIL
);
1035 hr
= notification2
->get_Tag(tagHString
.GetAddressOf());
1036 NS_ENSURE_TRUE(SUCCEEDED(hr
), E_FAIL
);
1039 const wchar_t* tagPtr
= tagHString
.GetRawBuffer(&len
);
1040 nsAutoString
tag(tagPtr
, len
);
1042 if (FindNotificationByTag(tag
, mAumid
)) {
1047 mBackend
->RemoveHandler(mName
, this);
1052 ToastNotificationHandler::OnFail(const ComPtr
<IToastNotification
>& notification
,
1053 const ComPtr
<IToastFailedEventArgs
>& aArgs
) {
1055 aArgs
->get_ErrorCode(&err
);
1056 MOZ_LOG(sWASLog
, LogLevel::Error
,
1057 ("Error creating notification, error: %ld", err
));
1059 if (mHandleActions
) {
1060 mAlertListener
->Observe(nullptr, "alerterror", mCookie
.get());
1064 mBackend
->RemoveHandler(mName
, this);
1068 nsresult
ToastNotificationHandler::TryShowAlert() {
1069 if (NS_WARN_IF(!ShowAlert())) {
1070 mBackend
->RemoveHandler(mName
, this);
1071 return NS_ERROR_FAILURE
;
1077 ToastNotificationHandler::OnImageMissing(nsISupports
*) {
1078 return TryShowAlert();
1082 ToastNotificationHandler::OnImageReady(nsISupports
*, imgIRequest
* aRequest
) {
1083 nsresult rv
= AsyncSaveImage(aRequest
);
1084 if (NS_FAILED(rv
)) {
1085 return TryShowAlert();
1090 nsresult
ToastNotificationHandler::AsyncSaveImage(imgIRequest
* aRequest
) {
1092 NS_GetSpecialDirectory(NS_OS_TEMP_DIR
, getter_AddRefs(mImageFile
));
1093 NS_ENSURE_SUCCESS(rv
, rv
);
1095 rv
= mImageFile
->Append(u
"notificationimages"_ns
);
1096 NS_ENSURE_SUCCESS(rv
, rv
);
1098 rv
= mImageFile
->Create(nsIFile::DIRECTORY_TYPE
, 0500);
1099 if (NS_FAILED(rv
) && rv
!= NS_ERROR_FILE_ALREADY_EXISTS
) {
1104 rv
= nsID::GenerateUUIDInPlace(uuid
);
1105 NS_ENSURE_SUCCESS(rv
, rv
);
1107 NSID_TrimBracketsASCII
uuidStr(uuid
);
1108 uuidStr
.AppendLiteral(".png");
1109 mImageFile
->AppendNative(uuidStr
);
1111 nsCOMPtr
<imgIContainer
> imgContainer
;
1112 rv
= aRequest
->GetImage(getter_AddRefs(imgContainer
));
1113 NS_ENSURE_SUCCESS(rv
, rv
);
1115 nsMainThreadPtrHandle
<ToastNotificationHandler
> self(
1116 new nsMainThreadPtrHolder
<ToastNotificationHandler
>(
1117 "ToastNotificationHandler", this));
1119 nsCOMPtr
<nsIFile
> imageFile(mImageFile
);
1120 RefPtr
<mozilla::gfx::SourceSurface
> surface
= imgContainer
->GetFrame(
1121 imgIContainer::FRAME_FIRST
,
1122 imgIContainer::FLAG_SYNC_DECODE
| imgIContainer::FLAG_ASYNC_NOTIFY
);
1123 nsCOMPtr
<nsIRunnable
> r
= NS_NewRunnableFunction(
1124 "ToastNotificationHandler::AsyncWriteImage",
1125 [self
, imageFile
, surface
]() -> void {
1126 nsresult rv
= NS_ERROR_FAILURE
;
1128 FILE* file
= nullptr;
1129 rv
= imageFile
->OpenANSIFileDesc("wb", &file
);
1130 if (NS_SUCCEEDED(rv
)) {
1131 rv
= gfxUtils::EncodeSourceSurface(surface
, ImageType::PNG
, u
""_ns
,
1132 gfxUtils::eBinaryEncode
, file
);
1137 nsCOMPtr
<nsIRunnable
> cbRunnable
= NS_NewRunnableFunction(
1138 "ToastNotificationHandler::AsyncWriteImageCb",
1139 [self
, rv
]() -> void {
1140 auto handler
= const_cast<ToastNotificationHandler
*>(self
.get());
1141 handler
->OnWriteImageFinished(rv
);
1144 NS_DispatchToMainThread(cbRunnable
);
1147 return mBackend
->BackgroundDispatch(r
);
1150 void ToastNotificationHandler::OnWriteImageFinished(nsresult rv
) {
1151 if (NS_SUCCEEDED(rv
)) {
1152 OnWriteImageSuccess();
1157 nsresult
ToastNotificationHandler::OnWriteImageSuccess() {
1160 nsCOMPtr
<nsIURI
> fileURI
;
1161 rv
= NS_NewFileURI(getter_AddRefs(fileURI
), mImageFile
);
1162 NS_ENSURE_SUCCESS(rv
, rv
);
1164 nsAutoCString uriStr
;
1165 rv
= fileURI
->GetSpec(uriStr
);
1166 NS_ENSURE_SUCCESS(rv
, rv
);
1168 AppendUTF8toUTF16(uriStr
, mImageUri
);
1175 } // namespace widget
1176 } // namespace mozilla