Bug 1931425 - Limit how often moz-label's #setStyles runs r=reusable-components-revie...
[gecko.git] / widget / windows / ToastNotificationHandler.cpp
blob5181a7c500f4ae47ab94ef2fdad80731605ea5a8
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>
11 #include "gfxUtils.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"
19 #endif
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"
36 #include "nsIURI.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"
45 #include "WinUtils.h"
47 #include "ToastNotification.h"
49 namespace mozilla {
50 namespace widget {
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,
75 IXmlDocument* xml) {
76 ComPtr<IXmlText> inputText;
77 HRESULT hr;
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);
89 return true;
92 static bool SetAttribute(ComPtr<IXmlElement>& element,
93 const HStringReference& name, const nsAString& value) {
94 HString valueStr;
95 valueStr.Set(PromiseFlatString(value).get());
97 HRESULT hr = element->SetAttribute(name.Get(), valueStr.Get());
98 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
100 return true;
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;
111 HRESULT hr =
112 toastXml->CreateElement(HStringReference(L"action").Get(), &action);
113 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
115 bool success =
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 +
128 actionArgs;
129 success = SetAttribute(action, HStringReference(L"arguments"), args);
130 NS_ENSURE_TRUE(success, false);
132 if (!actionPlacement.IsEmpty()) {
133 success =
134 SetAttribute(action, HStringReference(L"placement"), actionPlacement);
135 NS_ENSURE_TRUE(success, false);
138 if (!activationType.IsEmpty()) {
139 success = SetAttribute(action, HStringReference(L"activationType"),
140 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);
164 return true;
167 nsresult ToastNotificationHandler::GetWindowsTag(nsAString& aWindowsTag) {
168 aWindowsTag.Assign(mWindowsTag);
169 return NS_OK;
172 nsresult ToastNotificationHandler::SetWindowsTag(const nsAString& aWindowsTag) {
173 mWindowsTag.Assign(aWindowsTag);
174 return NS_OK;
177 // clang - format off
178 /* Populate the launch argument so the COM server can reconstruct the toast
179 * origin.
181 * program
182 * {MOZ_APP_NAME}
183 * profile
184 * {path to profile}
186 // clang-format on
187 Result<nsString, nsresult> ToastNotificationHandler::GetLaunchArgument() {
188 nsString launchArg;
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",
197 false)) {
198 // Include dummy key/value so that newline appended arguments aren't off by
199 // one line.
200 launchArg += u"invalid key\ninvalid value"_ns;
201 return launchArg;
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);
221 if (profileSvc) {
222 nsCOMPtr<nsIToolkitProfile> defaultProfile;
223 nsresult rv =
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)));
232 #endif
233 if (wantCurrentProfile) {
234 MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
235 getter_AddRefs(profDir)));
238 if (profDir) {
239 nsAutoString profilePath;
240 MOZ_TRY(profDir->GetPath(profilePath));
241 launchArg += u"\n"_ns + nsDependentString(kLaunchArgProfile) + u"\n"_ns +
242 profilePath;
245 // `windowsTag` argument.
246 launchArg +=
247 u"\n"_ns + nsDependentString(kLaunchArgTag) + u"\n"_ns + mWindowsTag;
249 // `logging` argument.
250 if (Preferences::GetBool(
251 "alerts.useSystemBackend.windows.notificationserver.verbose",
252 false)) {
253 // Signal notification to log verbose messages.
254 launchArg +=
255 u"\n"_ns + nsDependentString(kLaunchArgLogging) + u"\nverbose"_ns;
258 return launchArg;
261 static ComPtr<IToastNotificationManagerStatics>
262 GetToastNotificationManagerStatics() {
263 ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics;
264 HRESULT hr = GetActivationFactory(
265 HStringReference(
266 RuntimeClass_Windows_UI_Notifications_ToastNotificationManager)
267 .Get(),
268 &toastNotificationManagerStatics);
269 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
271 return toastNotificationManagerStatics;
274 ToastNotificationHandler::~ToastNotificationHandler() {
275 if (mImageRequest) {
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");
285 UnregisterHandler();
288 void ToastNotificationHandler::UnregisterHandler() {
289 if (mNotification) {
290 mNotification->remove_Dismissed(mDismissedToken);
291 mNotification->remove_Activated(mActivatedToken);
292 mNotification->remove_Failed(mFailedToken);
295 mNotification = nullptr;
296 mNotifier = nullptr;
298 SendFinished();
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());
315 #endif
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
324 // timeout.
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();
332 nsAutoString tag;
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.
346 tag += mName;
347 } else {
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.
352 tag += mName;
353 } else {
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);
358 nsAutoString uuid;
359 CopyASCIItoUTF16(nsDependentCSubstring(uuidString.get(), len), uuid);
361 tag += u"chrome#notag:"_ns;
362 tag += uuid;
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
368 // range.
369 HashNumber hash = HashString(tag);
370 mWindowsTag.AppendPrintf("%010u", hash);
372 return NS_OK;
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);
381 w.Start();
383 w.StringProperty("action", NS_ConvertUTF16toUTF8(aAction));
385 if (mIsSystemPrincipal) {
386 // Privileged/chrome alerts (not activated by Windows) can have custom
387 // relaunch data.
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));
397 } else {
398 if (!mHostPort.IsEmpty()) {
399 w.StringProperty("launchUrl", NS_ConvertUTF16toUTF8(mHostPort));
403 w.End();
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()) {
415 toastTemplate =
416 mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText03
417 : ToastTemplateType::ToastTemplateType_ToastText03;
418 } else {
419 toastTemplate =
420 mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText04
421 : ToastTemplateType::ToastTemplateType_ToastText04;
424 ComPtr<IXmlDocument> toastXml;
425 toastNotificationManagerStatics->GetTemplateContent(toastTemplate, &toastXml);
427 if (!toastXml) {
428 return nullptr;
431 nsresult ns;
432 HRESULT hr;
433 bool success;
435 if (mHasImage) {
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:
454 success =
455 SetAttribute(image, HStringReference(L"placement"), u"hero"_ns);
456 NS_ENSURE_TRUE(success, nullptr);
457 break;
458 case ImagePlacement::eIcon:
459 success = SetAttribute(image, HStringReference(L"placement"),
460 u"appLogoOverride"_ns);
461 NS_ENSURE_TRUE(success, nullptr);
462 break;
463 case ImagePlacement::eInline:
464 // No attribute placement attribute for inline images.
465 break;
469 ComPtr<IXmlNodeList> toastTextElements;
470 hr = toastXml->GetElementsByTagName(HStringReference(L"text").Get(),
471 &toastTextElements);
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(),
490 &toastElements);
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"),
503 u"reminder"_ns);
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(),
535 &bindingElements);
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"),
547 u"ToastGeneric"_ns);
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);
578 success =
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);
585 if (SUCCEEDED(hr)) {
586 // placement is supported on Windows 10 Anniversary Update or later
587 SetAttribute(placementText, HStringReference(L"placement"),
588 u"attribution"_ns);
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),
600 u"contextmenu"_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
609 // take effect.
610 wantSettings = false;
612 #endif
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.
625 nsString title;
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 "
631 "been replaced."));
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 "
640 "been replaced."));
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);
661 nsString actionArgs;
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`");
670 } else {
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,
695 rv);
696 NS_ENSURE_TRUE(!rv.Failed(), nullptr);
698 NS_ENSURE_TRUE(
699 AddActionNode(toastXml, actionsNode, NS_ConvertUTF8toUTF16(closeTitle),
700 u""_ns, u"dismiss"_ns, u""_ns, u"system"_ns),
701 nullptr);
704 ComPtr<IXmlNode> appendedChild;
705 hr = toastNodeRoot->AppendChild(actionsNode.Get(), &appendedChild);
706 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
708 if (mIsSilent) {
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);
723 return toastXml;
726 nsresult ToastNotificationHandler::CreateToastXmlString(
727 const nsAString& aImageURL, nsAString& aString) {
728 HRESULT hr;
730 if (!aImageURL.IsEmpty()) {
731 // For testing: don't fetch and write image to disk, just include the URL.
732 mHasImage = true;
733 mImageUri.Assign(aImageURL);
736 ComPtr<IXmlDocument> toastXml = CreateToastXmlDocument();
737 if (!toastXml) {
738 return NS_ERROR_FAILURE;
741 ComPtr<IXmlNodeSerializer> ser;
742 hr = toastXml.As(&ser);
743 NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE);
745 HString data;
746 hr = ser->GetXml(data.GetAddressOf());
747 NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE);
749 uint32_t len = 0;
750 const wchar_t* rawData = data.GetRawBuffer(&len);
751 NS_ENSURE_TRUE(rawData, NS_ERROR_FAILURE);
752 aString.Assign(rawData, len);
754 return NS_OK;
757 bool ToastNotificationHandler::ShowAlert() {
758 if (!mBackend->IsActiveHandler(mName, this)) {
759 return false;
762 ComPtr<IXmlDocument> toastXml = CreateToastXmlDocument();
764 if (!toastXml) {
765 return false;
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;
782 HRESULT hr;
784 hr = GetActivationFactory(
785 HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification)
786 .Get(),
787 &factory);
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));
800 }).Get(),
801 &mActivatedToken);
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));
809 }).Get(),
810 &mDismissedToken);
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));
818 }).Get(),
819 &mFailedToken);
820 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
822 ComPtr<IToastNotification2> notification2;
823 hr = mNotification.As(&notification2);
824 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
826 HString hTag;
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);
837 HString aumid;
838 hr = aumid.Set(mAumid.get());
839 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
840 hr = toastNotificationManagerStatics->CreateToastNotifierWithId(aumid.Get(),
841 &mNotifier);
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());
851 return true;
854 void ToastNotificationHandler::SendFinished() {
855 if (!mSentFinished && mAlertListener) {
856 mAlertListener->Observe(nullptr, "alertfinished", mCookie.get());
859 mSentFinished = true;
862 HRESULT
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;
872 if (inspectable) {
873 ComPtr<IToastActivatedEventArgs> eventArgs;
874 HRESULT hr = inspectable.As(&eventArgs);
875 if (SUCCEEDED(hr)) {
876 HString arguments;
877 hr = eventArgs->get_Arguments(arguments.GetAddressOf());
878 if (SUCCEEDED(hr)) {
879 uint32_t len = 0;
880 const char16_t* buffer = (char16_t*)arguments.GetRawBuffer(&len);
881 if (buffer) {
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(),
898 actionString);
899 } else {
900 // Next line is a value in a key/value pair, skip.
901 parse.SkipUntil(Tokenizer16::Token::NewLine());
903 // Skip 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.
918 SendFinished();
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));
928 if (winMediator) {
929 nsCOMPtr<mozIDOMWindowProxy> navWin;
930 winMediator->GetMostRecentBrowserWindow(getter_AddRefs(navWin));
931 if (navWin) {
932 nsCOMPtr<nsIWidget> widget =
933 WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin));
934 if (widget) {
935 SetForegroundWindow(
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(),
946 jsonData, false)) {
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);
960 return S_OK;
963 // Returns `nullptr` if no such toast exists.
964 /* static */ ComPtr<IToastNotification>
965 ToastNotificationHandler::FindNotificationByTag(const nsAString& aWindowsTag,
966 const nsAString& aAumid) {
967 HRESULT hr = S_OK;
969 HString current_id;
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))) {
999 continue;
1002 ComPtr<IToastNotification2> hist_toast2;
1003 hr = hist_toast.As(&hist_toast2);
1004 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
1006 HString history_id;
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) {
1014 return hist_toast;
1018 return nullptr;
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.
1026 HRESULT
1027 ToastNotificationHandler::OnDismiss(
1028 const ComPtr<IToastNotification>& notification,
1029 const ComPtr<IToastDismissedEventArgs>& aArgs) {
1030 ComPtr<IToastNotification2> notification2;
1031 HRESULT hr = notification.As(&notification2);
1032 NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL);
1034 HString tagHString;
1035 hr = notification2->get_Tag(tagHString.GetAddressOf());
1036 NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL);
1038 unsigned int len;
1039 const wchar_t* tagPtr = tagHString.GetRawBuffer(&len);
1040 nsAutoString tag(tagPtr, len);
1042 if (FindNotificationByTag(tag, mAumid)) {
1043 return S_OK;
1046 SendFinished();
1047 mBackend->RemoveHandler(mName, this);
1048 return S_OK;
1051 HRESULT
1052 ToastNotificationHandler::OnFail(const ComPtr<IToastNotification>& notification,
1053 const ComPtr<IToastFailedEventArgs>& aArgs) {
1054 HRESULT err;
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());
1063 SendFinished();
1064 mBackend->RemoveHandler(mName, this);
1065 return S_OK;
1068 nsresult ToastNotificationHandler::TryShowAlert() {
1069 if (NS_WARN_IF(!ShowAlert())) {
1070 mBackend->RemoveHandler(mName, this);
1071 return NS_ERROR_FAILURE;
1073 return NS_OK;
1076 NS_IMETHODIMP
1077 ToastNotificationHandler::OnImageMissing(nsISupports*) {
1078 return TryShowAlert();
1081 NS_IMETHODIMP
1082 ToastNotificationHandler::OnImageReady(nsISupports*, imgIRequest* aRequest) {
1083 nsresult rv = AsyncSaveImage(aRequest);
1084 if (NS_FAILED(rv)) {
1085 return TryShowAlert();
1087 return rv;
1090 nsresult ToastNotificationHandler::AsyncSaveImage(imgIRequest* aRequest) {
1091 nsresult rv =
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) {
1100 return rv;
1103 nsID uuid;
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;
1127 if (surface) {
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);
1133 fclose(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();
1154 TryShowAlert();
1157 nsresult ToastNotificationHandler::OnWriteImageSuccess() {
1158 nsresult rv;
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);
1170 mHasImage = true;
1172 return NS_OK;
1175 } // namespace widget
1176 } // namespace mozilla