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 "imgIContainer.h"
13 #include "imgIRequest.h"
14 #include "mozilla/gfx/2D.h"
15 #ifdef MOZ_BACKGROUNDTASKS
16 # include "mozilla/BackgroundTasks.h"
18 #include "mozilla/HashFunctions.h"
19 #include "mozilla/JSONStringWriteFuncs.h"
20 #include "mozilla/Result.h"
21 #include "mozilla/Logging.h"
22 #include "mozilla/Tokenizer.h"
23 #include "mozilla/Unused.h"
24 #include "mozilla/WindowsVersion.h"
25 #include "mozilla/intl/Localization.h"
26 #include "nsAppDirectoryServiceDefs.h"
27 #include "nsAppRunner.h"
28 #include "nsDirectoryServiceDefs.h"
29 #include "nsDirectoryServiceUtils.h"
30 #include "nsIDUtils.h"
31 #include "nsIStringBundle.h"
32 #include "nsIToolkitProfile.h"
33 #include "nsIToolkitProfileService.h"
35 #include "nsIWidget.h"
36 #include "nsIWindowMediator.h"
37 #include "nsNetUtil.h"
38 #include "nsPIDOMWindow.h"
39 #include "nsProxyRelease.h"
40 #include "nsXREDirProvider.h"
41 #include "ToastNotificationHeaderOnlyUtils.h"
42 #include "WidgetUtils.h"
45 #include "ToastNotification.h"
50 extern LazyLogModule sWASLog
;
52 using namespace ABI::Windows::Data::Xml::Dom
;
53 using namespace ABI::Windows::Foundation
;
54 using namespace ABI::Windows::UI::Notifications
;
55 using namespace Microsoft::WRL
;
56 using namespace Microsoft::WRL::Wrappers
;
57 using namespace toastnotification
;
59 // Needed to disambiguate internal and Windows `ToastNotification` classes.
60 using WinToastNotification
= ABI::Windows::UI::Notifications::ToastNotification
;
61 using ToastActivationHandler
=
62 ITypedEventHandler
<WinToastNotification
*, IInspectable
*>;
63 using ToastDismissedHandler
=
64 ITypedEventHandler
<WinToastNotification
*, ToastDismissedEventArgs
*>;
65 using ToastFailedHandler
=
66 ITypedEventHandler
<WinToastNotification
*, ToastFailedEventArgs
*>;
67 using IVectorView_ToastNotification
=
68 Collections::IVectorView
<WinToastNotification
*>;
70 NS_IMPL_ISUPPORTS(ToastNotificationHandler
, nsIAlertNotificationImageListener
)
72 static bool SetNodeValueString(const nsString
& aString
, IXmlNode
* node
,
74 ComPtr
<IXmlText
> inputText
;
76 hr
= xml
->CreateTextNode(HStringReference(aString
.get()).Get(), &inputText
);
77 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
79 ComPtr
<IXmlNode
> inputTextNode
;
80 hr
= inputText
.As(&inputTextNode
);
81 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
83 ComPtr
<IXmlNode
> appendedChild
;
84 hr
= node
->AppendChild(inputTextNode
.Get(), &appendedChild
);
85 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
90 static bool SetAttribute(ComPtr
<IXmlElement
>& element
,
91 const HStringReference
& name
, const nsAString
& value
) {
93 valueStr
.Set(PromiseFlatString(value
).get());
95 HRESULT hr
= element
->SetAttribute(name
.Get(), valueStr
.Get());
96 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
101 static bool AddActionNode(ComPtr
<IXmlDocument
>& toastXml
,
102 ComPtr
<IXmlNode
>& actionsNode
,
103 const nsAString
& actionTitle
,
104 const nsAString
& launchArg
,
105 const nsAString
& actionArgs
,
106 const nsAString
& actionPlacement
= u
""_ns
,
107 const nsAString
& activationType
= u
""_ns
) {
108 ComPtr
<IXmlElement
> action
;
110 toastXml
->CreateElement(HStringReference(L
"action").Get(), &action
);
111 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
114 SetAttribute(action
, HStringReference(L
"content"), actionTitle
);
115 NS_ENSURE_TRUE(success
, false);
117 // Action arguments overwrite the toast's launch arguments, so we need to
118 // prepend the launch arguments necessary for the Notification Server to
119 // reconstruct the toast's origin.
121 // Web Notification actions are arbitrary strings; to prevent breaking launch
122 // argument parsing the action argument must be last. All delimiters after
123 // `action` are part of the action arugment.
124 nsAutoString args
= launchArg
+ u
"\n"_ns
+
125 nsDependentString(kLaunchArgAction
) + u
"\n"_ns
+
127 success
= SetAttribute(action
, HStringReference(L
"arguments"), args
);
128 NS_ENSURE_TRUE(success
, false);
130 if (!actionPlacement
.IsEmpty()) {
132 SetAttribute(action
, HStringReference(L
"placement"), actionPlacement
);
133 NS_ENSURE_TRUE(success
, false);
136 if (!activationType
.IsEmpty()) {
137 success
= SetAttribute(action
, HStringReference(L
"activationType"),
139 NS_ENSURE_TRUE(success
, false);
141 // No special argument handling: when `activationType="system"`, `action` is
142 // a Windows-specific keyword, generally "dismiss" or "snooze".
143 success
= SetAttribute(action
, HStringReference(L
"arguments"), actionArgs
);
144 NS_ENSURE_TRUE(success
, false);
147 // Add <action> to <actions>
148 ComPtr
<IXmlNode
> actionNode
;
149 hr
= action
.As(&actionNode
);
150 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
152 ComPtr
<IXmlNode
> appendedChild
;
153 hr
= actionsNode
->AppendChild(actionNode
.Get(), &appendedChild
);
154 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
159 nsresult
ToastNotificationHandler::GetWindowsTag(nsAString
& aWindowsTag
) {
160 aWindowsTag
.Assign(mWindowsTag
);
164 nsresult
ToastNotificationHandler::SetWindowsTag(const nsAString
& aWindowsTag
) {
165 mWindowsTag
.Assign(aWindowsTag
);
169 // clang - format off
170 /* Populate the launch argument so the COM server can reconstruct the toast
179 Result
<nsString
, nsresult
> ToastNotificationHandler::GetLaunchArgument() {
182 // When the preference is false, the COM notification server will be invoked,
183 // discover that there is no `program`, and exit (successfully), after which
184 // Windows will invoke the in-product Windows 8-style callbacks. When true,
185 // the COM notification server will launch Firefox with sufficient arguments
186 // for Firefox to handle the notification.
187 if (!Preferences::GetBool(
188 "alerts.useSystemBackend.windows.notificationserver.enabled",
190 // Include dummy key/value so that newline appended arguments aren't off by
192 launchArg
+= u
"invalid key\ninvalid value"_ns
;
196 // `program` argument.
197 launchArg
+= nsDependentString(kLaunchArgProgram
) + u
"\n"_ns MOZ_APP_NAME
;
199 // `profile` argument.
200 nsCOMPtr
<nsIFile
> profDir
;
201 bool wantCurrentProfile
= true;
202 #ifdef MOZ_BACKGROUNDTASKS
203 if (BackgroundTasks::IsBackgroundTaskMode()) {
204 // Notifications popped from a background task want to invoke Firefox with a
205 // different profile -- the default browsing profile. We'd prefer to not
206 // specify a profile, so that the Firefox invoked by the notification server
207 // chooses its default profile, but this might pop the profile chooser in
208 // some configurations.
209 wantCurrentProfile
= false;
211 nsCOMPtr
<nsIToolkitProfileService
> profileSvc
=
212 do_GetService(NS_PROFILESERVICE_CONTRACTID
);
214 nsCOMPtr
<nsIToolkitProfile
> defaultProfile
;
216 profileSvc
->GetDefaultProfile(getter_AddRefs(defaultProfile
));
217 if (NS_SUCCEEDED(rv
) && defaultProfile
) {
218 // Not all installations have a default profile. But if one is set,
219 // then it should have a profile directory.
220 MOZ_TRY(defaultProfile
->GetRootDir(getter_AddRefs(profDir
)));
225 if (wantCurrentProfile
) {
226 MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR
,
227 getter_AddRefs(profDir
)));
231 nsAutoString profilePath
;
232 MOZ_TRY(profDir
->GetPath(profilePath
));
233 launchArg
+= u
"\n"_ns
+ nsDependentString(kLaunchArgProfile
) + u
"\n"_ns
+
237 // `windowsTag` argument.
239 u
"\n"_ns
+ nsDependentString(kLaunchArgTag
) + u
"\n"_ns
+ mWindowsTag
;
241 // `logging` argument.
242 if (Preferences::GetBool(
243 "alerts.useSystemBackend.windows.notificationserver.verbose",
245 // Signal notification to log verbose messages.
247 u
"\n"_ns
+ nsDependentString(kLaunchArgLogging
) + u
"\nverbose"_ns
;
253 static ComPtr
<IToastNotificationManagerStatics
>
254 GetToastNotificationManagerStatics() {
255 ComPtr
<IToastNotificationManagerStatics
> toastNotificationManagerStatics
;
256 HRESULT hr
= GetActivationFactory(
258 RuntimeClass_Windows_UI_Notifications_ToastNotificationManager
)
260 &toastNotificationManagerStatics
);
261 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
263 return toastNotificationManagerStatics
;
266 ToastNotificationHandler::~ToastNotificationHandler() {
268 mImageRequest
->Cancel(NS_BINDING_ABORTED
);
269 mImageRequest
= nullptr;
272 if (mHasImage
&& mImageFile
) {
273 DebugOnly
<nsresult
> rv
= mImageFile
->Remove(false);
274 NS_ASSERTION(NS_SUCCEEDED(rv
), "Cannot remove temporary image file");
280 void ToastNotificationHandler::UnregisterHandler() {
282 mNotification
->remove_Dismissed(mDismissedToken
);
283 mNotification
->remove_Activated(mActivatedToken
);
284 mNotification
->remove_Failed(mFailedToken
);
287 mNotification
= nullptr;
293 nsresult
ToastNotificationHandler::InitAlertAsync(
294 nsIAlertNotification
* aAlert
) {
295 MOZ_TRY(InitWindowsTag());
297 return aAlert
->LoadImage(/* aTimeout = */ 0, this, /* aUserData = */ nullptr,
298 getter_AddRefs(mImageRequest
));
301 // Uniquely identify this toast to Windows. Existing names and cookies are not
302 // suitable: we want something generated and unique. This is needed to check if
303 // toast is still present in the Windows Action Center when we receive a dismiss
306 // Local testing reveals that the space of tags is not global but instead is per
307 // AUMID. Since an installation uses a unique AUMID incorporating the install
308 // directory hash, it should not witness another installation's tag.
309 nsresult
ToastNotificationHandler::InitWindowsTag() {
310 mWindowsTag
.Truncate();
314 // Multiple profiles might overwrite each other's toast messages when a
315 // common name is used for a given host port. We prevent this by including
316 // the profile directory as part of the toast hash.
317 nsCOMPtr
<nsIFile
> profDir
;
318 MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR
,
319 getter_AddRefs(profDir
)));
320 MOZ_TRY(profDir
->GetPath(tag
));
322 if (!mHostPort
.IsEmpty()) {
323 // Notification originated from a web notification.
324 // `mName` will be in the form `{mHostPort}#tag:{tag}` if the notification
325 // was created with a tag and `{mHostPort}#notag:{uuid}` otherwise.
328 // Notification originated from the browser chrome.
329 if (!mName
.IsEmpty()) {
330 tag
+= u
"chrome#tag:"_ns
;
331 // Browser chrome notifications don't follow any convention for naming.
334 // No associated name, append a UUID to prevent reuse of the same tag.
335 nsIDToCString
uuidString(nsID::GenerateUUID());
336 size_t len
= strlen(uuidString
.get());
337 MOZ_ASSERT(len
== NSID_LENGTH
- 1);
339 CopyASCIItoUTF16(nsDependentCSubstring(uuidString
.get(), len
), uuid
);
341 tag
+= u
"chrome#notag:"_ns
;
346 // Windows notification tags are limited to 16 characters, or 64 characters
347 // after the Creators Update; therefore we hash the tag to fit the minimum
349 HashNumber hash
= HashString(tag
);
350 mWindowsTag
.AppendPrintf("%010u", hash
);
355 nsString
ToastNotificationHandler::ActionArgsJSONString(
356 const nsString
& aAction
, const nsString
& aOpaqueRelaunchData
= u
""_ns
) {
357 nsAutoCString actionArgsData
;
359 JSONStringRefWriteFunc
js(actionArgsData
);
360 JSONWriter
w(js
, JSONWriter::SingleLineStyle
);
363 w
.StringProperty("action", NS_ConvertUTF16toUTF8(aAction
));
365 if (mIsSystemPrincipal
) {
366 // Privileged/chrome alerts (not activated by Windows) can have custom
368 if (!aOpaqueRelaunchData
.IsEmpty()) {
369 w
.StringProperty("opaqueRelaunchData",
370 NS_ConvertUTF16toUTF8(aOpaqueRelaunchData
));
373 // Privileged alerts include any provided name for metrics.
374 if (!mName
.IsEmpty()) {
375 w
.StringProperty("privilegedName", NS_ConvertUTF16toUTF8(mName
));
378 if (!mHostPort
.IsEmpty()) {
379 w
.StringProperty("launchUrl", NS_ConvertUTF16toUTF8(mHostPort
));
385 return NS_ConvertUTF8toUTF16(actionArgsData
);
388 ComPtr
<IXmlDocument
> ToastNotificationHandler::CreateToastXmlDocument() {
389 ComPtr
<IToastNotificationManagerStatics
> toastNotificationManagerStatics
=
390 GetToastNotificationManagerStatics();
391 NS_ENSURE_TRUE(toastNotificationManagerStatics
, nullptr);
393 ToastTemplateType toastTemplate
;
394 if (mHostPort
.IsEmpty()) {
396 mHasImage
? ToastTemplateType::ToastTemplateType_ToastImageAndText03
397 : ToastTemplateType::ToastTemplateType_ToastText03
;
400 mHasImage
? ToastTemplateType::ToastTemplateType_ToastImageAndText04
401 : ToastTemplateType::ToastTemplateType_ToastText04
;
404 ComPtr
<IXmlDocument
> toastXml
;
405 toastNotificationManagerStatics
->GetTemplateContent(toastTemplate
, &toastXml
);
416 ComPtr
<IXmlNodeList
> toastImageElements
;
417 hr
= toastXml
->GetElementsByTagName(HStringReference(L
"image").Get(),
418 &toastImageElements
);
419 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
421 ComPtr
<IXmlNode
> imageNode
;
422 hr
= toastImageElements
->Item(0, &imageNode
);
423 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
425 ComPtr
<IXmlElement
> image
;
426 hr
= imageNode
.As(&image
);
427 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
429 success
= SetAttribute(image
, HStringReference(L
"src"), mImageUri
);
430 NS_ENSURE_TRUE(success
, nullptr);
433 ComPtr
<IXmlNodeList
> toastTextElements
;
434 hr
= toastXml
->GetElementsByTagName(HStringReference(L
"text").Get(),
436 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
438 ComPtr
<IXmlNode
> titleTextNodeRoot
;
439 hr
= toastTextElements
->Item(0, &titleTextNodeRoot
);
440 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
442 ComPtr
<IXmlNode
> msgTextNodeRoot
;
443 hr
= toastTextElements
->Item(1, &msgTextNodeRoot
);
444 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
446 success
= SetNodeValueString(mTitle
, titleTextNodeRoot
.Get(), toastXml
.Get());
447 NS_ENSURE_TRUE(success
, nullptr);
449 success
= SetNodeValueString(mMsg
, msgTextNodeRoot
.Get(), toastXml
.Get());
450 NS_ENSURE_TRUE(success
, nullptr);
452 ComPtr
<IXmlNodeList
> toastElements
;
453 hr
= toastXml
->GetElementsByTagName(HStringReference(L
"toast").Get(),
455 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
457 ComPtr
<IXmlNode
> toastNodeRoot
;
458 hr
= toastElements
->Item(0, &toastNodeRoot
);
459 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
461 ComPtr
<IXmlElement
> toastElement
;
462 hr
= toastNodeRoot
.As(&toastElement
);
463 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
465 if (mRequireInteraction
) {
466 success
= SetAttribute(toastElement
, HStringReference(L
"scenario"),
468 NS_ENSURE_TRUE(success
, nullptr);
471 auto maybeLaunchArg
= GetLaunchArgument();
472 NS_ENSURE_TRUE(maybeLaunchArg
.isOk(), nullptr);
473 nsString launchArg
= maybeLaunchArg
.unwrap();
475 nsString launchArgWithoutAction
= launchArg
;
477 if (!mIsSystemPrincipal
) {
478 // Unprivileged/content alerts can't have custom relaunch data.
479 NS_WARNING_ASSERTION(mOpaqueRelaunchData
.IsEmpty(),
480 "unprivileged/content alert "
481 "should have trivial `mOpaqueRelaunchData`");
484 launchArg
+= u
"\n"_ns
+ nsDependentString(kLaunchArgAction
) + u
"\n"_ns
+
485 ActionArgsJSONString(u
""_ns
, mOpaqueRelaunchData
);
487 success
= SetAttribute(toastElement
, HStringReference(L
"launch"), launchArg
);
488 NS_ENSURE_TRUE(success
, nullptr);
490 MOZ_LOG(sWASLog
, LogLevel::Debug
,
491 ("launchArg: '%s'", NS_ConvertUTF16toUTF8(launchArg
).get()));
493 // Use newer toast layout, which makes images larger, for system
494 // (chrome-privileged) toasts.
495 if (mIsSystemPrincipal
) {
496 ComPtr
<IXmlNodeList
> bindingElements
;
497 hr
= toastXml
->GetElementsByTagName(HStringReference(L
"binding").Get(),
499 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
501 ComPtr
<IXmlNode
> bindingNodeRoot
;
502 hr
= bindingElements
->Item(0, &bindingNodeRoot
);
503 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
505 ComPtr
<IXmlElement
> bindingElement
;
506 hr
= bindingNodeRoot
.As(&bindingElement
);
507 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
509 success
= SetAttribute(bindingElement
, HStringReference(L
"template"),
511 NS_ENSURE_TRUE(success
, nullptr);
514 ComPtr
<IXmlElement
> actions
;
515 hr
= toastXml
->CreateElement(HStringReference(L
"actions").Get(), &actions
);
516 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
518 ComPtr
<IXmlNode
> actionsNode
;
519 hr
= actions
.As(&actionsNode
);
520 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
522 nsCOMPtr
<nsIStringBundleService
> sbs
=
523 do_GetService(NS_STRINGBUNDLE_CONTRACTID
);
524 NS_ENSURE_TRUE(sbs
, nullptr);
526 nsCOMPtr
<nsIStringBundle
> bundle
;
527 sbs
->CreateBundle("chrome://alerts/locale/alert.properties",
528 getter_AddRefs(bundle
));
529 NS_ENSURE_TRUE(bundle
, nullptr);
531 if (!mHostPort
.IsEmpty()) {
532 AutoTArray
<nsString
, 1> formatStrings
= {mHostPort
};
534 ComPtr
<IXmlNode
> urlTextNodeRoot
;
535 hr
= toastTextElements
->Item(2, &urlTextNodeRoot
);
536 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
538 nsAutoString urlReference
;
539 bundle
->FormatStringFromName("source.label", formatStrings
, urlReference
);
542 SetNodeValueString(urlReference
, urlTextNodeRoot
.Get(), toastXml
.Get());
543 NS_ENSURE_TRUE(success
, nullptr);
545 if (IsWin10AnniversaryUpdateOrLater()) {
546 ComPtr
<IXmlElement
> placementText
;
547 hr
= urlTextNodeRoot
.As(&placementText
);
549 // placement is supported on Windows 10 Anniversary Update or later
550 SetAttribute(placementText
, HStringReference(L
"placement"),
555 nsAutoString disableButtonTitle
;
556 ns
= bundle
->FormatStringFromName("webActions.disableForOrigin.label",
557 formatStrings
, disableButtonTitle
);
558 NS_ENSURE_SUCCESS(ns
, nullptr);
560 AddActionNode(toastXml
, actionsNode
, disableButtonTitle
,
561 // TODO: launch into `about:preferences`?
562 launchArgWithoutAction
, ActionArgsJSONString(u
"snooze"_ns
),
566 bool wantSettings
= true;
567 #ifdef MOZ_BACKGROUNDTASKS
568 if (BackgroundTasks::IsBackgroundTaskMode()) {
569 // Notifications popped from a background task want to invoke Firefox with a
570 // different profile -- the default browsing profile. Don't link to Firefox
571 // settings in some different profile: the relevant Firefox settings won't
573 wantSettings
= false;
576 if (MOZ_LIKELY(wantSettings
)) {
577 nsAutoString settingsButtonTitle
;
578 bundle
->GetStringFromName("webActions.settings.label", settingsButtonTitle
);
579 success
= AddActionNode(
580 toastXml
, actionsNode
, settingsButtonTitle
, launchArgWithoutAction
,
581 // TODO: launch into `about:preferences`?
582 ActionArgsJSONString(u
"settings"_ns
), u
"contextmenu"_ns
);
583 NS_ENSURE_TRUE(success
, nullptr);
586 for (const auto& action
: mActions
) {
587 // Bug 1778596: include per-action icon from image URL.
589 ns
= action
->GetTitle(title
);
590 NS_ENSURE_SUCCESS(ns
, nullptr);
592 nsString actionString
;
593 ns
= action
->GetAction(actionString
);
594 NS_ENSURE_SUCCESS(ns
, nullptr);
596 nsString opaqueRelaunchData
;
597 ns
= action
->GetOpaqueRelaunchData(opaqueRelaunchData
);
598 NS_ENSURE_SUCCESS(ns
, nullptr);
600 MOZ_LOG(sWASLog
, LogLevel::Debug
,
601 ("launchArgWithoutAction for '%s': '%s'",
602 NS_ConvertUTF16toUTF8(actionString
).get(),
603 NS_ConvertUTF16toUTF8(launchArgWithoutAction
).get()));
605 // Privileged/chrome alerts can have actions that are activated by Windows.
606 // Recognize these actions and enable these activations.
607 bool activationType(false);
608 ns
= action
->GetWindowsSystemActivationType(&activationType
);
609 NS_ENSURE_SUCCESS(ns
, nullptr);
611 nsString
activationTypeString(
612 (mIsSystemPrincipal
&& activationType
) ? u
"system"_ns
: u
""_ns
);
615 if (mIsSystemPrincipal
&& activationType
) {
616 // Privileged/chrome alerts that are activated by Windows can't have
617 // custom relaunch data.
618 actionArgs
= actionString
;
620 NS_WARNING_ASSERTION(opaqueRelaunchData
.IsEmpty(),
621 "action with `windowsSystemActivationType=true` "
622 "should have trivial `opaqueRelaunchData`");
624 actionArgs
= ActionArgsJSONString(actionString
, opaqueRelaunchData
);
627 success
= AddActionNode(toastXml
, actionsNode
, title
,
628 /* launchArg */ launchArgWithoutAction
,
629 /* actionArgs */ actionArgs
,
630 /* actionPlacement */ u
""_ns
,
631 /* activationType */ activationTypeString
);
632 NS_ENSURE_TRUE(success
, nullptr);
635 // Windows ignores scenario=reminder added by mRequiredInteraction if
636 // there's no non-contextmenu activationType=background action.
637 if (mRequireInteraction
&& !mActions
.Length()) {
638 nsTArray
<nsCString
> resIds
= {
639 "toolkit/global/alert.ftl"_ns
,
641 RefPtr
<intl::Localization
> l10n
= intl::Localization::Create(resIds
, true);
642 IgnoredErrorResult rv
;
643 nsAutoCString closeTitle
;
644 l10n
->FormatValueSync("notification-default-dismiss"_ns
, {}, closeTitle
,
646 NS_ENSURE_TRUE(!rv
.Failed(), nullptr);
649 AddActionNode(toastXml
, actionsNode
, NS_ConvertUTF8toUTF16(closeTitle
),
650 launchArg
, u
""_ns
, u
""_ns
, u
"background"_ns
),
654 ComPtr
<IXmlNode
> appendedChild
;
655 hr
= toastNodeRoot
->AppendChild(actionsNode
.Get(), &appendedChild
);
656 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
659 ComPtr
<IXmlNode
> audioNode
;
660 // Create <audio silent="true"/> for silent notifications.
661 ComPtr
<IXmlElement
> audio
;
662 hr
= toastXml
->CreateElement(HStringReference(L
"audio").Get(), &audio
);
663 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
665 SetAttribute(audio
, HStringReference(L
"silent"), u
"true"_ns
);
667 hr
= audio
.As(&audioNode
);
668 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
669 hr
= toastNodeRoot
->AppendChild(audioNode
.Get(), &appendedChild
);
670 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
676 nsresult
ToastNotificationHandler::CreateToastXmlString(
677 const nsAString
& aImageURL
, nsAString
& aString
) {
680 if (!aImageURL
.IsEmpty()) {
681 // For testing: don't fetch and write image to disk, just include the URL.
683 mImageUri
.Assign(aImageURL
);
686 ComPtr
<IXmlDocument
> toastXml
= CreateToastXmlDocument();
688 return NS_ERROR_FAILURE
;
691 ComPtr
<IXmlNodeSerializer
> ser
;
692 hr
= toastXml
.As(&ser
);
693 NS_ENSURE_TRUE(SUCCEEDED(hr
), NS_ERROR_FAILURE
);
696 hr
= ser
->GetXml(data
.GetAddressOf());
697 NS_ENSURE_TRUE(SUCCEEDED(hr
), NS_ERROR_FAILURE
);
700 const wchar_t* rawData
= data
.GetRawBuffer(&len
);
701 NS_ENSURE_TRUE(rawData
, NS_ERROR_FAILURE
);
702 aString
.Assign(rawData
, len
);
707 bool ToastNotificationHandler::ShowAlert() {
708 if (!mBackend
->IsActiveHandler(mName
, this)) {
712 ComPtr
<IXmlDocument
> toastXml
= CreateToastXmlDocument();
718 return CreateWindowsNotificationFromXml(toastXml
);
721 bool ToastNotificationHandler::IsPrivate() { return mInPrivateBrowsing
; }
723 void ToastNotificationHandler::HideAlert() {
724 if (mNotifier
&& mNotification
) {
725 mNotifier
->Hide(mNotification
.Get());
729 bool ToastNotificationHandler::CreateWindowsNotificationFromXml(
730 ComPtr
<IXmlDocument
>& aXml
) {
731 ComPtr
<IToastNotificationFactory
> factory
;
734 hr
= GetActivationFactory(
735 HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification
)
738 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
740 hr
= factory
->CreateToastNotification(aXml
.Get(), &mNotification
);
741 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
743 RefPtr
<ToastNotificationHandler
> self
= this;
745 hr
= mNotification
->add_Activated(
746 Callback
<ToastActivationHandler
>([self
](IToastNotification
* aNotification
,
747 IInspectable
* aInspectable
) {
748 return self
->OnActivate(ComPtr
<IToastNotification
>(aNotification
),
749 ComPtr
<IInspectable
>(aInspectable
));
752 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
754 hr
= mNotification
->add_Dismissed(
755 Callback
<ToastDismissedHandler
>([self
](IToastNotification
* aNotification
,
756 IToastDismissedEventArgs
* aArgs
) {
757 return self
->OnDismiss(ComPtr
<IToastNotification
>(aNotification
),
758 ComPtr
<IToastDismissedEventArgs
>(aArgs
));
761 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
763 hr
= mNotification
->add_Failed(
764 Callback
<ToastFailedHandler
>([self
](IToastNotification
* aNotification
,
765 IToastFailedEventArgs
* aArgs
) {
766 return self
->OnFail(ComPtr
<IToastNotification
>(aNotification
),
767 ComPtr
<IToastFailedEventArgs
>(aArgs
));
770 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
772 ComPtr
<IToastNotification2
> notification2
;
773 hr
= mNotification
.As(¬ification2
);
774 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
777 hr
= hTag
.Set(mWindowsTag
.get());
778 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
780 hr
= notification2
->put_Tag(hTag
.Get());
781 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
783 ComPtr
<IToastNotificationManagerStatics
> toastNotificationManagerStatics
=
784 GetToastNotificationManagerStatics();
785 NS_ENSURE_TRUE(toastNotificationManagerStatics
, false);
788 hr
= aumid
.Set(mAumid
.get());
789 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
790 hr
= toastNotificationManagerStatics
->CreateToastNotifierWithId(aumid
.Get(),
792 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
794 hr
= mNotifier
->Show(mNotification
.Get());
795 NS_ENSURE_TRUE(SUCCEEDED(hr
), false);
797 if (mAlertListener
) {
798 mAlertListener
->Observe(nullptr, "alertshow", mCookie
.get());
804 void ToastNotificationHandler::SendFinished() {
805 if (!mSentFinished
&& mAlertListener
) {
806 mAlertListener
->Observe(nullptr, "alertfinished", mCookie
.get());
809 mSentFinished
= true;
813 ToastNotificationHandler::OnActivate(
814 const ComPtr
<IToastNotification
>& notification
,
815 const ComPtr
<IInspectable
>& inspectable
) {
816 MOZ_LOG(sWASLog
, LogLevel::Info
, ("OnActivate"));
818 if (mAlertListener
) {
819 // Extract the `action` value from the argument string.
820 nsAutoString actionString
;
822 ComPtr
<IToastActivatedEventArgs
> eventArgs
;
823 HRESULT hr
= inspectable
.As(&eventArgs
);
826 hr
= eventArgs
->get_Arguments(arguments
.GetAddressOf());
829 const char16_t
* buffer
= (char16_t
*)arguments
.GetRawBuffer(&len
);
831 MOZ_LOG(sWASLog
, LogLevel::Info
,
832 ("OnActivate: arguments: %s",
833 NS_ConvertUTF16toUTF8(buffer
).get()));
835 // Toast arguments are a newline separated key/value combination of
836 // launch arguments and an optional action argument provided as an
837 // argument to the toast's constructor. After the `action` key is
838 // found, the remainder of toast argument (including newlines) is
839 // the `action` value.
840 Tokenizer16
parse(buffer
);
841 nsDependentSubstring token
;
843 while (parse
.ReadUntil(Tokenizer16::Token::NewLine(), token
)) {
844 if (token
== nsDependentString(kLaunchArgAction
)) {
845 Unused
<< parse
.ReadUntil(Tokenizer16::Token::EndOfFile(),
848 // Next line is a value in a key/value pair, skip.
849 parse
.SkipUntil(Tokenizer16::Token::NewLine());
852 Tokenizer16::Token unused
;
853 Unused
<< parse
.Next(unused
);
860 // TODO: extract `action` from `actionString`, which is now JSON.
862 if (actionString
.EqualsLiteral("settings")) {
863 mAlertListener
->Observe(nullptr, "alertsettingscallback", mCookie
.get());
864 } else if (actionString
.EqualsLiteral("snooze")) {
865 mAlertListener
->Observe(nullptr, "alertdisablecallback", mCookie
.get());
866 } else if (mClickable
) {
867 // When clicking toast, focus moves to another process, but we want to set
868 // focus on Firefox process.
869 nsCOMPtr
<nsIWindowMediator
> winMediator(
870 do_GetService(NS_WINDOWMEDIATOR_CONTRACTID
));
872 nsCOMPtr
<mozIDOMWindowProxy
> navWin
;
873 winMediator
->GetMostRecentWindow(u
"navigator:browser",
874 getter_AddRefs(navWin
));
876 nsCOMPtr
<nsIWidget
> widget
=
877 WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin
));
880 static_cast<HWND
>(widget
->GetNativeData(NS_NATIVE_WINDOW
)));
884 mAlertListener
->Observe(nullptr, "alertclickcallback", mCookie
.get());
887 mBackend
->RemoveHandler(mName
, this);
891 // Returns `nullptr` if no such toast exists.
892 /* static */ ComPtr
<IToastNotification
>
893 ToastNotificationHandler::FindNotificationByTag(const nsAString
& aWindowsTag
,
894 const nsAString
& aAumid
) {
898 current_id
.Set(PromiseFlatString(aWindowsTag
).get());
900 ComPtr
<IToastNotificationManagerStatics
> manager
=
901 GetToastNotificationManagerStatics();
902 NS_ENSURE_TRUE(manager
, nullptr);
904 ComPtr
<IToastNotificationManagerStatics2
> manager2
;
905 hr
= manager
.As(&manager2
);
906 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
908 ComPtr
<IToastNotificationHistory
> history
;
909 hr
= manager2
->get_History(&history
);
910 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
911 ComPtr
<IToastNotificationHistory2
> history2
;
912 hr
= history
.As(&history2
);
913 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
915 ComPtr
<IVectorView_ToastNotification
> toasts
;
916 hr
= history2
->GetHistoryWithId(
917 HStringReference(PromiseFlatString(aAumid
).get()).Get(), &toasts
);
918 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
920 unsigned int hist_size
;
921 hr
= toasts
->get_Size(&hist_size
);
922 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
923 for (unsigned int i
= 0; i
< hist_size
; i
++) {
924 ComPtr
<IToastNotification
> hist_toast
;
925 hr
= toasts
->GetAt(i
, &hist_toast
);
926 if (NS_WARN_IF(FAILED(hr
))) {
930 ComPtr
<IToastNotification2
> hist_toast2
;
931 hr
= hist_toast
.As(&hist_toast2
);
932 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
935 hr
= hist_toast2
->get_Tag(history_id
.GetAddressOf());
936 NS_ENSURE_TRUE(SUCCEEDED(hr
), nullptr);
938 // We can not directly compare IToastNotification objects; their IUnknown
939 // pointers should be equivalent but under inspection were not. Therefore we
940 // use the notification's tag instead.
941 if (current_id
== history_id
) {
949 // A single toast message can receive multiple dismiss events, at most one for
950 // the popup and at most one for the action center. We can't simply count
951 // dismiss events as the user may have disabled either popups or action center
952 // notifications, therefore we have to check if the toast remains in the history
953 // (action center) to determine if the toast is fully dismissed.
955 ToastNotificationHandler::OnDismiss(
956 const ComPtr
<IToastNotification
>& notification
,
957 const ComPtr
<IToastDismissedEventArgs
>& aArgs
) {
958 ComPtr
<IToastNotification2
> notification2
;
959 HRESULT hr
= notification
.As(¬ification2
);
960 NS_ENSURE_TRUE(SUCCEEDED(hr
), E_FAIL
);
963 hr
= notification2
->get_Tag(tagHString
.GetAddressOf());
964 NS_ENSURE_TRUE(SUCCEEDED(hr
), E_FAIL
);
967 const wchar_t* tagPtr
= tagHString
.GetRawBuffer(&len
);
968 nsAutoString
tag(tagPtr
, len
);
970 if (FindNotificationByTag(tag
, mAumid
)) {
975 mBackend
->RemoveHandler(mName
, this);
980 ToastNotificationHandler::OnFail(const ComPtr
<IToastNotification
>& notification
,
981 const ComPtr
<IToastFailedEventArgs
>& aArgs
) {
983 aArgs
->get_ErrorCode(&err
);
984 MOZ_LOG(sWASLog
, LogLevel::Error
,
985 ("Error creating notification, error: %ld", err
));
988 mBackend
->RemoveHandler(mName
, this);
992 nsresult
ToastNotificationHandler::TryShowAlert() {
993 if (NS_WARN_IF(!ShowAlert())) {
994 mBackend
->RemoveHandler(mName
, this);
995 return NS_ERROR_FAILURE
;
1001 ToastNotificationHandler::OnImageMissing(nsISupports
*) {
1002 return TryShowAlert();
1006 ToastNotificationHandler::OnImageReady(nsISupports
*, imgIRequest
* aRequest
) {
1007 nsresult rv
= AsyncSaveImage(aRequest
);
1008 if (NS_FAILED(rv
)) {
1009 return TryShowAlert();
1014 nsresult
ToastNotificationHandler::AsyncSaveImage(imgIRequest
* aRequest
) {
1016 NS_GetSpecialDirectory(NS_OS_TEMP_DIR
, getter_AddRefs(mImageFile
));
1017 NS_ENSURE_SUCCESS(rv
, rv
);
1019 rv
= mImageFile
->Append(u
"notificationimages"_ns
);
1020 NS_ENSURE_SUCCESS(rv
, rv
);
1022 rv
= mImageFile
->Create(nsIFile::DIRECTORY_TYPE
, 0500);
1023 if (NS_FAILED(rv
) && rv
!= NS_ERROR_FILE_ALREADY_EXISTS
) {
1028 rv
= nsID::GenerateUUIDInPlace(uuid
);
1029 NS_ENSURE_SUCCESS(rv
, rv
);
1031 NSID_TrimBracketsASCII
uuidStr(uuid
);
1032 uuidStr
.AppendLiteral(".png");
1033 mImageFile
->AppendNative(uuidStr
);
1035 nsCOMPtr
<imgIContainer
> imgContainer
;
1036 rv
= aRequest
->GetImage(getter_AddRefs(imgContainer
));
1037 NS_ENSURE_SUCCESS(rv
, rv
);
1039 nsMainThreadPtrHandle
<ToastNotificationHandler
> self(
1040 new nsMainThreadPtrHolder
<ToastNotificationHandler
>(
1041 "ToastNotificationHandler", this));
1043 nsCOMPtr
<nsIFile
> imageFile(mImageFile
);
1044 RefPtr
<mozilla::gfx::SourceSurface
> surface
= imgContainer
->GetFrame(
1045 imgIContainer::FRAME_FIRST
,
1046 imgIContainer::FLAG_SYNC_DECODE
| imgIContainer::FLAG_ASYNC_NOTIFY
);
1047 nsCOMPtr
<nsIRunnable
> r
= NS_NewRunnableFunction(
1048 "ToastNotificationHandler::AsyncWriteImage",
1049 [self
, imageFile
, surface
]() -> void {
1050 nsresult rv
= NS_ERROR_FAILURE
;
1052 FILE* file
= nullptr;
1053 rv
= imageFile
->OpenANSIFileDesc("wb", &file
);
1054 if (NS_SUCCEEDED(rv
)) {
1055 rv
= gfxUtils::EncodeSourceSurface(surface
, ImageType::PNG
, u
""_ns
,
1056 gfxUtils::eBinaryEncode
, file
);
1061 nsCOMPtr
<nsIRunnable
> cbRunnable
= NS_NewRunnableFunction(
1062 "ToastNotificationHandler::AsyncWriteImageCb",
1063 [self
, rv
]() -> void {
1064 auto handler
= const_cast<ToastNotificationHandler
*>(self
.get());
1065 handler
->OnWriteImageFinished(rv
);
1068 NS_DispatchToMainThread(cbRunnable
);
1071 return mBackend
->BackgroundDispatch(r
);
1074 void ToastNotificationHandler::OnWriteImageFinished(nsresult rv
) {
1075 if (NS_SUCCEEDED(rv
)) {
1076 OnWriteImageSuccess();
1081 nsresult
ToastNotificationHandler::OnWriteImageSuccess() {
1084 nsCOMPtr
<nsIURI
> fileURI
;
1085 rv
= NS_NewFileURI(getter_AddRefs(fileURI
), mImageFile
);
1086 NS_ENSURE_SUCCESS(rv
, rv
);
1088 nsAutoCString uriStr
;
1089 rv
= fileURI
->GetSpec(uriStr
);
1090 NS_ENSURE_SUCCESS(rv
, rv
);
1092 AppendUTF8toUTF16(uriStr
, mImageUri
);
1099 } // namespace widget
1100 } // namespace mozilla